Skip to content

Commit 0d8f3d9

Browse files
Merge pull request #7 from pipedrive/AINATIVEM-44
AINATIVEM-44 OAuth callback, token repository, and pipedrive client generator
2 parents e8fff8b + dc2f6ca commit 0d8f3d9

21 files changed

Lines changed: 643 additions & 242 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"lint": "eslint src",
1313
"test": "vitest run",
1414
"typecheck": "tsc --noEmit",
15+
"clean": "rm -rf apps/",
1516
"generate": "tsx src/cli.ts apps/test-app",
1617
"prepare": "husky"
1718
},

src/cli.test.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,13 @@ import { pathToFileURL } from 'node:url';
33
import { isCliEntrypoint, nextStepLines } from './cli.js';
44

55
describe('nextStepLines', () => {
6-
it('prints backend-only next steps for apps without App Extensions', () => {
7-
expect(nextStepLines({ nameOrPath: 'test-app', database: 'sqlite', installDeps: false, hasAppExtensions: false }).join('\n')).toBe(`
6+
it('outputs the four next steps', () => {
7+
expect(nextStepLines({ nameOrPath: 'test-app' }).join('\n')).toBe(`
88
Next steps:
99
cd test-app
1010
cp .env.example .env
11-
npm install
12-
npm run dev`);
13-
});
14-
15-
it('prints the Compose Watch command when App Extensions are selected', () => {
16-
expect(nextStepLines({ nameOrPath: 'test-app', database: 'postgres', installDeps: true, hasAppExtensions: true }).join('\n')).toBe(`
17-
Next steps:
18-
cd test-app
19-
cp .env.example .env
20-
docker-compose up --watch`);
11+
# fill in PIPEDRIVE_CLIENT_ID and PIPEDRIVE_CLIENT_SECRET
12+
docker compose up`);
2113
});
2214
});
2315

src/cli.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
11
import * as clack from '@clack/prompts';
2-
import { spawn } from 'node:child_process';
32
import { realpathSync } from 'node:fs';
43
import { basename, resolve } from 'node:path';
54
import { fileURLToPath, pathToFileURL } from 'node:url';
65
import { promptAppExtensions } from './prompts/appExtensions.js';
76
import { promptDatabase } from './prompts/database.js';
87
import { promptProjectName } from './prompts/projectName.js';
9-
import { promptWebhooks } from './prompts/webhooks.js';
108
import { nodeGenerator } from './generators/node/index.js';
11-
import type { Database } from './generators/interface.js';
129

1310
interface NextStepOptions {
1411
nameOrPath: string;
15-
database: Database;
16-
installDeps: boolean;
17-
hasAppExtensions: boolean;
1812
}
1913

2014
export function nextStepLines(options: NextStepOptions): string[] {
21-
const needsDocker = options.database === 'postgres' || options.database === 'mysql';
22-
const runWithCompose = options.hasAppExtensions;
23-
24-
const steps = [`cd ${options.nameOrPath}`, 'cp .env.example .env'];
25-
26-
if (runWithCompose) {
27-
steps.push('docker-compose up --watch');
28-
} else {
29-
if (needsDocker) steps.push('docker-compose up -d db');
30-
if (!options.installDeps) steps.push('npm install');
31-
steps.push('npm run dev');
32-
}
15+
const steps = [
16+
`cd ${options.nameOrPath}`,
17+
'cp .env.example .env',
18+
'# fill in PIPEDRIVE_CLIENT_ID and PIPEDRIVE_CLIENT_SECRET',
19+
'docker compose up',
20+
];
3321

3422
return ['', 'Next steps:', ...steps.map((s) => ` ${s}`)];
3523
}
@@ -62,39 +50,20 @@ async function main(): Promise<void> {
6250
const nameOrPath = await promptProjectName(process.argv[2]);
6351
const database = await promptDatabase();
6452
const appExtensions = await promptAppExtensions();
65-
const webhooks = await promptWebhooks();
6653

6754
const outputDir = resolve(process.cwd(), nameOrPath);
6855
const projectName = basename(outputDir);
6956

7057
try {
71-
await nodeGenerator.generate(outputDir, { projectName, database, appExtensions, webhooks });
58+
await nodeGenerator.generate(outputDir, { projectName, database, appExtensions });
7259
} catch (error) {
7360
clack.log.error(`Generation failed: ${error instanceof Error ? error.message : String(error)}`);
7461
process.exit(1);
7562
}
7663

7764
clack.outro(`✓ Created ${projectName}`);
7865

79-
const installDeps = await clack.confirm({ message: 'Install dependencies now?' });
80-
if (clack.isCancel(installDeps)) process.exit(0);
81-
82-
if (installDeps) {
83-
const spinner = clack.spinner();
84-
spinner.start('Installing dependencies');
85-
const ok = await new Promise<boolean>((resolve) => {
86-
const child = spawn('npm', ['install'], { cwd: outputDir, stdio: 'ignore' });
87-
child.on('close', (code) => resolve(code === 0));
88-
});
89-
spinner.stop(ok ? 'Dependencies installed' : 'npm install failed — run it manually');
90-
}
91-
92-
printNextSteps({
93-
nameOrPath,
94-
database,
95-
installDeps: Boolean(installDeps),
96-
hasAppExtensions: appExtensions.length > 0,
97-
});
66+
printNextSteps({ nameOrPath });
9867
}
9968

10069
if (isCliEntrypoint(import.meta.url, process.argv[1])) {

src/generators/interface.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export type AppExtensionType = 'custom-panel' | 'custom-modal';
44
export interface GeneratorOptions {
55
projectName: string;
66
database: Database;
7-
webhooks: boolean;
87
appExtensions: AppExtensionType[];
98
}
109

src/generators/node/app.test.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,39 @@ describe('generateApp', () => {
2121
const content = await getAppContent({
2222
projectName: 'test-app',
2323
database: 'postgres',
24-
webhooks: false,
2524
appExtensions: [],
2625
});
2726
expect(content).toContain("from 'express'");
2827
expect(content).toContain("from './oauth/index.js'");
28+
expect(content).toContain('createAuthRedirect');
2929
expect(content).toContain("app.use('/oauth'");
3030
});
3131

32-
it('includes webhooks import and mount when webhooks is true', async () => {
32+
it('has root route that redirects to oauth when not installed', async () => {
3333
const content = await getAppContent({
3434
projectName: 'test-app',
3535
database: 'postgres',
36-
webhooks: true,
3736
appExtensions: [],
3837
});
39-
expect(content).toContain("from './webhooks/index.js'");
40-
expect(content).toContain("app.use('/webhooks'");
38+
expect(content).toContain("app.get('/'");
39+
expect(content).toContain('createAuthRedirect()');
40+
expect(content).toContain('client.deals.getDeals()');
4141
});
4242

43-
it('excludes webhooks when webhooks is false', async () => {
43+
it('has global error handler', async () => {
4444
const content = await getAppContent({
4545
projectName: 'test-app',
4646
database: 'postgres',
47-
webhooks: false,
4847
appExtensions: [],
4948
});
50-
expect(content).not.toContain('./webhooks/index.js');
49+
expect(content).toContain('NextFunction');
50+
expect(content).toContain('res.status(500)');
5151
});
5252

5353
it('includes panel import and mount when custom-panel is selected', async () => {
5454
const content = await getAppContent({
5555
projectName: 'test-app',
5656
database: 'postgres',
57-
webhooks: false,
5857
appExtensions: ['custom-panel'],
5958
});
6059
expect(content).toContain("from './app-extensions/panel/index.js'");
@@ -67,7 +66,6 @@ describe('generateApp', () => {
6766
const content = await getAppContent({
6867
projectName: 'test-app',
6968
database: 'postgres',
70-
webhooks: false,
7169
appExtensions: ['custom-modal'],
7270
});
7371
expect(content).toContain("from './app-extensions/modal/index.js'");
@@ -78,7 +76,6 @@ describe('generateApp', () => {
7876
const content = await getAppContent({
7977
projectName: 'test-app',
8078
database: 'sqlite',
81-
webhooks: false,
8279
appExtensions: [],
8380
});
8481
expect(content).not.toContain('./app-extensions');

src/generators/node/app.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dedent from 'dedent';
12
import { join } from 'path';
23
import { writeFile } from '../../utils/writeFile.js';
34
import type { GeneratorOptions } from '../interface.js';
@@ -11,25 +12,55 @@ export async function generateApp(outputDir: string, options: GeneratorOptions):
1112

1213
const mounts = new RouterMountBuilder()
1314
.add('/oauth', 'oauthRouter')
14-
.addIf(options.webhooks, '/webhooks', 'webhooksRouter')
1515
.addIf(hasPanel, '/extensions/panel', 'panelRouter')
1616
.addIf(hasModal, '/extensions/modal', 'modalRouter')
1717
.build();
1818

19+
const rootRoute = dedent`
20+
app.get('/', async (_req, res, next) => {
21+
try {
22+
const rows = await db.select().from(pipedriveTokens).orderBy(desc(pipedriveTokens.updatedAt)).limit(1);
23+
if (!rows[0]) {
24+
res.redirect(createAuthRedirect());
25+
return;
26+
}
27+
const client = await getClient(rows[0].pipedriveCompanyId);
28+
const deals = await client.deals.getDeals();
29+
res.json(deals);
30+
} catch (err) {
31+
next(err);
32+
}
33+
});
34+
`;
35+
36+
const errorHandler = dedent`
37+
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
38+
console.error(err);
39+
res.status(500).send(err.message);
40+
});
41+
`;
42+
1943
const content = new SourceFileBuilder()
2044
.importDefault('express', 'express')
45+
.import('express', ['NextFunction', 'Request', 'Response'])
2146
.importIf(hasAppExtensions, 'node:path', ['join'])
2247
.importDefault('./oauth/index.js', 'oauthRouter')
23-
.importDefaultIf(options.webhooks, './webhooks/index.js', 'webhooksRouter')
48+
.import('./oauth/index.js', ['createAuthRedirect'])
49+
.import('./pipedrive/client.js', ['getClient'])
50+
.import('./database/index.js', ['db'])
51+
.import('./database/schema.js', ['pipedriveTokens'])
52+
.import('drizzle-orm', ['desc'])
2453
.importDefaultIf(hasPanel, './app-extensions/panel/index.js', 'panelRouter')
2554
.importDefaultIf(hasModal, './app-extensions/modal/index.js', 'modalRouter')
2655
.addBlock('const app = express();')
56+
.addBlock(rootRoute)
2757
.addBlockIf(
2858
hasAppExtensions,
2959
"const appExtensionAssetsPath = join(process.cwd(), 'frontend/app-extension-ui/dist/assets');",
3060
)
3161
.addBlockIf(hasAppExtensions, "app.use('/extensions/assets', express.static(appExtensionAssetsPath));")
3262
.addBlock(mounts)
63+
.addBlock(errorHandler)
3364
.exportDefault('app')
3465
.build();
3566

src/generators/node/appExtensions.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ describe('generateAppExtensions', () => {
2121
const options: GeneratorOptions = {
2222
projectName: 'test-app',
2323
database: 'postgres',
24-
webhooks: false,
2524
appExtensions: ['custom-panel'],
2625
};
2726
await generateAppExtensions(tmpDir, options);
@@ -40,7 +39,6 @@ describe('generateAppExtensions', () => {
4039
const options: GeneratorOptions = {
4140
projectName: 'test-app',
4241
database: 'postgres',
43-
webhooks: false,
4442
appExtensions: ['custom-modal'],
4543
};
4644
await generateAppExtensions(tmpDir, options);
@@ -58,7 +56,6 @@ describe('generateAppExtensions', () => {
5856
const options: GeneratorOptions = {
5957
projectName: 'test-app',
6058
database: 'postgres',
61-
webhooks: false,
6259
appExtensions: ['custom-panel', 'custom-modal'],
6360
};
6461
await generateAppExtensions(tmpDir, options);

0 commit comments

Comments
 (0)