nango 0.59.13 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +64 -10
- package/dist/index.js.map +1 -1
- package/dist/migrations/toZeroYaml.d.ts +50 -0
- package/dist/migrations/toZeroYaml.js +972 -0
- package/dist/migrations/toZeroYaml.js.map +1 -0
- package/dist/migrations/toZeroYaml.unit.test.js +298 -0
- package/dist/migrations/toZeroYaml.unit.test.js.map +1 -0
- package/dist/services/dryrun.service.d.ts +5 -4
- package/dist/services/dryrun.service.js +77 -23
- package/dist/services/dryrun.service.js.map +1 -1
- package/dist/services/sdk.js +2 -1
- package/dist/services/sdk.js.map +1 -1
- package/dist/services/sdk.service.unit.test.js +2 -1
- package/dist/services/sdk.service.unit.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/zeroYaml/compile.d.ts +8 -0
- package/dist/zeroYaml/compile.js +22 -71
- package/dist/zeroYaml/compile.js.map +1 -1
- package/dist/zeroYaml/compile.unit.cli-test.d.ts +1 -0
- package/dist/zeroYaml/{compile.unit.test.js → compile.unit.cli-test.js} +2 -1
- package/dist/zeroYaml/compile.unit.cli-test.js.map +1 -0
- package/dist/zeroYaml/constants.d.ts +18 -0
- package/dist/zeroYaml/constants.js +62 -0
- package/dist/zeroYaml/constants.js.map +1 -0
- package/dist/zeroYaml/definitions.d.ts +2 -1
- package/dist/zeroYaml/definitions.js +7 -11
- package/dist/zeroYaml/definitions.js.map +1 -1
- package/dist/zeroYaml/definitions.unit.test.js +5 -2
- package/dist/zeroYaml/definitions.unit.test.js.map +1 -1
- package/dist/zeroYaml/deploy.d.ts +7 -0
- package/dist/zeroYaml/deploy.js +426 -0
- package/dist/zeroYaml/deploy.js.map +1 -0
- package/dist/zeroYaml/dev.d.ts +4 -0
- package/dist/zeroYaml/dev.js +297 -0
- package/dist/zeroYaml/dev.js.map +1 -0
- package/dist/zeroYaml/init.js +6 -2
- package/dist/zeroYaml/init.js.map +1 -1
- package/dist/zeroYaml/utils.d.ts +3 -0
- package/dist/zeroYaml/utils.js +7 -0
- package/dist/zeroYaml/utils.js.map +1 -0
- package/dist/zeroYaml/zodToNango.js +1 -1
- package/dist/zeroYaml/zodToNango.unit.test.js +5 -3
- package/dist/zeroYaml/zodToNango.unit.test.js.map +1 -1
- package/example/.env.example +16 -0
- package/example/.nango/.gitkeep +0 -0
- package/example/github/actions/createIssue.ts +1 -1
- package/example/github/syncs/fetchIssues.ts +3 -4
- package/example/index.ts +3 -3
- package/example/tsconfig.json +30 -0
- package/package.json +18 -16
- package/dist/zeroYaml/compile.unit.test.js.map +0 -1
- /package/dist/{zeroYaml/compile.unit.test.d.ts → migrations/toZeroYaml.unit.test.d.ts} +0 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import jscodeshift from 'jscodeshift';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { Err, Ok } from '../utils/result.js';
|
|
9
|
+
import { printDebug } from '../utils.js';
|
|
10
|
+
import { NANGO_VERSION } from '../version.js';
|
|
11
|
+
import { compileAll } from '../zeroYaml/compile.js';
|
|
12
|
+
import { compileAllFiles } from '../services/compile.service.js';
|
|
13
|
+
import { loadYamlAndGenerate } from '../services/model.service.js';
|
|
14
|
+
import { exampleFolder } from '../zeroYaml/constants.js';
|
|
15
|
+
import { syncTsConfig } from '../zeroYaml/utils.js';
|
|
16
|
+
const allowedTypesImports = ['ActionError', 'ProxyConfiguration'];
|
|
17
|
+
const batchMethods = ['batchSave', 'batchUpdate', 'batchDelete'];
|
|
18
|
+
export async function migrateToZeroYaml({ fullPath, debug }) {
|
|
19
|
+
const spinner = ora({ text: 'Precompiling' }).start();
|
|
20
|
+
const { success } = await compileAllFiles({ fullPath, debug });
|
|
21
|
+
if (!success) {
|
|
22
|
+
spinner.fail();
|
|
23
|
+
console.log(chalk.red('Failed to compile. Exiting'));
|
|
24
|
+
return Err('failed_to_precompile');
|
|
25
|
+
}
|
|
26
|
+
const parsed = loadYamlAndGenerate({ fullPath, debug });
|
|
27
|
+
if (!parsed) {
|
|
28
|
+
return Err('failed_to_parse');
|
|
29
|
+
}
|
|
30
|
+
spinner.succeed();
|
|
31
|
+
{
|
|
32
|
+
const spinner = ora({ text: 'Init folder' }).start();
|
|
33
|
+
await addPackageJson({ fullPath, debug });
|
|
34
|
+
await syncTsConfig({ fullPath });
|
|
35
|
+
spinner.succeed();
|
|
36
|
+
}
|
|
37
|
+
{
|
|
38
|
+
const spinner = ora({ text: 'Generating models.ts' }).start();
|
|
39
|
+
const content = generateModelsTs({ parsed });
|
|
40
|
+
// Write to models.ts
|
|
41
|
+
const modelsPath = path.join(fullPath, 'models.ts');
|
|
42
|
+
await fs.promises.writeFile(modelsPath, content);
|
|
43
|
+
spinner.succeed();
|
|
44
|
+
}
|
|
45
|
+
console.log('Processing scripts');
|
|
46
|
+
for (const integration of parsed.integrations) {
|
|
47
|
+
for (const sync of integration.syncs) {
|
|
48
|
+
const fp = path.join(integration.providerConfigKey, 'syncs', `${sync.name}.ts`);
|
|
49
|
+
const targetFile = path.join(fullPath, fp);
|
|
50
|
+
const spinner = ora({ text: `Migrating: ${fp}` }).start();
|
|
51
|
+
try {
|
|
52
|
+
if (await hasSymlinkInPath(targetFile, fullPath)) {
|
|
53
|
+
spinner.warn('Skipping symlink');
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const content = await getContent({ targetFile });
|
|
57
|
+
const transformed = transformSync({ content, sync, models: parsed.models });
|
|
58
|
+
await fs.promises.writeFile(targetFile, transformed);
|
|
59
|
+
spinner.succeed();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
spinner.fail();
|
|
63
|
+
console.error(chalk.red(err));
|
|
64
|
+
return Err('failed_to_compile_one_file');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const action of integration.actions) {
|
|
68
|
+
const fp = path.join(integration.providerConfigKey, 'actions', `${action.name}.ts`);
|
|
69
|
+
const targetFile = path.join(fullPath, fp);
|
|
70
|
+
const spinner = ora({ text: `Migrating: ${fp}` }).start();
|
|
71
|
+
try {
|
|
72
|
+
if (await hasSymlinkInPath(targetFile, fullPath)) {
|
|
73
|
+
spinner.warn('Skipping symlink');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const content = await getContent({ targetFile });
|
|
77
|
+
const transformed = transformAction({ content, action, models: parsed.models });
|
|
78
|
+
await fs.promises.writeFile(targetFile, transformed);
|
|
79
|
+
spinner.succeed();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
spinner.fail();
|
|
83
|
+
console.error(chalk.red(err));
|
|
84
|
+
return Err('failed_to_compile_one_file');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const onEventScript of Object.entries(integration.onEventScripts)) {
|
|
88
|
+
for (const eventName of onEventScript[1]) {
|
|
89
|
+
const fp = path.join(integration.providerConfigKey, 'on-events', `${eventName}.ts`);
|
|
90
|
+
const targetFile = path.join(fullPath, fp);
|
|
91
|
+
const spinner = ora({ text: `Migrating: ${fp}` }).start();
|
|
92
|
+
try {
|
|
93
|
+
if (await hasSymlinkInPath(targetFile, fullPath)) {
|
|
94
|
+
spinner.warn('Skipping symlink');
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const content = await getContent({ targetFile });
|
|
98
|
+
const transformed = transformOnEvents({ eventType: onEventScript[0], content, models: parsed.models });
|
|
99
|
+
await fs.promises.writeFile(targetFile, transformed);
|
|
100
|
+
spinner.succeed();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
spinner.fail();
|
|
104
|
+
console.error(chalk.red(err));
|
|
105
|
+
return Err('failed_to_compile_one_file');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// After migration, process all remaining .ts files in fullPath
|
|
111
|
+
{
|
|
112
|
+
console.log('Processing helper files');
|
|
113
|
+
await processHelperFiles({ fullPath, parsed });
|
|
114
|
+
}
|
|
115
|
+
{
|
|
116
|
+
const spinner = ora({ text: 'Running npm install' }).start();
|
|
117
|
+
await runNpmInstall(fullPath);
|
|
118
|
+
spinner.succeed();
|
|
119
|
+
}
|
|
120
|
+
{
|
|
121
|
+
const spinner = ora({ text: 'Generating index.ts' }).start();
|
|
122
|
+
await generateIndexTs({ fullPath, parsed });
|
|
123
|
+
spinner.succeed();
|
|
124
|
+
}
|
|
125
|
+
{
|
|
126
|
+
const spinner = ora({ text: 'Deleting nango.yaml' }).start();
|
|
127
|
+
await fs.promises.rm(path.join(fullPath, 'nango.yaml'));
|
|
128
|
+
spinner.succeed();
|
|
129
|
+
}
|
|
130
|
+
{
|
|
131
|
+
await compileAll({ fullPath, debug });
|
|
132
|
+
}
|
|
133
|
+
return Ok(undefined);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get script content
|
|
137
|
+
*/
|
|
138
|
+
async function getContent({ targetFile }) {
|
|
139
|
+
const res = await fs.promises.readFile(targetFile);
|
|
140
|
+
return res.toString();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Runs npm install in the given directory
|
|
144
|
+
*/
|
|
145
|
+
async function runNpmInstall(fullPath) {
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
const proc = spawn('npm', ['install', '--no-audit', '--no-fund', '--no-progress'], {
|
|
148
|
+
cwd: fullPath,
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
shell: true
|
|
151
|
+
});
|
|
152
|
+
proc.on('close', (code) => {
|
|
153
|
+
if (code === 0) {
|
|
154
|
+
resolve(undefined);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
reject(new Error(`npm install failed with exit code ${code}`));
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Helper to remove type annotations from parameters and build an exec property for jscodeshift AST nodes
|
|
164
|
+
*/
|
|
165
|
+
function buildExecProp(j, func, execReturnType) {
|
|
166
|
+
let params;
|
|
167
|
+
let bodyNode;
|
|
168
|
+
let isAsync = false;
|
|
169
|
+
if (func.type === 'FunctionDeclaration') {
|
|
170
|
+
params = func.params.map((param) => {
|
|
171
|
+
if (param.type === 'Identifier') {
|
|
172
|
+
return j.identifier(param.name);
|
|
173
|
+
}
|
|
174
|
+
return param;
|
|
175
|
+
});
|
|
176
|
+
bodyNode = func.body;
|
|
177
|
+
isAsync = !!func.async;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
params = func.params;
|
|
181
|
+
bodyNode = func.body;
|
|
182
|
+
isAsync = !!func.async;
|
|
183
|
+
}
|
|
184
|
+
params[0].extra = { ...(params[0].extra || {}), parenthesized: true };
|
|
185
|
+
const execArrow = j.arrowFunctionExpression(params, bodyNode);
|
|
186
|
+
execArrow.async = isAsync;
|
|
187
|
+
// Add return type if provided
|
|
188
|
+
if (execReturnType) {
|
|
189
|
+
execArrow.returnType = j.tsTypeAnnotation(j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([j.tsTypeReference(j.identifier(execReturnType))])));
|
|
190
|
+
}
|
|
191
|
+
return j.objectProperty(j.identifier('exec'), execArrow);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Transforms a sync to zero-yaml
|
|
195
|
+
*/
|
|
196
|
+
export function transformSync({ content, sync, models }) {
|
|
197
|
+
const j = jscodeshift.withParser('ts');
|
|
198
|
+
const root = j(content);
|
|
199
|
+
removeLegacyImports({ root, j });
|
|
200
|
+
// Add import { createSync } from 'nango'
|
|
201
|
+
const importDecl = j.importDeclaration([j.importSpecifier(j.identifier('createSync'))], j.literal('nango'));
|
|
202
|
+
root.get().node.program.body.unshift(importDecl);
|
|
203
|
+
// Remove batch type arguments from all functions
|
|
204
|
+
removeBatchTypeArguments({ root, j });
|
|
205
|
+
// Wrap default function
|
|
206
|
+
root.find(j.ExportDefaultDeclaration).forEach((path) => {
|
|
207
|
+
const func = path.node.declaration;
|
|
208
|
+
if (func.type !== 'FunctionDeclaration') {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Preserve leading comments
|
|
212
|
+
const leadingComments = func.leadingComments || path.node.leadingComments;
|
|
213
|
+
const execProp = buildExecProp(j, func);
|
|
214
|
+
// Creats default props
|
|
215
|
+
const descriptionProp = j.objectProperty(j.identifier('description'), j.stringLiteral(sync.description));
|
|
216
|
+
const versionProp = j.objectProperty(j.identifier('version'), j.stringLiteral(sync.version || '0.0.1'));
|
|
217
|
+
const frequencyProp = j.objectProperty(j.identifier('frequency'), j.stringLiteral(sync.runs));
|
|
218
|
+
const autoStartProp = j.objectProperty(j.identifier('autoStart'), j.booleanLiteral(sync.auto_start));
|
|
219
|
+
const syncTypeProp = j.objectProperty(j.identifier('syncType'), j.stringLiteral(sync.sync_type));
|
|
220
|
+
const trackDeletesProp = j.objectProperty(j.identifier('trackDeletes'), j.booleanLiteral(sync.track_deletes));
|
|
221
|
+
const endpointsProp = j.objectProperty(j.identifier('endpoints'), j.arrayExpression(sync.endpoints.map((ep) => j.objectExpression(Object.entries(ep)
|
|
222
|
+
.map(([k, v]) => {
|
|
223
|
+
if (typeof v === 'string') {
|
|
224
|
+
return j.objectProperty(j.identifier(k), j.stringLiteral(v));
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
})
|
|
228
|
+
.filter((prop) => !!prop)))));
|
|
229
|
+
const props = [descriptionProp, versionProp, frequencyProp, autoStartProp, syncTypeProp, trackDeletesProp, endpointsProp];
|
|
230
|
+
if (sync.webhookSubscriptions.length > 0) {
|
|
231
|
+
const webhookSubscriptionsProp = j.objectProperty(j.identifier('webhookSubscriptions'), Array.isArray(sync.webhookSubscriptions) ? j.arrayExpression(sync.webhookSubscriptions.map((w) => j.stringLiteral(w))) : j.arrayExpression([]));
|
|
232
|
+
props.push(webhookSubscriptionsProp);
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(sync.scopes) && sync.scopes.length > 0) {
|
|
235
|
+
const scopesProp = j.objectProperty(j.identifier('scopes'), j.arrayExpression(sync.scopes.map((s) => j.stringLiteral(s))));
|
|
236
|
+
props.push(scopesProp);
|
|
237
|
+
}
|
|
238
|
+
if (sync.output) {
|
|
239
|
+
const modelProps = sync.output
|
|
240
|
+
.map((modelName) => {
|
|
241
|
+
return j.objectProperty(j.identifier(modelName), j.identifier(modelName));
|
|
242
|
+
})
|
|
243
|
+
.filter((prop) => !!prop);
|
|
244
|
+
if (modelProps.length > 0) {
|
|
245
|
+
const modelsProp = j.objectProperty(j.identifier('models'), j.objectExpression(modelProps));
|
|
246
|
+
props.push(modelsProp);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (sync.input) {
|
|
250
|
+
const metadataProp = j.objectProperty(j.identifier('metadata'), j.identifier(sync.input));
|
|
251
|
+
props.push(metadataProp);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Default to z.object({}) if no input to avoid more errors on migration
|
|
255
|
+
const metadataProp = j.objectProperty(j.identifier('metadata'), j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('object')), [j.objectExpression([])]));
|
|
256
|
+
props.push(metadataProp);
|
|
257
|
+
}
|
|
258
|
+
// Add exec prop
|
|
259
|
+
props.push(execProp);
|
|
260
|
+
// Find and move onWebhookPayloadReceived if present
|
|
261
|
+
let onWebhookArrow = null;
|
|
262
|
+
let onWebhookComments = null;
|
|
263
|
+
root.find(j.ExportNamedDeclaration).forEach((p) => {
|
|
264
|
+
const decl = p.node.declaration;
|
|
265
|
+
if (decl && decl.type === 'FunctionDeclaration' && decl.id && decl.id.name === 'onWebhookPayloadReceived') {
|
|
266
|
+
const webhookExecProp = buildExecProp(j, decl);
|
|
267
|
+
// Extract the arrow function from the exec property
|
|
268
|
+
onWebhookArrow = webhookExecProp.value;
|
|
269
|
+
// Capture comments
|
|
270
|
+
onWebhookComments = decl.leadingComments || p.node.leadingComments;
|
|
271
|
+
// Remove the original function declaration
|
|
272
|
+
j(p).remove();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (onWebhookArrow) {
|
|
276
|
+
const onWebhookProp = j.objectProperty(j.identifier('onWebhook'), onWebhookArrow);
|
|
277
|
+
if (onWebhookComments) {
|
|
278
|
+
onWebhookProp.comments = onWebhookComments;
|
|
279
|
+
}
|
|
280
|
+
props.push(onWebhookProp);
|
|
281
|
+
}
|
|
282
|
+
const obj = j.objectExpression(props);
|
|
283
|
+
const syncVar = j.variableDeclaration('const', [j.variableDeclarator(j.identifier('sync'), j.callExpression(j.identifier('createSync'), [obj]))]);
|
|
284
|
+
if (leadingComments) {
|
|
285
|
+
syncVar.comments = leadingComments;
|
|
286
|
+
}
|
|
287
|
+
const nangoType = createNangoLocalType({ j, name: 'NangoSyncLocal', variable: 'sync' });
|
|
288
|
+
const exportDefault = j.exportDefaultDeclaration(j.identifier('sync'));
|
|
289
|
+
path.replace(syncVar, nangoType, exportDefault);
|
|
290
|
+
});
|
|
291
|
+
// Replace all type references to NangoAction with new type Nango
|
|
292
|
+
root.find(j.TSTypeReference).forEach((path) => {
|
|
293
|
+
if (path.node.typeName.type === 'Identifier' && path.node.typeName.name === 'NangoSync') {
|
|
294
|
+
path.node.typeName.name = 'NangoSyncLocal';
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Find all used Types in the file that might be available in "nango"
|
|
298
|
+
const allModelNames = Array.from(models.keys());
|
|
299
|
+
reImportTypes({ root, j, usedModels: allModelNames });
|
|
300
|
+
const transformed = root.toSource();
|
|
301
|
+
return transformed;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Transforms an action to zero-yaml
|
|
305
|
+
*/
|
|
306
|
+
export function transformAction({ content, action, models }) {
|
|
307
|
+
const j = jscodeshift.withParser('ts');
|
|
308
|
+
const root = j(content);
|
|
309
|
+
removeLegacyImports({ root, j });
|
|
310
|
+
// Add import { createAction } from 'nango'
|
|
311
|
+
const importDecl = j.importDeclaration([j.importSpecifier(j.identifier('createAction'))], j.literal('nango'));
|
|
312
|
+
root.get().node.program.body.unshift(importDecl);
|
|
313
|
+
// Remove batch type arguments from all functions
|
|
314
|
+
removeBatchTypeArguments({ root, j });
|
|
315
|
+
// Find the default export async function (runAction or similar)
|
|
316
|
+
root.find(j.ExportDefaultDeclaration).forEach((path) => {
|
|
317
|
+
const func = path.node.declaration;
|
|
318
|
+
if (func.type !== 'FunctionDeclaration') {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Preserve leading comments
|
|
322
|
+
const leadingComments = func.leadingComments || path.node.leadingComments;
|
|
323
|
+
// Determine output type for exec return type
|
|
324
|
+
let outputType = undefined;
|
|
325
|
+
if (Array.isArray(action.output) && action.output.length > 0 && typeof action.output[0] === 'string') {
|
|
326
|
+
outputType = action.output[0];
|
|
327
|
+
}
|
|
328
|
+
else if (typeof action.output === 'string') {
|
|
329
|
+
outputType = action.output;
|
|
330
|
+
}
|
|
331
|
+
const execProp = buildExecProp(j, func, outputType);
|
|
332
|
+
// Build createAction object
|
|
333
|
+
const descriptionProp = j.objectProperty(j.identifier('description'), j.stringLiteral(action.description));
|
|
334
|
+
const versionProp = j.objectProperty(j.identifier('version'), j.stringLiteral(action.version || '0.0.1'));
|
|
335
|
+
const props = [descriptionProp, versionProp];
|
|
336
|
+
if (action.endpoint) {
|
|
337
|
+
const endpointProp = j.objectProperty(j.identifier('endpoint'), j.objectExpression(Object.entries(action.endpoint)
|
|
338
|
+
.map(([k, v]) => {
|
|
339
|
+
if (typeof v === 'string') {
|
|
340
|
+
return j.objectProperty(j.identifier(k), j.stringLiteral(v));
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
})
|
|
344
|
+
.filter((prop) => !!prop)));
|
|
345
|
+
props.push(endpointProp);
|
|
346
|
+
}
|
|
347
|
+
const inputProp = j.objectProperty(j.identifier('input'), typeof action.input === 'string' ? j.identifier(action.input) : j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('void')), []));
|
|
348
|
+
props.push(inputProp);
|
|
349
|
+
let outputProp = null;
|
|
350
|
+
if (Array.isArray(action.output) && action.output.length > 0 && typeof action.output[0] === 'string') {
|
|
351
|
+
outputProp = j.objectProperty(j.identifier('output'), j.identifier(action.output[0]));
|
|
352
|
+
}
|
|
353
|
+
else if (typeof action.output === 'string') {
|
|
354
|
+
outputProp = j.objectProperty(j.identifier('output'), j.identifier(action.output));
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
outputProp = j.objectProperty(j.identifier('output'), j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('void')), []));
|
|
358
|
+
}
|
|
359
|
+
props.push(outputProp);
|
|
360
|
+
if (Array.isArray(action.scopes) && action.scopes.length > 0) {
|
|
361
|
+
const scopesProp = j.objectProperty(j.identifier('scopes'), j.arrayExpression(action.scopes.map((s) => j.stringLiteral(s))));
|
|
362
|
+
props.push(scopesProp);
|
|
363
|
+
}
|
|
364
|
+
props.push(execProp);
|
|
365
|
+
const obj = j.objectExpression(props);
|
|
366
|
+
const actionVar = j.variableDeclaration('const', [j.variableDeclarator(j.identifier('action'), j.callExpression(j.identifier('createAction'), [obj]))]);
|
|
367
|
+
if (leadingComments) {
|
|
368
|
+
actionVar.comments = leadingComments;
|
|
369
|
+
}
|
|
370
|
+
const nangoType = createNangoLocalType({ j, name: 'NangoActionLocal', variable: 'action' });
|
|
371
|
+
const exportDefault = j.exportDefaultDeclaration(j.identifier('action'));
|
|
372
|
+
path.replace(actionVar, nangoType, exportDefault);
|
|
373
|
+
});
|
|
374
|
+
// Replace all type references to NangoAction with new type Nango
|
|
375
|
+
root.find(j.TSTypeReference).forEach((path) => {
|
|
376
|
+
if (path.node.typeName.type === 'Identifier' && path.node.typeName.name === 'NangoAction') {
|
|
377
|
+
path.node.typeName.name = 'NangoActionLocal';
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
// Find all used Types in the file that might be available in "nango"
|
|
381
|
+
const allModelNames = Array.from(models.keys());
|
|
382
|
+
reImportTypes({ root, j, usedModels: allModelNames });
|
|
383
|
+
const transformed = root.toSource();
|
|
384
|
+
return transformed;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Transforms an on-event script to zero-yaml
|
|
388
|
+
*/
|
|
389
|
+
export function transformOnEvents({ content, eventType, models }) {
|
|
390
|
+
const j = jscodeshift.withParser('ts');
|
|
391
|
+
const root = j(content);
|
|
392
|
+
removeLegacyImports({ root, j });
|
|
393
|
+
// Add import { createOnEvent } from 'nango'
|
|
394
|
+
const importDecl = j.importDeclaration([j.importSpecifier(j.identifier('createOnEvent'))], j.literal('nango'));
|
|
395
|
+
root.get().node.program.body.unshift(importDecl);
|
|
396
|
+
// Remove batch type arguments from all functions
|
|
397
|
+
removeBatchTypeArguments({ root, j });
|
|
398
|
+
// Find the default export async function (onEvent or similar)
|
|
399
|
+
root.find(j.ExportDefaultDeclaration).forEach((path) => {
|
|
400
|
+
const func = path.node.declaration;
|
|
401
|
+
if (func.type !== 'FunctionDeclaration') {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Preserve leading comments
|
|
405
|
+
const leadingComments = func.leadingComments || path.node.leadingComments;
|
|
406
|
+
const execProp = buildExecProp(j, func);
|
|
407
|
+
// Build createOnEvent object
|
|
408
|
+
const eventProp = j.objectProperty(j.identifier('event'), j.stringLiteral(eventType));
|
|
409
|
+
const descriptionProp = j.objectProperty(j.identifier('description'), j.stringLiteral(`${eventType} event handler`));
|
|
410
|
+
const props = [eventProp, descriptionProp, execProp];
|
|
411
|
+
const obj = j.objectExpression(props);
|
|
412
|
+
const exportDefault = j.exportDefaultDeclaration(j.callExpression(j.identifier('createOnEvent'), [obj]));
|
|
413
|
+
if (leadingComments) {
|
|
414
|
+
exportDefault.comments = leadingComments;
|
|
415
|
+
}
|
|
416
|
+
path.replace(exportDefault);
|
|
417
|
+
});
|
|
418
|
+
// Find all used Types in the file that might be available in "nango"
|
|
419
|
+
const allModelNames = Array.from(models.keys());
|
|
420
|
+
reImportTypes({ root, j, usedModels: allModelNames });
|
|
421
|
+
const transformed = root.toSource();
|
|
422
|
+
return transformed;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Removes legacy imports
|
|
426
|
+
*/
|
|
427
|
+
function removeLegacyImports({ root, j }) {
|
|
428
|
+
root.find(j.ImportDeclaration)
|
|
429
|
+
.filter((path) => {
|
|
430
|
+
const source = path.node.source.value;
|
|
431
|
+
return typeof source === 'string' && (/models(\.js)?$/.test(source) || /models(\.js)?$/.test(source.replace(/^.*\//, '')));
|
|
432
|
+
})
|
|
433
|
+
.remove();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Re-imports allowed types if they are used
|
|
437
|
+
*/
|
|
438
|
+
function reImportTypes({ root, j, usedModels }) {
|
|
439
|
+
// Find all used allowedTypesImports in the file
|
|
440
|
+
const usedAllowedTypes = new Set();
|
|
441
|
+
const usedModelTypes = new Set();
|
|
442
|
+
root.find(j.TSTypeReference).forEach((path) => {
|
|
443
|
+
if (path.node.typeName.type !== 'Identifier') {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const name = path.node.typeName.name;
|
|
447
|
+
if (allowedTypesImports.includes(name)) {
|
|
448
|
+
usedAllowedTypes.add(name);
|
|
449
|
+
}
|
|
450
|
+
else if (usedModels.includes(name)) {
|
|
451
|
+
usedModelTypes.add(name);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
// Also find types used in interface extends (e.g., interface X extends ProxyConfiguration)
|
|
455
|
+
root.find(j.TSInterfaceDeclaration).forEach((path) => {
|
|
456
|
+
if (!path.node.extends) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
path.node.extends.forEach((ext) => {
|
|
460
|
+
if (ext.expression.type !== 'Identifier') {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const name = ext.expression.name;
|
|
464
|
+
if (allowedTypesImports.includes(name)) {
|
|
465
|
+
usedAllowedTypes.add(name);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
// Also find implicit model usages in object properties (e.g., { models: { Input: InputSchema } })
|
|
470
|
+
root.find(j.ObjectProperty).forEach((path) => {
|
|
471
|
+
if (path.node.value.type === 'Identifier' && usedModels.includes(path.node.value.name)) {
|
|
472
|
+
usedModelTypes.add(path.node.value.name);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// Also find generic type arguments in CallExpression and NewExpression (e.g., nango.ActionError<ActionErrorResponse>)
|
|
476
|
+
function scanTypeArguments(node) {
|
|
477
|
+
const typeArgs = node.typeArguments || node.typeParameters;
|
|
478
|
+
if (!typeArgs || !typeArgs.params) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
typeArgs.params.forEach((param) => {
|
|
482
|
+
if (param.type === 'TSTypeReference' && param.typeName.type === 'Identifier') {
|
|
483
|
+
const name = param.typeName.name;
|
|
484
|
+
if (allowedTypesImports.includes(name)) {
|
|
485
|
+
usedAllowedTypes.add(name);
|
|
486
|
+
}
|
|
487
|
+
else if (usedModels.includes(name)) {
|
|
488
|
+
usedModelTypes.add(name);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
root.find(j.CallExpression).forEach((path) => scanTypeArguments(path.node));
|
|
494
|
+
root.find(j.NewExpression).forEach((path) => scanTypeArguments(path.node));
|
|
495
|
+
// Fallback: scan for all Identifier nodes that match a model name
|
|
496
|
+
root.find(j.Identifier).forEach((path) => {
|
|
497
|
+
const name = path.node.name;
|
|
498
|
+
if (usedModels.includes(name)) {
|
|
499
|
+
usedModelTypes.add(name);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
// Prepare imports
|
|
503
|
+
const importDecls = [];
|
|
504
|
+
if (usedAllowedTypes.size > 0) {
|
|
505
|
+
const importTypeDecl = j.importDeclaration(Array.from(usedAllowedTypes).map((type) => j.importSpecifier(j.identifier(type))), j.literal('nango'));
|
|
506
|
+
importTypeDecl.importKind = 'type';
|
|
507
|
+
importDecls.push(importTypeDecl);
|
|
508
|
+
}
|
|
509
|
+
if (usedModelTypes.size > 0) {
|
|
510
|
+
importDecls.push(j.importDeclaration([...Array.from(usedModelTypes.values()).map((name) => j.importSpecifier(j.identifier(name)))], j.literal('../../models.js')));
|
|
511
|
+
}
|
|
512
|
+
// Import z if any z.* is used (e.g., z.never())
|
|
513
|
+
const usesZ = root.find(j.Identifier, { name: 'z' }).size() > 0;
|
|
514
|
+
const hasZodImport = root
|
|
515
|
+
.find(j.ImportDeclaration)
|
|
516
|
+
.filter((path) => path.node.source.value === 'zod')
|
|
517
|
+
.size() > 0;
|
|
518
|
+
if (usesZ && !hasZodImport) {
|
|
519
|
+
importDecls.push(j.importDeclaration([j.importSpecifier(j.identifier('z'))], j.literal('zod')));
|
|
520
|
+
}
|
|
521
|
+
// Insert all at once, in order, after the last import
|
|
522
|
+
if (importDecls.length > 0) {
|
|
523
|
+
const body = root.get().node.program.body;
|
|
524
|
+
let insertIdx = 0;
|
|
525
|
+
for (let i = 0; i < body.length; i++) {
|
|
526
|
+
if (body[i].type === 'ImportDeclaration') {
|
|
527
|
+
insertIdx = i + 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
body.splice(insertIdx, 0, ...importDecls);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Adds a package.json file to the given directory if it doesn't exist
|
|
535
|
+
* Otherwise, it updates the existing package.json file
|
|
536
|
+
*/
|
|
537
|
+
async function addPackageJson({ fullPath, debug }) {
|
|
538
|
+
// Ensure package.json exists and has nango in devDependencies
|
|
539
|
+
const packageJsonPath = path.join(fullPath, 'package.json');
|
|
540
|
+
const examplePackageJsonPath = path.join(exampleFolder, 'package.json');
|
|
541
|
+
let packageJsonExists = false;
|
|
542
|
+
try {
|
|
543
|
+
await fs.promises.access(packageJsonPath, fs.constants.F_OK);
|
|
544
|
+
packageJsonExists = true;
|
|
545
|
+
}
|
|
546
|
+
catch (_err) {
|
|
547
|
+
packageJsonExists = false;
|
|
548
|
+
}
|
|
549
|
+
const examplePkgRaw = await fs.promises.readFile(examplePackageJsonPath, 'utf-8');
|
|
550
|
+
const examplePkg = JSON.parse(examplePkgRaw);
|
|
551
|
+
let pkg;
|
|
552
|
+
if (!packageJsonExists) {
|
|
553
|
+
printDebug('package.json does not exist', debug);
|
|
554
|
+
pkg = examplePkg;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
printDebug('package.json exists, updating', debug);
|
|
558
|
+
const pkgRaw = await fs.promises.readFile(packageJsonPath, 'utf-8');
|
|
559
|
+
pkg = JSON.parse(pkgRaw);
|
|
560
|
+
}
|
|
561
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
562
|
+
pkg.devDependencies['nango'] = NANGO_VERSION;
|
|
563
|
+
const zodVersion = (examplePkg.devDependencies && examplePkg.devDependencies['zod']);
|
|
564
|
+
pkg.devDependencies['zod'] = zodVersion;
|
|
565
|
+
// Remove nango and zod from dependencies just in case they were added as prod
|
|
566
|
+
if (pkg.dependencies?.['nango']) {
|
|
567
|
+
delete pkg.dependencies['nango'];
|
|
568
|
+
}
|
|
569
|
+
if (pkg.dependencies?.['zod']) {
|
|
570
|
+
delete pkg.dependencies['zod'];
|
|
571
|
+
}
|
|
572
|
+
await fs.promises.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2));
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Generates an index.ts file importing all syncs, actions, and on-event scripts for the given integrations.
|
|
576
|
+
*/
|
|
577
|
+
async function generateIndexTs({ fullPath, parsed }) {
|
|
578
|
+
const indexLines = [];
|
|
579
|
+
for (const integration of parsed.integrations) {
|
|
580
|
+
const base = integration.providerConfigKey;
|
|
581
|
+
indexLines.push(`// -- Integration: ${base}`);
|
|
582
|
+
for (const sync of integration.syncs) {
|
|
583
|
+
indexLines.push(`import './${base}/syncs/${sync.name}.js';`);
|
|
584
|
+
}
|
|
585
|
+
for (const action of integration.actions) {
|
|
586
|
+
indexLines.push(`import './${base}/actions/${action.name}.js';`);
|
|
587
|
+
}
|
|
588
|
+
for (const [_eventType, eventNames] of Object.entries(integration.onEventScripts)) {
|
|
589
|
+
for (const eventName of eventNames) {
|
|
590
|
+
indexLines.push(`import './${base}/on-events/${eventName}.js';`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
indexLines.push('');
|
|
594
|
+
}
|
|
595
|
+
const indexPath = path.join(fullPath, 'index.ts');
|
|
596
|
+
await fs.promises.writeFile(indexPath, indexLines.join('\n'));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Removes type arguments from nango.batchSave, batchUpdate, batchDelete calls in all function bodies in the file
|
|
600
|
+
*/
|
|
601
|
+
function removeBatchTypeArguments({ root, j }) {
|
|
602
|
+
root.find(j.FunctionDeclaration).forEach((funcPath) => {
|
|
603
|
+
j(funcPath.node.body)
|
|
604
|
+
.find(j.CallExpression)
|
|
605
|
+
.forEach((callPath) => {
|
|
606
|
+
const callee = callPath.node.callee;
|
|
607
|
+
if (callee.type === 'MemberExpression' &&
|
|
608
|
+
callee.object.type === 'Identifier' &&
|
|
609
|
+
callee.object.name === 'nango' &&
|
|
610
|
+
callee.property.type === 'Identifier' &&
|
|
611
|
+
batchMethods.includes(callee.property.name)) {
|
|
612
|
+
if ('typeArguments' in callPath.node && callPath.node.typeArguments) {
|
|
613
|
+
callPath.node.typeArguments = null;
|
|
614
|
+
}
|
|
615
|
+
if ('typeParameters' in callPath.node && callPath.node.typeParameters) {
|
|
616
|
+
callPath.node.typeParameters = null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Generate models.ts with Zod models/types for all models in parsed.models
|
|
624
|
+
*/
|
|
625
|
+
export function generateModelsTs({ parsed }) {
|
|
626
|
+
const j = jscodeshift.withParser('ts');
|
|
627
|
+
const root = j('');
|
|
628
|
+
// Add import { z } from 'zod';
|
|
629
|
+
root.get().node.program.body.push(j.importDeclaration([j.importSpecifier(j.identifier('z'))], j.literal('zod')));
|
|
630
|
+
// Generate all models as Zod schemas and type aliases, and export them
|
|
631
|
+
const allModelNames = Array.from(parsed.models.keys());
|
|
632
|
+
const sortedModels = topoSortModels(allModelNames, parsed.models);
|
|
633
|
+
for (const modelName of sortedModels) {
|
|
634
|
+
const model = parsed.models.get(modelName);
|
|
635
|
+
if (!model) {
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
// Exported Zod model declaration
|
|
639
|
+
root.get().node.program.body.push(j.exportNamedDeclaration(j.variableDeclaration('const', [j.variableDeclarator(j.identifier(modelName), nangoModelToZod({ j, model, referencedModels: allModelNames }))]), []));
|
|
640
|
+
// Exported type alias
|
|
641
|
+
root.get().node.program.body.push(j.exportNamedDeclaration(j.tsTypeAliasDeclaration(j.identifier(modelName), j.tsTypeReference(j.tsQualifiedName(j.identifier('z'), j.identifier('infer')), j.tsTypeParameterInstantiation([j.tsTypeQuery(j.identifier(modelName))]))), []));
|
|
642
|
+
}
|
|
643
|
+
// Export all models
|
|
644
|
+
root.get().node.program.body.push(j.exportNamedDeclaration(j.variableDeclaration('const', [
|
|
645
|
+
j.variableDeclarator(j.identifier('models'), j.objectExpression(sortedModels.map((modelName) => j.objectProperty(j.identifier(modelName), j.identifier(modelName)))))
|
|
646
|
+
])));
|
|
647
|
+
return root.toSource();
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Converts a NangoModel type to Zod AST
|
|
651
|
+
*/
|
|
652
|
+
export function nangoModelToZod({ j, model, referencedModels }) {
|
|
653
|
+
// Check for dynamic field
|
|
654
|
+
const isDynamic = model.fields.find((field) => {
|
|
655
|
+
if (field.dynamic) {
|
|
656
|
+
return field;
|
|
657
|
+
}
|
|
658
|
+
return false;
|
|
659
|
+
});
|
|
660
|
+
if (isDynamic) {
|
|
661
|
+
// z.object({ ... }).catchall(valueType)
|
|
662
|
+
const valueType = nangoTypeToZodAst({ j, field: isDynamic, referencedModels: referencedModels || [] });
|
|
663
|
+
const safeValueType = valueType ?? j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('any')), []);
|
|
664
|
+
// All other fields
|
|
665
|
+
const otherFields = model.fields.filter((f) => f !== isDynamic);
|
|
666
|
+
const otherProps = otherFields
|
|
667
|
+
.map((field) => {
|
|
668
|
+
const zodAst = nangoTypeToZodAst({ j, field, referencedModels: referencedModels || [] });
|
|
669
|
+
if (!zodAst)
|
|
670
|
+
return undefined;
|
|
671
|
+
return j.objectProperty(j.identifier(field.name), zodAst);
|
|
672
|
+
})
|
|
673
|
+
.filter((prop) => !!prop);
|
|
674
|
+
const objectExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('object')), [j.objectExpression(otherProps)]);
|
|
675
|
+
return j.callExpression(j.memberExpression(objectExpr, j.identifier('catchall')), [safeValueType]);
|
|
676
|
+
}
|
|
677
|
+
if (model.isAnon) {
|
|
678
|
+
return nangoTypeToZodAst({ j, field: model.fields[0], referencedModels: referencedModels || [] });
|
|
679
|
+
}
|
|
680
|
+
// regular object
|
|
681
|
+
const properties = model.fields
|
|
682
|
+
.map((field) => {
|
|
683
|
+
const zodAst = nangoTypeToZodAst({ j, field, referencedModels: referencedModels || [] });
|
|
684
|
+
if (!zodAst)
|
|
685
|
+
return undefined;
|
|
686
|
+
return j.objectProperty(j.identifier(field.name), zodAst);
|
|
687
|
+
})
|
|
688
|
+
.filter((prop) => !!prop);
|
|
689
|
+
return j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('object')), [j.objectExpression(properties)]);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Converts a NangoModelField type to Zod AST
|
|
693
|
+
*/
|
|
694
|
+
function nangoTypeToZodAst({ j, field, referencedModels }) {
|
|
695
|
+
// Handle union
|
|
696
|
+
if (field.union && Array.isArray(field.value)) {
|
|
697
|
+
const unionArgs = field.value
|
|
698
|
+
.map((v) => nangoTypeToZodAst({ j, field: v, referencedModels }))
|
|
699
|
+
.filter((arg) => !!arg);
|
|
700
|
+
let unionExpr = unionArgs.length <= 1
|
|
701
|
+
? unionArgs[0]
|
|
702
|
+
: j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('union')), [j.arrayExpression(unionArgs)]);
|
|
703
|
+
if (field.optional) {
|
|
704
|
+
unionExpr = j.callExpression(j.memberExpression(unionExpr, j.identifier('optional')), []);
|
|
705
|
+
}
|
|
706
|
+
return unionExpr;
|
|
707
|
+
}
|
|
708
|
+
// Handle array
|
|
709
|
+
if (field.array) {
|
|
710
|
+
let arrExpr;
|
|
711
|
+
// Array of ts types (no value)
|
|
712
|
+
if (!Array.isArray(field.value)) {
|
|
713
|
+
return j.callExpression(j.memberExpression(nangoTypeToZodAst({ j, field: { ...field, array: false }, referencedModels }), j.identifier('array')), []);
|
|
714
|
+
}
|
|
715
|
+
// Array of one or multiple values
|
|
716
|
+
const elementTypes = [];
|
|
717
|
+
for (const value of field.value) {
|
|
718
|
+
const ast = nangoTypeToZodAst({ j, field: { ...value }, referencedModels });
|
|
719
|
+
if (ast !== undefined) {
|
|
720
|
+
elementTypes.push(ast);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (elementTypes.length === 0) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
let elementType;
|
|
727
|
+
if (elementTypes.length === 1) {
|
|
728
|
+
elementType = elementTypes[0];
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
elementType = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('union')), [j.arrayExpression(elementTypes)]);
|
|
732
|
+
}
|
|
733
|
+
arrExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('array')), [elementType]);
|
|
734
|
+
if (field.optional) {
|
|
735
|
+
arrExpr = j.callExpression(j.memberExpression(arrExpr, j.identifier('optional')), []);
|
|
736
|
+
}
|
|
737
|
+
return arrExpr;
|
|
738
|
+
}
|
|
739
|
+
// Handle base types and nested objects
|
|
740
|
+
let baseExpr;
|
|
741
|
+
if (typeof field.value === 'string') {
|
|
742
|
+
switch (field.value) {
|
|
743
|
+
case 'string':
|
|
744
|
+
case 'number':
|
|
745
|
+
case 'boolean':
|
|
746
|
+
case 'Date':
|
|
747
|
+
case 'any':
|
|
748
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier(field.value.toLocaleLowerCase())), []);
|
|
749
|
+
break;
|
|
750
|
+
case 'null':
|
|
751
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('nullable')), []);
|
|
752
|
+
break;
|
|
753
|
+
case 'undefined':
|
|
754
|
+
// return on purpose to skip
|
|
755
|
+
return;
|
|
756
|
+
case 'Record<string, any>':
|
|
757
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('object')), [j.objectExpression([])]);
|
|
758
|
+
break;
|
|
759
|
+
case 'any[]':
|
|
760
|
+
baseExpr = j.callExpression(j.memberExpression(j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('any')), []), j.identifier('array')), []);
|
|
761
|
+
break;
|
|
762
|
+
default:
|
|
763
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('literal')), [j.stringLiteral(field.value)]);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else if (Array.isArray(field.value)) {
|
|
767
|
+
// If not union/array, treat as nested object
|
|
768
|
+
const nested = nangoModelToZod({ j, model: { name: '', fields: field.value }, referencedModels: referencedModels || [] });
|
|
769
|
+
baseExpr = nested
|
|
770
|
+
? nested.type === 'Identifier'
|
|
771
|
+
? j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('lazy')), [j.arrowFunctionExpression([], nested)])
|
|
772
|
+
: nested
|
|
773
|
+
: j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('any')), []);
|
|
774
|
+
}
|
|
775
|
+
else if (field.value === null) {
|
|
776
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('null')), []);
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
baseExpr = j.callExpression(j.memberExpression(j.identifier('z'), j.identifier('any')), []);
|
|
780
|
+
}
|
|
781
|
+
if (field.optional) {
|
|
782
|
+
baseExpr = j.callExpression(j.memberExpression(baseExpr, j.identifier('optional')), []);
|
|
783
|
+
}
|
|
784
|
+
// Handle model reference
|
|
785
|
+
if (field.model && typeof field.value === 'string' && referencedModels && referencedModels.includes(field.value)) {
|
|
786
|
+
return j.identifier(field.value);
|
|
787
|
+
}
|
|
788
|
+
return baseExpr;
|
|
789
|
+
}
|
|
790
|
+
// Helper to extract dependencies for a model
|
|
791
|
+
function getModelDependencies(model) {
|
|
792
|
+
const deps = new Set();
|
|
793
|
+
function walkField(field) {
|
|
794
|
+
if (field.model && typeof field.value === 'string') {
|
|
795
|
+
deps.add(field.value);
|
|
796
|
+
}
|
|
797
|
+
if (Array.isArray(field.value)) {
|
|
798
|
+
for (const v of field.value) {
|
|
799
|
+
walkField(v);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (field.union && Array.isArray(field.value)) {
|
|
803
|
+
for (const v of field.value) {
|
|
804
|
+
walkField(v);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
for (const field of model.fields) {
|
|
809
|
+
walkField(field);
|
|
810
|
+
}
|
|
811
|
+
return deps;
|
|
812
|
+
}
|
|
813
|
+
// Topological sort
|
|
814
|
+
function topoSortModels(modelNames, models) {
|
|
815
|
+
const visited = new Set();
|
|
816
|
+
const temp = new Set();
|
|
817
|
+
const result = [];
|
|
818
|
+
function visit(name) {
|
|
819
|
+
if (visited.has(name))
|
|
820
|
+
return;
|
|
821
|
+
if (temp.has(name))
|
|
822
|
+
return; // Prevent cycles
|
|
823
|
+
temp.add(name);
|
|
824
|
+
const model = models.get(name);
|
|
825
|
+
if (model) {
|
|
826
|
+
for (const dep of getModelDependencies(model)) {
|
|
827
|
+
if (modelNames.includes(dep)) {
|
|
828
|
+
visit(dep);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
temp.delete(name);
|
|
833
|
+
visited.add(name);
|
|
834
|
+
result.push(name);
|
|
835
|
+
}
|
|
836
|
+
for (const name of modelNames) {
|
|
837
|
+
visit(name);
|
|
838
|
+
}
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
// export type NangoSyncLocal = Parameters<(typeof sync)['exec']>[0]
|
|
842
|
+
function createNangoLocalType({ j, name, variable }) {
|
|
843
|
+
return j.exportNamedDeclaration(j.tsTypeAliasDeclaration(j.identifier(name), j.tsIndexedAccessType(j.tsTypeReference(j.identifier('Parameters'), j.tsTypeParameterInstantiation([j.tsIndexedAccessType(j.tsTypeQuery(j.identifier(variable)), j.tsLiteralType(j.stringLiteral('exec')))])), j.tsLiteralType(j.numericLiteral(0)))));
|
|
844
|
+
}
|
|
845
|
+
// Helper: For each file in the list, update model imports for NangoSync/NangoAction
|
|
846
|
+
async function processHelperFiles({ fullPath, parsed }) {
|
|
847
|
+
const files = await glob('**/*.ts', {
|
|
848
|
+
cwd: fullPath,
|
|
849
|
+
ignore: ['**/.nango/**', '**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
850
|
+
absolute: true
|
|
851
|
+
});
|
|
852
|
+
// Build list of integration files to process
|
|
853
|
+
const integrationFiles = new Set();
|
|
854
|
+
for (const integration of parsed.integrations) {
|
|
855
|
+
for (const sync of integration.syncs) {
|
|
856
|
+
integrationFiles.add(path.join(fullPath, integration.providerConfigKey, 'syncs', `${sync.name}.ts`));
|
|
857
|
+
}
|
|
858
|
+
for (const action of integration.actions) {
|
|
859
|
+
integrationFiles.add(path.join(fullPath, integration.providerConfigKey, 'actions', `${action.name}.ts`));
|
|
860
|
+
}
|
|
861
|
+
for (const [_, eventNames] of Object.entries(integration.onEventScripts)) {
|
|
862
|
+
for (const name of eventNames) {
|
|
863
|
+
integrationFiles.add(path.join(fullPath, integration.providerConfigKey, 'on-events', `${name}.ts`));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Filter out integration files from the glob list since they were already processed
|
|
868
|
+
for (const absPath of files) {
|
|
869
|
+
if (integrationFiles.has(absPath)) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const spinner = ora({ text: `Migrating ${absPath.replace(fullPath, '')}` }).start();
|
|
873
|
+
if (await hasSymlinkInPath(absPath, fullPath)) {
|
|
874
|
+
spinner.warn('Skipping symlink');
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const content = await fs.promises.readFile(absPath, 'utf-8');
|
|
878
|
+
const { root, changed } = processHelperFile({ content });
|
|
879
|
+
if (changed) {
|
|
880
|
+
await fs.promises.writeFile(absPath, root.toSource());
|
|
881
|
+
}
|
|
882
|
+
spinner.succeed();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
export function processHelperFile({ content }) {
|
|
886
|
+
const allowedTypesImportsHelper = [...allowedTypesImports, 'NangoAction', 'NangoSync'];
|
|
887
|
+
const j = jscodeshift.withParser('ts');
|
|
888
|
+
const root = j(content);
|
|
889
|
+
let changed = false;
|
|
890
|
+
// Find import from models
|
|
891
|
+
root.find(j.ImportDeclaration).forEach((pathNode) => {
|
|
892
|
+
const source = pathNode.node.source.value;
|
|
893
|
+
if (typeof source !== 'string') {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// Find NangoSync/NangoAction specifiers (only ImportSpecifier)
|
|
897
|
+
const specifiers = pathNode.node.specifiers || [];
|
|
898
|
+
const nangoSpecifiers = specifiers.filter((s) => isImportSpecifier(s) && allowedTypesImportsHelper.includes(s.imported.name));
|
|
899
|
+
if (nangoSpecifiers.length === 0) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
// Remove NangoSync/NangoAction from models import
|
|
903
|
+
pathNode.node.specifiers = specifiers.filter((s) => !(isImportSpecifier(s) && allowedTypesImportsHelper.includes(s.imported.name)));
|
|
904
|
+
changed = true;
|
|
905
|
+
// Add or update import from 'nango'
|
|
906
|
+
const nangoImport = root.find(j.ImportDeclaration, { source: { value: 'nango' } });
|
|
907
|
+
if (nangoImport.size() > 0) {
|
|
908
|
+
// Add to existing import
|
|
909
|
+
nangoImport.get(0).node.specifiers = [
|
|
910
|
+
...nangoImport.get(0).node.specifiers,
|
|
911
|
+
...nangoSpecifiers
|
|
912
|
+
.filter(isImportSpecifier)
|
|
913
|
+
.filter((s) => typeof s.imported.name === 'string')
|
|
914
|
+
.map((s) => {
|
|
915
|
+
const name = s.imported.name;
|
|
916
|
+
return j.importSpecifier(j.identifier(name));
|
|
917
|
+
})
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
// Add new import
|
|
922
|
+
root.get().node.program.body.unshift(j.importDeclaration(nangoSpecifiers
|
|
923
|
+
.filter(isImportSpecifier)
|
|
924
|
+
.filter((s) => typeof s.imported.name === 'string')
|
|
925
|
+
.map((s) => {
|
|
926
|
+
const name = s.imported.name;
|
|
927
|
+
return j.importSpecifier(j.identifier(name));
|
|
928
|
+
}), j.literal('nango')));
|
|
929
|
+
}
|
|
930
|
+
// If models import is now empty, remove it
|
|
931
|
+
if (!pathNode.node.specifiers || pathNode.node.specifiers.length === 0) {
|
|
932
|
+
j(pathNode).remove();
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
return { root, changed };
|
|
936
|
+
}
|
|
937
|
+
// Helper type guard for ImportSpecifier
|
|
938
|
+
function isImportSpecifier(s) {
|
|
939
|
+
return (typeof s === 'object' &&
|
|
940
|
+
s !== null &&
|
|
941
|
+
'type' in s &&
|
|
942
|
+
s.type === 'ImportSpecifier' &&
|
|
943
|
+
'imported' in s &&
|
|
944
|
+
s.imported &&
|
|
945
|
+
s.imported.type === 'Identifier');
|
|
946
|
+
}
|
|
947
|
+
// Helper to check if a file or any parent directory is a symlink
|
|
948
|
+
async function hasSymlinkInPath(filePath, stopAtPath) {
|
|
949
|
+
let current = path.resolve(filePath);
|
|
950
|
+
while (true) {
|
|
951
|
+
try {
|
|
952
|
+
const stat = await fs.promises.lstat(current);
|
|
953
|
+
if (stat.isSymbolicLink()) {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
// ignore
|
|
959
|
+
}
|
|
960
|
+
if (current === stopAtPath) {
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
const parent = path.dirname(current);
|
|
964
|
+
if (parent === current) {
|
|
965
|
+
// reached root
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
current = parent;
|
|
969
|
+
}
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
//# sourceMappingURL=toZeroYaml.js.map
|