@ductape/cli 0.2.0 → 0.2.2

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.
Files changed (44) hide show
  1. package/.env.ductape.example +5 -0
  2. package/DB_MIGRATE_PLAN.md +223 -0
  3. package/dist/commands/apply.d.ts +6 -0
  4. package/dist/commands/apply.js +71 -0
  5. package/dist/commands/db-migrate.d.ts +19 -0
  6. package/dist/commands/db-migrate.js +154 -0
  7. package/dist/commands/db-schema.d.ts +6 -0
  8. package/dist/commands/db-schema.js +135 -0
  9. package/dist/commands/install.d.ts +1 -1
  10. package/dist/commands/install.js +47 -23
  11. package/dist/commands/platform.js +1 -1
  12. package/dist/index.js +68 -14
  13. package/dist/lib/apply-loaders.d.ts +6 -0
  14. package/dist/lib/apply-loaders.js +24 -0
  15. package/dist/lib/config.d.ts +2 -1
  16. package/dist/lib/config.js +9 -4
  17. package/dist/lib/db-types.d.ts +71 -0
  18. package/dist/lib/db-types.js +1 -0
  19. package/dist/lib/migration-files.d.ts +4 -0
  20. package/dist/lib/migration-files.js +43 -0
  21. package/dist/lib/platform-api.js +15 -1
  22. package/dist/lib/resources.js +3 -1
  23. package/dist/lib/schema-loader.d.ts +30 -0
  24. package/dist/lib/schema-loader.js +117 -0
  25. package/dist/lib/templates.js +75 -1
  26. package/ductape.example.ts +18 -0
  27. package/package.json +1 -1
  28. package/src/commands/apply.ts +105 -0
  29. package/src/commands/db-migrate.ts +211 -0
  30. package/src/commands/db-schema.ts +163 -0
  31. package/src/commands/install.ts +52 -24
  32. package/src/commands/platform.ts +1 -1
  33. package/src/index.ts +78 -16
  34. package/src/lib/apply-loaders.ts +30 -0
  35. package/src/lib/config.ts +9 -4
  36. package/src/lib/db-types.ts +104 -0
  37. package/src/lib/migration-files.ts +58 -0
  38. package/src/lib/platform-api.ts +15 -1
  39. package/src/lib/resources.ts +3 -1
  40. package/src/lib/schema-loader.ts +152 -0
  41. package/src/lib/templates.ts +80 -1
  42. package/templates/api-gateway.conf +178 -0
  43. package/templates/docker-compose.release.yml +265 -0
  44. package/templates/platform.env +16 -0
@@ -44,7 +44,7 @@ export async function runStart(opts: {
44
44
  '--build-arg',
45
45
  `VITE_API_BASE_URL=${apiBase}`,
46
46
  '--build-arg',
47
- 'VITE_APP_ENV=development',
47
+ 'VITE_APP_ENV=self',
48
48
  'workbench',
49
49
  ],
50
50
  platformDir,
package/src/index.ts CHANGED
@@ -15,6 +15,8 @@ import { runStart, runStop, runStatus } from './commands/platform.js';
15
15
  import { runResourceCrud, runResourcesList } from './commands/resources.js';
16
16
  import { runCloud } from './commands/cloud.js';
17
17
  import { runDb, runDbContext } from './commands/db.js';
18
+ import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
19
+ import { runDbSchemaGenerate } from './commands/db-schema.js';
18
20
  import { runGraph } from './commands/graph.js';
19
21
  import { runSecrets } from './commands/secrets.js';
20
22
  import {
@@ -28,6 +30,7 @@ import { runCompletion } from './commands/completion.js';
28
30
  import { runProducts, runProductApps } from './commands/products.js';
29
31
  import { runApps } from './commands/apps.js';
30
32
  import { runAppsImport } from './commands/apps-import.js';
33
+ import { runApply, type ApplyType } from './commands/apply.js';
31
34
 
32
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
36
  const pkg = JSON.parse(
@@ -181,6 +184,22 @@ program
181
184
  ),
182
185
  );
183
186
 
187
+ const APPLY_TYPES = ['sessions', 'notifications', 'events'] as const;
188
+
189
+ program
190
+ .command('apply')
191
+ .description('Sync declared sessions, notifications and events to your product')
192
+ .argument('[type]', `${APPLY_TYPES.join(' | ')}`)
193
+ .option('--dry-run', 'Print what would be created/updated without applying')
194
+ .action(
195
+ wrap((type: string | undefined, opts) => {
196
+ if (type && !(APPLY_TYPES as readonly string[]).includes(type)) {
197
+ throw new Error(`Unknown type "${type}". Use: ${APPLY_TYPES.join(', ')}`);
198
+ }
199
+ return runApply(type as ApplyType | undefined, { dryRun: Boolean(opts.dryRun) });
200
+ }),
201
+ );
202
+
184
203
  program
185
204
  .command('start')
186
205
  .description('Start platform via docker compose')
@@ -203,11 +222,8 @@ program.command('status').option('--json', 'JSON output').action(wrap((opts) =>
203
222
 
204
223
  const products = program
205
224
  .command('products')
206
- .description('Workspace products CRUD (REST + sdk-proxy update)');
207
-
208
- products
209
- .command('<verb>')
210
- .description('list | get | create | update | delete')
225
+ .description('Workspace products CRUD (REST + sdk-proxy update)')
226
+ .argument('[verb]', 'list | get | create | update | delete')
211
227
  .option('--profile <name>')
212
228
  .option('--status <status>', 'Filter for list: all, active, draft, …', 'all')
213
229
  .option('--id <id>', 'Product _id (get, delete)')
@@ -217,7 +233,10 @@ products
217
233
  .option('--no-interactive', 'Require -f JSON for create/update')
218
234
  .option('--json', 'JSON output')
219
235
  .action(
220
- wrap((verb: string, opts) => runProducts(verb, opts, [])),
236
+ wrap((verb: string | undefined, opts) => {
237
+ if (!verb) throw new Error('Specify a verb: list | get | create | update | delete');
238
+ return runProducts(verb, opts, []);
239
+ }),
221
240
  );
222
241
 
223
242
  const productApps = products.command('apps').description('Apps connected to a product');
@@ -229,11 +248,10 @@ productApps
229
248
  .option('--json', 'JSON output')
230
249
  .action(wrap((opts) => runProductApps('list', opts, [])));
231
250
 
232
- const apps = program.command('apps').description('Workspace integration apps CRUD (REST)');
233
-
234
- apps
235
- .command('<verb>')
236
- .description('list | get | create | update | delete')
251
+ const apps = program
252
+ .command('apps')
253
+ .description('Workspace integration apps CRUD (REST)')
254
+ .argument('[verb]', 'list | get | create | update | delete')
237
255
  .option('--profile <name>')
238
256
  .option('--status <status>', 'Filter for list', 'all')
239
257
  .option('--id <id>', 'App _id')
@@ -243,7 +261,12 @@ apps
243
261
  .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
244
262
  .option('--no-interactive', 'Require -f JSON for create/update')
245
263
  .option('--json', 'JSON output')
246
- .action(wrap((verb: string, opts) => runApps(verb, opts, [])));
264
+ .action(
265
+ wrap((verb: string | undefined, opts) => {
266
+ if (!verb) throw new Error('Specify a verb: list | get | create | update | delete');
267
+ return runApps(verb, opts, []);
268
+ }),
269
+ );
247
270
 
248
271
  apps
249
272
  .command('import <file>')
@@ -277,7 +300,8 @@ const resources = program.command('resources').description('Component CRUD (sdk-
277
300
  resources.command('types').option('--json', 'JSON output').action(wrap((opts) => runResourcesList(Boolean(opts.json))));
278
301
 
279
302
  resources
280
- .command('<type> <verb>')
303
+ .argument('<type>', 'Resource type (e.g. storage, database, cache …)')
304
+ .argument('<verb>', 'list | get | create | update | delete | connect')
281
305
  .option('-t, --tag <tag>')
282
306
  .option('-f, --file <path>', 'JSON body (or use interactive prompts)')
283
307
  .option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
@@ -309,16 +333,54 @@ const db = program.command('db').description('Database runtime (db-proxy)');
309
333
 
310
334
  db.command('context').option('--json', 'JSON output').action(wrap((opts) => runDbContext(Boolean(opts.json))));
311
335
 
336
+ const dbMigrate = db
337
+ .command('migrate')
338
+ .description('Run pending migrations from ductape/database/migrations/')
339
+ .option('--env <tag>', 'Override environment slug')
340
+ .option('--db <tag>', 'Limit to one db entry from schema.json')
341
+ .option('--dry-run', 'Print what would run without applying')
342
+ .action(wrap((opts) => runDbMigrate({ env: opts.env, db: opts.db, dryRun: Boolean(opts.dryRun) })));
343
+
344
+ dbMigrate
345
+ .command('status')
346
+ .description('Show applied vs pending migrations')
347
+ .option('--env <tag>', 'Override environment slug')
348
+ .option('--db <tag>', 'Limit to one db entry')
349
+ .option('--json', 'JSON output')
350
+ .action(wrap((opts) => runDbMigrateStatus({ env: opts.env, db: opts.db, json: Boolean(opts.json) })));
351
+
352
+ dbMigrate
353
+ .command('rollback')
354
+ .description('Roll back the last N migrations')
355
+ .option('--env <tag>', 'Override environment slug')
356
+ .option('--db <tag>', 'Limit to one db entry')
357
+ .option('-n <count>', 'Number of migrations to roll back', '1')
358
+ .action(wrap((opts) => runDbMigrateRollback({ env: opts.env, db: opts.db, n: Number(opts.n) })));
359
+
360
+ const dbSchema = db.command('schema').description('Schema management for ductape/database/schema.json');
361
+
362
+ dbSchema
363
+ .command('generate')
364
+ .description('Diff schema.json vs applied migrations and write new migration files')
365
+ .option('--db <tag>', 'Limit to one db entry from schema.json')
366
+ .option('--destructive', 'Also generate drop migrations for removed fields/tables')
367
+ .action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
368
+
312
369
  db
313
- .command('<verb>')
370
+ .argument('[verb]', 'connect | query | …')
314
371
  .option('-f, --file <path>')
315
372
  .option('--json', 'JSON output')
316
- .action(wrap((verb: string, opts) => runDb(verb, { file: opts.file, json: opts.json })));
373
+ .action(
374
+ wrap((verb: string | undefined, opts) => {
375
+ if (!verb) throw new Error('Specify a verb: connect | query');
376
+ return runDb(verb, { file: opts.file, json: opts.json });
377
+ }),
378
+ );
317
379
 
318
380
  const graph = program.command('graph').description('Graph runtime (graph-proxy)');
319
381
 
320
382
  graph
321
- .command('<verb>')
383
+ .argument('<verb>', 'connect | query | …')
322
384
  .option('-f, --file <path>')
323
385
  .option('--json', 'JSON output')
324
386
  .action(wrap((verb: string, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROJECT_CONFIG_DIR } from './config.js';
4
+
5
+ export type ApplyItem = Record<string, unknown> & { tag: string };
6
+
7
+ function loadArrayFile(filePath: string): ApplyItem[] | null {
8
+ if (!fs.existsSync(filePath)) return null;
9
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
10
+ if (!Array.isArray(raw)) {
11
+ throw new Error(`${path.basename(filePath)} must be a JSON array.`);
12
+ }
13
+ return raw as ApplyItem[];
14
+ }
15
+
16
+ function declarationPath(projectDir: string, filename: string): string {
17
+ return path.join(projectDir, PROJECT_CONFIG_DIR, filename);
18
+ }
19
+
20
+ export function loadSessions(projectDir: string): ApplyItem[] | null {
21
+ return loadArrayFile(declarationPath(projectDir, 'sessions.json'));
22
+ }
23
+
24
+ export function loadNotifications(projectDir: string): ApplyItem[] | null {
25
+ return loadArrayFile(declarationPath(projectDir, 'notifications.json'));
26
+ }
27
+
28
+ export function loadEvents(projectDir: string): ApplyItem[] | null {
29
+ return loadArrayFile(declarationPath(projectDir, 'events.json'));
30
+ }
package/src/lib/config.ts CHANGED
@@ -8,7 +8,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  export const GLOBAL_DIR = path.join(os.homedir(), '.ductape');
9
9
  export const CREDENTIALS_PATH = path.join(GLOBAL_DIR, 'credentials.json');
10
10
  export const GLOBAL_CONFIG_PATH = path.join(GLOBAL_DIR, 'config.json');
11
- export const PROJECT_CONFIG_DIR = '.ductape';
11
+ export const HUB_PLATFORM_DIR = path.join(GLOBAL_DIR, 'platform');
12
+ export const PROJECT_CONFIG_DIR = 'ductape';
12
13
  export const PROJECT_CONFIG_PATH = path.join(PROJECT_CONFIG_DIR, 'config.json');
13
14
 
14
15
  export interface Credentials {
@@ -198,7 +199,7 @@ export function loadProjectConfig(): ResolvedProjectConfig {
198
199
  const workspace_id = resolveWorkspaceId(cfg, creds);
199
200
  if (!workspace_id) {
200
201
  throw new Error(
201
- 'No workspace resolved. Set workspace_tag in .ductape/config.json or run `ductape workspaces use`.',
202
+ 'No workspace resolved. Set workspace_tag in ductape/config.json or run `ductape workspaces use`.',
202
203
  );
203
204
  }
204
205
  return { ...cfg, workspace_id };
@@ -214,12 +215,16 @@ export function resolvePlatformDir(): string {
214
215
  if (process.env.DUCTAPE_PLATFORM_DIR) {
215
216
  return path.resolve(process.env.DUCTAPE_PLATFORM_DIR);
216
217
  }
217
- // cli/ -> repo root -> platform/
218
+ // Hub install: ~/.ductape/platform/ (written by `ductape install`)
219
+ if (fs.existsSync(path.join(HUB_PLATFORM_DIR, 'docker-compose.yml'))) {
220
+ return HUB_PLATFORM_DIR;
221
+ }
222
+ // Dev/monorepo: cli/ -> repo root -> platform/
218
223
  const fromPackage = path.resolve(__dirname, '../../../platform');
219
224
  if (fs.existsSync(path.join(fromPackage, 'docker-compose.yml'))) {
220
225
  return fromPackage;
221
226
  }
222
227
  throw new Error(
223
- 'Platform directory not found. Set DUCTAPE_PLATFORM_DIR to the folder containing docker-compose.yml',
228
+ 'Platform not installed. Run `ductape install` to pull the platform from Docker Hub.',
224
229
  );
225
230
  }
@@ -0,0 +1,104 @@
1
+ export type FieldType =
2
+ | 'string'
3
+ | 'text'
4
+ | 'integer'
5
+ | 'bigint'
6
+ | 'smallint'
7
+ | 'decimal'
8
+ | 'float'
9
+ | 'double'
10
+ | 'uuid'
11
+ | 'boolean'
12
+ | 'date'
13
+ | 'time'
14
+ | 'datetime'
15
+ | 'timestamp'
16
+ | 'binary'
17
+ | 'blob'
18
+ | 'json'
19
+ | 'object'
20
+ | 'array'
21
+ | 'enum';
22
+
23
+ export interface IFieldDefinition {
24
+ name: string;
25
+ type: FieldType;
26
+ nullable?: boolean;
27
+ unique?: boolean;
28
+ primaryKey?: boolean;
29
+ autoGenerate?: boolean;
30
+ defaultValue?: unknown;
31
+ maxLength?: number;
32
+ precision?: number;
33
+ scale?: number;
34
+ enumValues?: string[];
35
+ arrayElementType?: FieldType;
36
+ }
37
+
38
+ export interface ICreateCollectionOp {
39
+ type: 'createCollection';
40
+ name: string;
41
+ fields: IFieldDefinition[];
42
+ ifNotExists?: boolean;
43
+ }
44
+
45
+ export interface IDropCollectionOp {
46
+ type: 'dropCollection';
47
+ name: string;
48
+ ifExists?: boolean;
49
+ cascade?: boolean;
50
+ }
51
+
52
+ export interface IAddFieldOp {
53
+ type: 'addField';
54
+ collection: string;
55
+ field: IFieldDefinition;
56
+ }
57
+
58
+ export interface IDropFieldOp {
59
+ type: 'dropField';
60
+ collection: string;
61
+ fieldName: string;
62
+ cascade?: boolean;
63
+ }
64
+
65
+ export interface ICreateIndexOp {
66
+ type: 'createIndex';
67
+ collection: string;
68
+ name: string;
69
+ fields: Array<{ name: string; order?: 'asc' | 'desc' }>;
70
+ unique?: boolean;
71
+ ifNotExists?: boolean;
72
+ }
73
+
74
+ export interface IDropIndexOp {
75
+ type: 'dropIndex';
76
+ collection: string;
77
+ name: string;
78
+ ifExists?: boolean;
79
+ }
80
+
81
+ export type IMigrationOperation =
82
+ | ICreateCollectionOp
83
+ | IDropCollectionOp
84
+ | IAddFieldOp
85
+ | IDropFieldOp
86
+ | ICreateIndexOp
87
+ | IDropIndexOp;
88
+
89
+ export interface IMigration {
90
+ tag: string;
91
+ name: string;
92
+ description?: string;
93
+ up: IMigrationOperation[];
94
+ down: IMigrationOperation[];
95
+ dependencies?: string[];
96
+ createdAt?: string;
97
+ }
98
+
99
+ export interface IMigrationHistory {
100
+ tag: string;
101
+ name: string;
102
+ checksum?: string;
103
+ appliedAt?: string;
104
+ }
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROJECT_CONFIG_DIR } from './config.js';
4
+ import type { IMigration } from './db-types.js';
5
+
6
+ export function getMigrationsDir(projectDir: string, dbTag: string): string {
7
+ return path.join(projectDir, PROJECT_CONFIG_DIR, 'database', 'migrations', dbTag);
8
+ }
9
+
10
+ export function loadMigrationFiles(projectDir: string, dbTag: string): IMigration[] {
11
+ const dir = getMigrationsDir(projectDir, dbTag);
12
+ if (!fs.existsSync(dir)) return [];
13
+
14
+ const files = fs
15
+ .readdirSync(dir)
16
+ .filter((f) => f.endsWith('.json'))
17
+ .sort();
18
+
19
+ return files.map((file) => {
20
+ const raw = JSON.parse(
21
+ fs.readFileSync(path.join(dir, file), 'utf8'),
22
+ ) as IMigration;
23
+ return raw;
24
+ });
25
+ }
26
+
27
+ function makeTimestamp(counter: number): string {
28
+ const now = new Date();
29
+ const pad = (n: number, len = 2) => String(n).padStart(len, '0');
30
+ const ts =
31
+ String(now.getFullYear()) +
32
+ pad(now.getMonth() + 1) +
33
+ pad(now.getDate()) +
34
+ pad(now.getHours()) +
35
+ pad(now.getMinutes()) +
36
+ pad(now.getSeconds());
37
+ return `${ts}_${pad(counter, 3)}`;
38
+ }
39
+
40
+ export function writeMigrationFile(
41
+ projectDir: string,
42
+ dbTag: string,
43
+ migration: IMigration,
44
+ counter: number,
45
+ ): string {
46
+ const dir = getMigrationsDir(projectDir, dbTag);
47
+ fs.mkdirSync(dir, { recursive: true });
48
+
49
+ const safeName = migration.name
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, '_')
52
+ .replace(/^_|_$/g, '');
53
+ const ts = makeTimestamp(counter);
54
+ const filename = `${ts}_${safeName}.json`;
55
+ const filePath = path.join(dir, filename);
56
+ fs.writeFileSync(filePath, JSON.stringify(migration, null, 2) + '\n');
57
+ return filePath;
58
+ }
@@ -34,6 +34,20 @@ function unwrapOne<T>(result: unknown): T {
34
34
 
35
35
  // —— Products (integrations REST + sdk-proxy for update) ——
36
36
 
37
+ const PRODUCT_LIST_ARRAY_FIELDS = [
38
+ 'apps', 'caches', 'quota', 'fallback', 'storage', 'brokers',
39
+ 'sessions', 'messageBrokers', 'functions', 'variables', 'auths',
40
+ 'databases', 'graphs', 'jobs', 'healthchecks', 'notifications',
41
+ 'features', 'vectors', 'agents', 'models', 'steps_data',
42
+ ];
43
+
44
+ function summarizeProduct(p: unknown): unknown {
45
+ if (!p || typeof p !== 'object') return p;
46
+ const out: Record<string, unknown> = { ...(p as Record<string, unknown>) };
47
+ for (const field of PRODUCT_LIST_ARRAY_FIELDS) delete out[field];
48
+ return out;
49
+ }
50
+
37
51
  export async function listProducts(
38
52
  ctx: WorkspaceContext,
39
53
  status = 'all',
@@ -42,7 +56,7 @@ export async function listProducts(
42
56
  `/integrations/v1/workspace/${ctx.workspaceId}/${status}`,
43
57
  workspaceAuthQuery(ctx),
44
58
  );
45
- return unwrapList(result);
59
+ return unwrapList<unknown>(result).map(summarizeProduct);
46
60
  }
47
61
 
48
62
  export async function getProduct(
@@ -19,6 +19,8 @@ export const RESOURCE_MODULES: Record<string, SDKModule> = {
19
19
  actions: 'actions',
20
20
  notification: 'notifications',
21
21
  notifications: 'notifications',
22
+ event: 'messageBrokers',
23
+ events: 'messageBrokers',
22
24
  broker: 'messageBrokers',
23
25
  brokers: 'messageBrokers',
24
26
  'message-brokers': 'messageBrokers',
@@ -144,7 +146,7 @@ export function listResourceTypes(): string[] {
144
146
  'features',
145
147
  'actions',
146
148
  'notifications',
147
- 'message-brokers',
149
+ 'events',
148
150
  'sessions',
149
151
  'secrets',
150
152
  'quotas',
@@ -0,0 +1,152 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { PROJECT_CONFIG_DIR } from './config.js';
4
+ import type { FieldType, IFieldDefinition } from './db-types.js';
5
+
6
+ export interface MongooseFieldDef {
7
+ type?: string | string[];
8
+ required?: boolean;
9
+ unique?: boolean;
10
+ default?: unknown;
11
+ maxlength?: number;
12
+ enum?: string[];
13
+ primaryKey?: boolean;
14
+ autoGenerate?: boolean;
15
+ float?: boolean;
16
+ ref?: string;
17
+ index?: boolean;
18
+ }
19
+
20
+ export interface SchemaEntry {
21
+ db: string;
22
+ tables: Record<string, Record<string, MongooseFieldDef>>;
23
+ }
24
+
25
+ export function getSchemaPath(projectDir: string): string {
26
+ return path.join(projectDir, PROJECT_CONFIG_DIR, 'database', 'schema.json');
27
+ }
28
+
29
+ export function loadSchemaFile(projectDir: string): SchemaEntry[] {
30
+ const schemaPath = getSchemaPath(projectDir);
31
+ if (!fs.existsSync(schemaPath)) {
32
+ throw new Error(
33
+ `No schema.json found at ${schemaPath}. Run \`ductape init\` to scaffold it.`,
34
+ );
35
+ }
36
+ const raw = JSON.parse(fs.readFileSync(schemaPath, 'utf8')) as unknown;
37
+ if (!Array.isArray(raw)) {
38
+ throw new Error('schema.json must be a JSON array of { db, tables } objects.');
39
+ }
40
+ return raw as SchemaEntry[];
41
+ }
42
+
43
+ const TYPE_MAP: Record<string, FieldType> = {
44
+ string: 'string',
45
+ String: 'string',
46
+ text: 'text',
47
+ Text: 'text',
48
+ number: 'integer',
49
+ Number: 'integer',
50
+ integer: 'integer',
51
+ Integer: 'integer',
52
+ int: 'integer',
53
+ Int: 'integer',
54
+ boolean: 'boolean',
55
+ Boolean: 'boolean',
56
+ bool: 'boolean',
57
+ Bool: 'boolean',
58
+ date: 'datetime',
59
+ Date: 'datetime',
60
+ datetime: 'datetime',
61
+ DateTime: 'datetime',
62
+ timestamp: 'timestamp',
63
+ Timestamp: 'timestamp',
64
+ buffer: 'binary',
65
+ Buffer: 'binary',
66
+ binary: 'binary',
67
+ Binary: 'binary',
68
+ mixed: 'object',
69
+ Mixed: 'object',
70
+ object: 'object',
71
+ Object: 'object',
72
+ array: 'array',
73
+ Array: 'array',
74
+ objectid: 'uuid',
75
+ ObjectId: 'uuid',
76
+ uuid: 'uuid',
77
+ UUID: 'uuid',
78
+ decimal128: 'decimal',
79
+ Decimal128: 'decimal',
80
+ decimal: 'decimal',
81
+ Decimal: 'decimal',
82
+ map: 'object',
83
+ Map: 'object',
84
+ bigint: 'bigint',
85
+ BigInt: 'bigint',
86
+ float: 'float',
87
+ Float: 'float',
88
+ double: 'double',
89
+ Double: 'double',
90
+ json: 'json',
91
+ JSON: 'json',
92
+ blob: 'blob',
93
+ Blob: 'blob',
94
+ smallint: 'smallint',
95
+ SmallInt: 'smallint',
96
+ time: 'time',
97
+ Time: 'time',
98
+ };
99
+
100
+ function resolveBaseType(raw: string | string[] | undefined, def: MongooseFieldDef): FieldType {
101
+ const t = Array.isArray(raw) ? raw[0] : (raw ?? 'String');
102
+ if (def.enum?.length) return 'enum';
103
+ const mapped = TYPE_MAP[t];
104
+ if (!mapped) return 'string';
105
+ if (mapped === 'integer' && def.float) return 'float';
106
+ return mapped;
107
+ }
108
+
109
+ export function parseField(name: string, def: MongooseFieldDef): IFieldDefinition {
110
+ const type = resolveBaseType(def.type, def);
111
+ const field: IFieldDefinition = { name, type };
112
+
113
+ if (def.required === true) field.nullable = false;
114
+ if (def.unique === true) field.unique = true;
115
+ if (def.primaryKey === true) field.primaryKey = true;
116
+ if (def.autoGenerate === true) field.autoGenerate = true;
117
+ if (def.maxlength != null) field.maxLength = def.maxlength;
118
+
119
+ if (def.default !== undefined) {
120
+ field.defaultValue =
121
+ def.default === 'now' || def.default === 'NOW' ? 'CURRENT_TIMESTAMP' : def.default;
122
+ }
123
+
124
+ if (type === 'enum' && def.enum?.length) {
125
+ field.enumValues = def.enum;
126
+ }
127
+
128
+ return field;
129
+ }
130
+
131
+ export interface ParsedTable {
132
+ name: string;
133
+ fields: IFieldDefinition[];
134
+ indexFields: Array<{ fieldName: string; unique: boolean }>;
135
+ }
136
+
137
+ export function parseTableDef(
138
+ tableName: string,
139
+ fieldDefs: Record<string, MongooseFieldDef>,
140
+ ): ParsedTable {
141
+ const fields: IFieldDefinition[] = [];
142
+ const indexFields: Array<{ fieldName: string; unique: boolean }> = [];
143
+
144
+ for (const [name, def] of Object.entries(fieldDefs)) {
145
+ fields.push(parseField(name, def));
146
+ if (def.index === true) {
147
+ indexFields.push({ fieldName: name, unique: def.unique === true });
148
+ }
149
+ }
150
+
151
+ return { name: tableName, fields, indexFields };
152
+ }