@ductape/cli 0.2.9 → 0.2.11
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-migrate.js +5 -5
- 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-migrate.ts +5 -7
- package/src/commands/db-schema.ts +90 -0
- package/src/index.ts +7 -1
- package/src/lib/integrations.ts +54 -0
|
@@ -6,9 +6,9 @@ import { fail, printJson } from '../lib/output.js';
|
|
|
6
6
|
function buildDbContext(dbTag, envSlug, productTag) {
|
|
7
7
|
return { database: dbTag, env: envSlug, product: productTag };
|
|
8
8
|
}
|
|
9
|
-
async function getAppliedTags(proxy, dbContext
|
|
9
|
+
async function getAppliedTags(proxy, dbContext) {
|
|
10
10
|
try {
|
|
11
|
-
const result = await proxy.execute('migration.
|
|
11
|
+
const result = await proxy.execute('migration.history', [], dbContext);
|
|
12
12
|
const list = Array.isArray(result)
|
|
13
13
|
? result
|
|
14
14
|
: Array.isArray(result.data)
|
|
@@ -44,7 +44,7 @@ export async function runDbMigrate(opts) {
|
|
|
44
44
|
console.log(`[${db}] No migration files found. Run \`ductape db schema generate\` first.`);
|
|
45
45
|
continue;
|
|
46
46
|
}
|
|
47
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
47
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
48
48
|
const pending = migrations.filter((m) => !appliedTags.includes(m.tag));
|
|
49
49
|
if (pending.length === 0) {
|
|
50
50
|
console.log(`[${db}] Already up to date.`);
|
|
@@ -92,7 +92,7 @@ export async function runDbMigrateStatus(opts) {
|
|
|
92
92
|
const { db } = entry;
|
|
93
93
|
const dbContext = buildDbContext(db, envSlug, productTag);
|
|
94
94
|
const migrations = loadMigrationFiles(dir, db);
|
|
95
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
95
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
96
96
|
const applied = migrations.filter((m) => appliedTags.includes(m.tag)).map((m) => m.tag);
|
|
97
97
|
const pending = migrations.filter((m) => !appliedTags.includes(m.tag)).map((m) => m.tag);
|
|
98
98
|
statusReport[db] = { applied, pending };
|
|
@@ -132,7 +132,7 @@ export async function runDbMigrateRollback(opts) {
|
|
|
132
132
|
const { db } = entry;
|
|
133
133
|
const dbContext = buildDbContext(db, envSlug, productTag);
|
|
134
134
|
const migrations = loadMigrationFiles(dir, db);
|
|
135
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
135
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
136
136
|
const applied = migrations.filter((m) => appliedTags.includes(m.tag));
|
|
137
137
|
const toRollback = applied.slice(-rollbackCount).reverse();
|
|
138
138
|
if (toRollback.length === 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
|
@@ -30,13 +30,11 @@ function buildDbContext(dbTag: string, envSlug: string, productTag: string): Dat
|
|
|
30
30
|
async function getAppliedTags(
|
|
31
31
|
proxy: ReturnType<typeof getDbProxy>,
|
|
32
32
|
dbContext: DatabaseContext,
|
|
33
|
-
productTag: string,
|
|
34
|
-
envSlug: string,
|
|
35
33
|
): Promise<string[]> {
|
|
36
34
|
try {
|
|
37
35
|
const result = await proxy.execute<unknown>(
|
|
38
|
-
'migration.
|
|
39
|
-
[
|
|
36
|
+
'migration.history',
|
|
37
|
+
[],
|
|
40
38
|
dbContext,
|
|
41
39
|
);
|
|
42
40
|
const list = Array.isArray(result)
|
|
@@ -80,7 +78,7 @@ export async function runDbMigrate(opts: MigrateOpts): Promise<void> {
|
|
|
80
78
|
continue;
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
81
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
84
82
|
const pending = migrations.filter((m) => !appliedTags.includes(m.tag));
|
|
85
83
|
|
|
86
84
|
if (pending.length === 0) {
|
|
@@ -139,7 +137,7 @@ export async function runDbMigrateStatus(opts: MigrateStatusOpts): Promise<void>
|
|
|
139
137
|
const { db } = entry;
|
|
140
138
|
const dbContext = buildDbContext(db, envSlug, productTag);
|
|
141
139
|
const migrations = loadMigrationFiles(dir, db);
|
|
142
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
140
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
143
141
|
|
|
144
142
|
const applied = migrations.filter((m) => appliedTags.includes(m.tag)).map((m) => m.tag);
|
|
145
143
|
const pending = migrations.filter((m) => !appliedTags.includes(m.tag)).map((m) => m.tag);
|
|
@@ -183,7 +181,7 @@ export async function runDbMigrateRollback(opts: MigrateRollbackOpts): Promise<v
|
|
|
183
181
|
const { db } = entry;
|
|
184
182
|
const dbContext = buildDbContext(db, envSlug, productTag);
|
|
185
183
|
const migrations = loadMigrationFiles(dir, db);
|
|
186
|
-
const appliedTags = await getAppliedTags(proxy, dbContext
|
|
184
|
+
const appliedTags = await getAppliedTags(proxy, dbContext);
|
|
187
185
|
|
|
188
186
|
const applied = migrations.filter((m) => appliedTags.includes(m.tag));
|
|
189
187
|
const toRollback = applied.slice(-rollbackCount).reverse();
|
|
@@ -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
|
+
}
|