@ductape/cli 0.2.10 → 0.2.12
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/commands/db-schema.d.ts +4 -0
- package/dist/commands/db-schema.js +70 -0
- package/dist/index.js +6 -1
- package/dist/lib/integrations.d.ts +23 -0
- package/dist/lib/integrations.js +27 -0
- package/package.json +1 -1
- package/src/commands/db-schema.ts +90 -0
- package/src/index.ts +7 -1
- package/src/lib/integrations.ts +54 -0
|
@@ -3,4 +3,8 @@ interface SchemaGenerateOpts {
|
|
|
3
3
|
destructive?: boolean;
|
|
4
4
|
}
|
|
5
5
|
export declare function runDbSchemaGenerate(opts: SchemaGenerateOpts): Promise<void>;
|
|
6
|
+
interface SchemaPushOpts {
|
|
7
|
+
db: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function runDbSchemaPush(opts: SchemaPushOpts): Promise<void>;
|
|
6
10
|
export {};
|
|
@@ -5,6 +5,7 @@ import { loadSchemaFile, getSchemaPath, parseTableDef } from '../lib/schema-load
|
|
|
5
5
|
import { loadMigrationFiles, writeMigrationFile } from '../lib/migration-files.js';
|
|
6
6
|
import { fail } from '../lib/output.js';
|
|
7
7
|
import { getDbProxy, requireSession } from '../lib/proxy/context.js';
|
|
8
|
+
import { pushDatabaseSchema } from '../lib/integrations.js';
|
|
8
9
|
function replayMigrations(migrations) {
|
|
9
10
|
const schema = new Map();
|
|
10
11
|
for (const migration of migrations) {
|
|
@@ -338,3 +339,72 @@ function requireSilent() {
|
|
|
338
339
|
return null;
|
|
339
340
|
}
|
|
340
341
|
}
|
|
342
|
+
function adaptColumn(col) {
|
|
343
|
+
return {
|
|
344
|
+
name: String(col.name ?? ''),
|
|
345
|
+
type: String(col.type ?? 'string'),
|
|
346
|
+
nullable: col.nullable !== false && col.nullable !== 'NO',
|
|
347
|
+
primaryKey: Boolean(col.primaryKey ?? col.isPrimaryKey),
|
|
348
|
+
unique: Boolean(col.unique ?? col.isUnique),
|
|
349
|
+
autoIncrement: Boolean(col.autoIncrement ?? col.isAutoIncrement ?? col.autoGenerate),
|
|
350
|
+
...(col.defaultValue != null ? { defaultValue: col.defaultValue } : {}),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export async function runDbSchemaPush(opts) {
|
|
354
|
+
const session = requireSession();
|
|
355
|
+
const proxy = getDbProxy(session);
|
|
356
|
+
const envSlug = session.project.env_slug;
|
|
357
|
+
const productTag = session.project.product_tag;
|
|
358
|
+
const dbContext = { database: opts.db, env: envSlug, product: productTag };
|
|
359
|
+
console.log(`Connecting to database "${opts.db}" (env: ${envSlug})...`);
|
|
360
|
+
let tableNames = [];
|
|
361
|
+
try {
|
|
362
|
+
const raw = await proxy.execute('schema.list', [null], dbContext);
|
|
363
|
+
if (Array.isArray(raw)) {
|
|
364
|
+
tableNames = raw.map((r) => typeof r === 'string' ? r : String(r.name ?? '')).filter(Boolean);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
throw new Error(`Failed to list tables: ${e.message}. Make sure the database is connected.`);
|
|
369
|
+
}
|
|
370
|
+
if (tableNames.length === 0) {
|
|
371
|
+
console.log('No tables found in the database. Nothing to push.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
console.log(`Found ${tableNames.length} table(s): ${tableNames.join(', ')}`);
|
|
375
|
+
const now = new Date().toISOString();
|
|
376
|
+
const tableSchemas = [];
|
|
377
|
+
for (const tableName of tableNames) {
|
|
378
|
+
try {
|
|
379
|
+
const tableSchema = await proxy.execute('schema.describe', [tableName], dbContext);
|
|
380
|
+
const cols = Array.isArray(tableSchema?.columns)
|
|
381
|
+
? tableSchema.columns
|
|
382
|
+
: Array.isArray(tableSchema) ? tableSchema : [];
|
|
383
|
+
if (cols.length === 0) {
|
|
384
|
+
console.warn(` [skip] ${tableName}: no columns returned`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const columns = cols
|
|
388
|
+
.filter((c) => c.name)
|
|
389
|
+
.map(adaptColumn);
|
|
390
|
+
tableSchemas.push({ table: tableName, columns, updated_at: now });
|
|
391
|
+
console.log(` ${tableName}: ${columns.length} column(s)`);
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
console.warn(` [skip] ${tableName}: ${e.message}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (tableSchemas.length === 0) {
|
|
398
|
+
console.log('No schema data collected. Nothing pushed.');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
console.log(`\nPushing schema for ${tableSchemas.length} table(s) to Ductape...`);
|
|
402
|
+
const result = await pushDatabaseSchema(session.apiUrl, session.credentials, {
|
|
403
|
+
workspace_id: session.project.workspace_id,
|
|
404
|
+
product_tag: productTag,
|
|
405
|
+
database_tag: opts.db,
|
|
406
|
+
table_schemas: tableSchemas,
|
|
407
|
+
});
|
|
408
|
+
console.log(`Done. Synced ${result.synced} table(s): ${result.tables.join(', ')}`);
|
|
409
|
+
console.log('The AI payload generator will now use this schema for field guidance.');
|
|
410
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ 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
18
|
import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
|
|
19
|
-
import { runDbSchemaGenerate } from './commands/db-schema.js';
|
|
19
|
+
import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
|
|
20
20
|
import { runGraph } from './commands/graph.js';
|
|
21
21
|
import { runSecrets } from './commands/secrets.js';
|
|
22
22
|
import { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
|
|
@@ -305,6 +305,11 @@ dbSchema
|
|
|
305
305
|
.option('--db <tag>', 'Limit to one db entry from schema.json')
|
|
306
306
|
.option('--destructive', 'Also generate drop migrations for removed fields/tables')
|
|
307
307
|
.action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
|
|
308
|
+
dbSchema
|
|
309
|
+
.command('push')
|
|
310
|
+
.description('Read live table schema from the database and sync it to the Ductape server (enables AI field guidance)')
|
|
311
|
+
.requiredOption('--db <tag>', 'Database tag to sync')
|
|
312
|
+
.action(wrap((opts) => runDbSchemaPush({ db: opts.db })));
|
|
308
313
|
db
|
|
309
314
|
.argument('[verb]', 'connect | query | …')
|
|
310
315
|
.option('-f, --file <path>')
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { Credentials } from './config.js';
|
|
2
|
+
export interface TableColumnRecord {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
nullable: boolean;
|
|
6
|
+
primaryKey: boolean;
|
|
7
|
+
unique: boolean;
|
|
8
|
+
autoIncrement: boolean;
|
|
9
|
+
defaultValue?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface TableSchemaRecord {
|
|
12
|
+
table: string;
|
|
13
|
+
columns: TableColumnRecord[];
|
|
14
|
+
updated_at: string;
|
|
15
|
+
}
|
|
2
16
|
export interface GeneratePayloadRequest {
|
|
3
17
|
workspace_id: string;
|
|
4
18
|
user_id: string;
|
|
@@ -18,3 +32,12 @@ export interface GeneratePayloadResponse {
|
|
|
18
32
|
meta: Record<string, unknown>;
|
|
19
33
|
}
|
|
20
34
|
export declare function generateExecutablePayload(apiUrl: string, creds: Credentials, request: GeneratePayloadRequest): Promise<GeneratePayloadResponse>;
|
|
35
|
+
export declare function pushDatabaseSchema(apiUrl: string, creds: Credentials, opts: {
|
|
36
|
+
workspace_id: string;
|
|
37
|
+
product_tag: string;
|
|
38
|
+
database_tag: string;
|
|
39
|
+
table_schemas: TableSchemaRecord[];
|
|
40
|
+
}): Promise<{
|
|
41
|
+
synced: number;
|
|
42
|
+
tables: string[];
|
|
43
|
+
}>;
|
package/dist/lib/integrations.js
CHANGED
|
@@ -18,3 +18,30 @@ export async function generateExecutablePayload(apiUrl, creds, request) {
|
|
|
18
18
|
}
|
|
19
19
|
return (body.data ?? body);
|
|
20
20
|
}
|
|
21
|
+
export async function pushDatabaseSchema(apiUrl, creds, opts) {
|
|
22
|
+
const url = `${apiUrl}/integrations/v1/databases/schema-sync`;
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
Authorization: `Bearer ${creds.auth_token}`,
|
|
28
|
+
'x-access-key': creds.public_key,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
workspace_id: opts.workspace_id,
|
|
32
|
+
product_tag: opts.product_tag,
|
|
33
|
+
database_tag: opts.database_tag,
|
|
34
|
+
table_schemas: opts.table_schemas,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const body = await res.json();
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const msg = body.errors ?? body.message;
|
|
40
|
+
throw new Error(typeof msg === 'string' ? msg : `Schema sync failed (${res.status})`);
|
|
41
|
+
}
|
|
42
|
+
if (body.status === false) {
|
|
43
|
+
const msg = body.errors ?? body.message;
|
|
44
|
+
throw new Error(typeof msg === 'string' ? msg : 'Schema sync failed');
|
|
45
|
+
}
|
|
46
|
+
return (body.data ?? body);
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -5,6 +5,8 @@ import { loadSchemaFile, getSchemaPath, parseTableDef } from '../lib/schema-load
|
|
|
5
5
|
import { loadMigrationFiles, writeMigrationFile } from '../lib/migration-files.js';
|
|
6
6
|
import { fail } from '../lib/output.js';
|
|
7
7
|
import { getDbProxy, requireSession } from '../lib/proxy/context.js';
|
|
8
|
+
import { pushDatabaseSchema } from '../lib/integrations.js';
|
|
9
|
+
import type { TableSchemaRecord, TableColumnRecord } from '../lib/integrations.js';
|
|
8
10
|
import type { SchemaEntry, MongooseFieldDef } from '../lib/schema-loader.js';
|
|
9
11
|
import type {
|
|
10
12
|
IMigration,
|
|
@@ -335,3 +337,91 @@ export async function runDbSchemaGenerate(opts: SchemaGenerateOpts): Promise<voi
|
|
|
335
337
|
function requireSilent(): ReturnType<typeof requireSession> | null {
|
|
336
338
|
try { return requireSession(); } catch { return null; }
|
|
337
339
|
}
|
|
340
|
+
|
|
341
|
+
interface SchemaPushOpts {
|
|
342
|
+
db: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function adaptColumn(col: Record<string, unknown>): TableColumnRecord {
|
|
346
|
+
return {
|
|
347
|
+
name: String(col.name ?? ''),
|
|
348
|
+
type: String(col.type ?? 'string'),
|
|
349
|
+
nullable: col.nullable !== false && col.nullable !== 'NO',
|
|
350
|
+
primaryKey: Boolean(col.primaryKey ?? col.isPrimaryKey),
|
|
351
|
+
unique: Boolean(col.unique ?? col.isUnique),
|
|
352
|
+
autoIncrement: Boolean(col.autoIncrement ?? col.isAutoIncrement ?? col.autoGenerate),
|
|
353
|
+
...(col.defaultValue != null ? { defaultValue: col.defaultValue } : {}),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function runDbSchemaPush(opts: SchemaPushOpts): Promise<void> {
|
|
358
|
+
const session = requireSession();
|
|
359
|
+
const proxy = getDbProxy(session);
|
|
360
|
+
const envSlug = session.project.env_slug;
|
|
361
|
+
const productTag = session.project.product_tag;
|
|
362
|
+
const dbContext = { database: opts.db, env: envSlug, product: productTag };
|
|
363
|
+
|
|
364
|
+
console.log(`Connecting to database "${opts.db}" (env: ${envSlug})...`);
|
|
365
|
+
|
|
366
|
+
let tableNames: string[] = [];
|
|
367
|
+
try {
|
|
368
|
+
const raw = await proxy.execute<unknown>('schema.list', [null], dbContext);
|
|
369
|
+
if (Array.isArray(raw)) {
|
|
370
|
+
tableNames = (raw as unknown[]).map((r) =>
|
|
371
|
+
typeof r === 'string' ? r : String((r as Record<string, unknown>).name ?? ''),
|
|
372
|
+
).filter(Boolean);
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
throw new Error(`Failed to list tables: ${(e as Error).message}. Make sure the database is connected.`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (tableNames.length === 0) {
|
|
379
|
+
console.log('No tables found in the database. Nothing to push.');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log(`Found ${tableNames.length} table(s): ${tableNames.join(', ')}`);
|
|
384
|
+
|
|
385
|
+
const now = new Date().toISOString();
|
|
386
|
+
const tableSchemas: TableSchemaRecord[] = [];
|
|
387
|
+
|
|
388
|
+
for (const tableName of tableNames) {
|
|
389
|
+
try {
|
|
390
|
+
const tableSchema = await proxy.execute<unknown>('schema.describe', [tableName], dbContext);
|
|
391
|
+
const cols: unknown[] = Array.isArray((tableSchema as Record<string, unknown>)?.columns)
|
|
392
|
+
? ((tableSchema as Record<string, unknown>).columns as unknown[])
|
|
393
|
+
: Array.isArray(tableSchema) ? (tableSchema as unknown[]) : [];
|
|
394
|
+
|
|
395
|
+
if (cols.length === 0) {
|
|
396
|
+
console.warn(` [skip] ${tableName}: no columns returned`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const columns = (cols as Record<string, unknown>[])
|
|
401
|
+
.filter((c) => c.name)
|
|
402
|
+
.map(adaptColumn);
|
|
403
|
+
|
|
404
|
+
tableSchemas.push({ table: tableName, columns, updated_at: now });
|
|
405
|
+
console.log(` ${tableName}: ${columns.length} column(s)`);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.warn(` [skip] ${tableName}: ${(e as Error).message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (tableSchemas.length === 0) {
|
|
412
|
+
console.log('No schema data collected. Nothing pushed.');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log(`\nPushing schema for ${tableSchemas.length} table(s) to Ductape...`);
|
|
417
|
+
|
|
418
|
+
const result = await pushDatabaseSchema(session.apiUrl, session.credentials, {
|
|
419
|
+
workspace_id: session.project.workspace_id,
|
|
420
|
+
product_tag: productTag,
|
|
421
|
+
database_tag: opts.db,
|
|
422
|
+
table_schemas: tableSchemas,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
console.log(`Done. Synced ${result.synced} table(s): ${result.tables.join(', ')}`);
|
|
426
|
+
console.log('The AI payload generator will now use this schema for field guidance.');
|
|
427
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ 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
18
|
import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
|
|
19
|
-
import { runDbSchemaGenerate } from './commands/db-schema.js';
|
|
19
|
+
import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
|
|
20
20
|
import { runGraph } from './commands/graph.js';
|
|
21
21
|
import { runSecrets } from './commands/secrets.js';
|
|
22
22
|
import {
|
|
@@ -398,6 +398,12 @@ dbSchema
|
|
|
398
398
|
.option('--destructive', 'Also generate drop migrations for removed fields/tables')
|
|
399
399
|
.action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
|
|
400
400
|
|
|
401
|
+
dbSchema
|
|
402
|
+
.command('push')
|
|
403
|
+
.description('Read live table schema from the database and sync it to the Ductape server (enables AI field guidance)')
|
|
404
|
+
.requiredOption('--db <tag>', 'Database tag to sync')
|
|
405
|
+
.action(wrap((opts) => runDbSchemaPush({ db: opts.db })));
|
|
406
|
+
|
|
401
407
|
db
|
|
402
408
|
.argument('[verb]', 'connect | query | …')
|
|
403
409
|
.option('-f, --file <path>')
|
package/src/lib/integrations.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { Credentials } from './config.js';
|
|
2
2
|
|
|
3
|
+
export interface TableColumnRecord {
|
|
4
|
+
name: string;
|
|
5
|
+
type: string;
|
|
6
|
+
nullable: boolean;
|
|
7
|
+
primaryKey: boolean;
|
|
8
|
+
unique: boolean;
|
|
9
|
+
autoIncrement: boolean;
|
|
10
|
+
defaultValue?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TableSchemaRecord {
|
|
14
|
+
table: string;
|
|
15
|
+
columns: TableColumnRecord[];
|
|
16
|
+
updated_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export interface GeneratePayloadRequest {
|
|
4
20
|
workspace_id: string;
|
|
5
21
|
user_id: string;
|
|
@@ -45,3 +61,41 @@ export async function generateExecutablePayload(
|
|
|
45
61
|
}
|
|
46
62
|
return (body.data ?? body) as GeneratePayloadResponse;
|
|
47
63
|
}
|
|
64
|
+
|
|
65
|
+
export async function pushDatabaseSchema(
|
|
66
|
+
apiUrl: string,
|
|
67
|
+
creds: Credentials,
|
|
68
|
+
opts: {
|
|
69
|
+
workspace_id: string;
|
|
70
|
+
product_tag: string;
|
|
71
|
+
database_tag: string;
|
|
72
|
+
table_schemas: TableSchemaRecord[];
|
|
73
|
+
},
|
|
74
|
+
): Promise<{ synced: number; tables: string[] }> {
|
|
75
|
+
const url = `${apiUrl}/integrations/v1/databases/schema-sync`;
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
Authorization: `Bearer ${creds.auth_token}`,
|
|
81
|
+
'x-access-key': creds.public_key,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
workspace_id: opts.workspace_id,
|
|
85
|
+
product_tag: opts.product_tag,
|
|
86
|
+
database_tag: opts.database_tag,
|
|
87
|
+
table_schemas: opts.table_schemas,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const body = await res.json() as Record<string, unknown>;
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
const msg = (body as any).errors ?? (body as any).message;
|
|
94
|
+
throw new Error(typeof msg === 'string' ? msg : `Schema sync failed (${res.status})`);
|
|
95
|
+
}
|
|
96
|
+
if (body.status === false) {
|
|
97
|
+
const msg = (body as any).errors ?? (body as any).message;
|
|
98
|
+
throw new Error(typeof msg === 'string' ? msg : 'Schema sync failed');
|
|
99
|
+
}
|
|
100
|
+
return (body.data ?? body) as { synced: number; tables: string[] };
|
|
101
|
+
}
|