@ductape/cli 0.2.23 → 0.3.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/CHANGELOG.md +7 -0
- package/README.md +7 -0
- package/dist/commands/db-migrate.js +47 -8
- package/dist/commands/migrate-codebase.d.ts +190 -0
- package/dist/commands/migrate-codebase.js +776 -0
- package/dist/commands/migration-artifact.d.ts +28 -0
- package/dist/commands/migration-artifact.js +101 -0
- package/dist/commands/migration-assurance.d.ts +10 -0
- package/dist/commands/migration-assurance.js +120 -0
- package/dist/commands/migration-capabilities.d.ts +53 -0
- package/dist/commands/migration-capabilities.js +66 -0
- package/dist/commands/migration-database.d.ts +26 -0
- package/dist/commands/migration-database.js +162 -0
- package/dist/commands/migration-e2e.d.ts +68 -0
- package/dist/commands/migration-e2e.js +189 -0
- package/dist/commands/migration-environments.d.ts +20 -0
- package/dist/commands/migration-environments.js +58 -0
- package/dist/commands/migration-generated.d.ts +68 -0
- package/dist/commands/migration-generated.js +217 -0
- package/dist/commands/migration-portfolio.d.ts +88 -0
- package/dist/commands/migration-portfolio.js +223 -0
- package/dist/commands/migration-products.d.ts +23 -0
- package/dist/commands/migration-products.js +88 -0
- package/dist/commands/migration-review.d.ts +248 -0
- package/dist/commands/migration-review.js +624 -0
- package/dist/commands/migration-secrets.d.ts +24 -0
- package/dist/commands/migration-secrets.js +93 -0
- package/dist/commands/migration-slice.d.ts +132 -0
- package/dist/commands/migration-slice.js +266 -0
- package/dist/commands/migration-verification.d.ts +12 -0
- package/dist/commands/migration-verification.js +194 -0
- package/dist/commands/products.d.ts +1 -0
- package/dist/commands/products.js +33 -1
- package/dist/commands/secrets.d.ts +8 -0
- package/dist/commands/secrets.js +44 -0
- package/dist/index.js +376 -4
- package/dist/lib/migration-artifact.d.ts +19 -0
- package/dist/lib/migration-artifact.js +114 -0
- package/package.json +8 -1
- package/API_PARITY.md +0 -58
- package/DB_MIGRATE_PLAN.md +0 -223
- package/ductape.example.ts +0 -18
- package/src/commands/apply.ts +0 -153
- package/src/commands/apps-import.ts +0 -43
- package/src/commands/apps.ts +0 -92
- package/src/commands/cloud.ts +0 -68
- package/src/commands/completion.ts +0 -119
- package/src/commands/db-migrate.ts +0 -209
- package/src/commands/db-schema.ts +0 -429
- package/src/commands/db.ts +0 -63
- package/src/commands/generate.ts +0 -74
- package/src/commands/graph.ts +0 -51
- package/src/commands/init.ts +0 -39
- package/src/commands/install.ts +0 -76
- package/src/commands/link.ts +0 -84
- package/src/commands/login.ts +0 -145
- package/src/commands/logout.ts +0 -7
- package/src/commands/platform.ts +0 -123
- package/src/commands/products.ts +0 -228
- package/src/commands/profiles.ts +0 -23
- package/src/commands/resources.ts +0 -213
- package/src/commands/secrets.ts +0 -46
- package/src/commands/unlink.ts +0 -15
- package/src/commands/whoami.ts +0 -27
- package/src/commands/workspaces.ts +0 -98
- package/src/index.ts +0 -523
- package/src/lib/api-client.ts +0 -88
- package/src/lib/app-import.ts +0 -82
- package/src/lib/apply-loaders.ts +0 -30
- package/src/lib/config.ts +0 -230
- package/src/lib/context-store.ts +0 -77
- package/src/lib/db-methods.ts +0 -56
- package/src/lib/db-types.ts +0 -104
- package/src/lib/encryption.ts +0 -12
- package/src/lib/forms/enums.ts +0 -52
- package/src/lib/forms/index.ts +0 -8
- package/src/lib/forms/prompt.ts +0 -209
- package/src/lib/forms/schemas.ts +0 -288
- package/src/lib/forms/types.ts +0 -27
- package/src/lib/http.ts +0 -76
- package/src/lib/integrations.ts +0 -110
- package/src/lib/interactive-opts.ts +0 -13
- package/src/lib/migration-files.ts +0 -58
- package/src/lib/output.ts +0 -39
- package/src/lib/platform-api.ts +0 -209
- package/src/lib/proxy/context.ts +0 -94
- package/src/lib/proxy/db-proxy.ts +0 -40
- package/src/lib/proxy/graph-proxy.ts +0 -64
- package/src/lib/proxy/sdk-proxy.ts +0 -58
- package/src/lib/read-body.ts +0 -57
- package/src/lib/resources.ts +0 -160
- package/src/lib/schema-loader.ts +0 -152
- package/src/lib/snippets.ts +0 -165
- package/src/lib/templates.ts +0 -157
- package/src/lib/workspace-context.ts +0 -43
- package/src/lib/workspaces.ts +0 -283
- package/test/apply-loaders.test.ts +0 -40
- package/test/encryption.test.ts +0 -20
- package/test/forms.test.ts +0 -25
- package/test/http.test.ts +0 -15
- package/test/platform-api.test.ts +0 -42
- package/test/resources.test.ts +0 -23
- package/test/workspace-resolve.test.ts +0 -29
- package/tsconfig.json +0 -15
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { findProjectConfig } from '../lib/config.js';
|
|
4
|
-
import { loadSchemaFile, getSchemaPath, parseTableDef } from '../lib/schema-loader.js';
|
|
5
|
-
import { loadMigrationFiles, writeMigrationFile } from '../lib/migration-files.js';
|
|
6
|
-
import { fail } from '../lib/output.js';
|
|
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';
|
|
10
|
-
import type { SchemaEntry, MongooseFieldDef } from '../lib/schema-loader.js';
|
|
11
|
-
import type {
|
|
12
|
-
IMigration,
|
|
13
|
-
IMigrationOperation,
|
|
14
|
-
ICreateCollectionOp,
|
|
15
|
-
IDropCollectionOp,
|
|
16
|
-
IAddFieldOp,
|
|
17
|
-
ICreateIndexOp,
|
|
18
|
-
} from '../lib/db-types.js';
|
|
19
|
-
|
|
20
|
-
interface SchemaGenerateOpts {
|
|
21
|
-
db?: string;
|
|
22
|
-
destructive?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface ReconstructedTable {
|
|
26
|
-
fields: Set<string>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function replayMigrations(migrations: IMigration[]): Map<string, ReconstructedTable> {
|
|
30
|
-
const schema = new Map<string, ReconstructedTable>();
|
|
31
|
-
for (const migration of migrations) {
|
|
32
|
-
for (const op of migration.up) {
|
|
33
|
-
if (op.type === 'createCollection') {
|
|
34
|
-
if (!schema.has(op.name)) {
|
|
35
|
-
schema.set(op.name, { fields: new Set(op.fields.map((f) => f.name)) });
|
|
36
|
-
}
|
|
37
|
-
} else if (op.type === 'dropCollection') {
|
|
38
|
-
schema.delete(op.name);
|
|
39
|
-
} else if (op.type === 'addField') {
|
|
40
|
-
const t = schema.get(op.collection);
|
|
41
|
-
if (t) t.fields.add(op.field.name);
|
|
42
|
-
} else if (op.type === 'dropField') {
|
|
43
|
-
const t = schema.get(op.collection);
|
|
44
|
-
if (t) t.fields.delete(op.fieldName);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return schema;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function columnTypeToMongooseType(raw: string): string {
|
|
52
|
-
// Normalise: uppercase, collapse whitespace (handles "character varying", "timestamp without time zone", etc.)
|
|
53
|
-
const t = raw.toUpperCase().replace(/\s+/g, ' ').trim();
|
|
54
|
-
switch (t) {
|
|
55
|
-
case 'BIGINT': case 'INT8': return 'bigint';
|
|
56
|
-
case 'SMALLINT': case 'TINYINT': case 'INT2': return 'smallint';
|
|
57
|
-
case 'INTEGER': case 'INT': case 'INT4': case 'NUMBER': return 'number';
|
|
58
|
-
case 'FLOAT': case 'REAL': case 'FLOAT4': case 'FLOAT8': case 'DOUBLE PRECISION': return 'number'; // caller sets float:true
|
|
59
|
-
case 'DOUBLE': return 'double';
|
|
60
|
-
case 'DECIMAL': case 'NUMERIC': case 'DECIMAL128': return 'decimal';
|
|
61
|
-
case 'TEXT': case 'MEDIUMTEXT': case 'LONGTEXT': case 'TINYTEXT': return 'text';
|
|
62
|
-
case 'STRING': case 'VARCHAR': case 'CHARACTER VARYING': case 'CHAR': case 'CHARACTER': case 'BPCHAR': return 'string';
|
|
63
|
-
case 'BOOLEAN': case 'BOOL': return 'boolean';
|
|
64
|
-
case 'DATE': case 'DATETIME': return 'date';
|
|
65
|
-
case 'TIME': case 'TIME WITHOUT TIME ZONE': case 'TIME WITH TIME ZONE': return 'time';
|
|
66
|
-
case 'TIMESTAMP': case 'TIMESTAMPTZ': case 'TIMESTAMP WITHOUT TIME ZONE': case 'TIMESTAMP WITH TIME ZONE': return 'timestamp';
|
|
67
|
-
case 'JSON': case 'JSONB': return 'json';
|
|
68
|
-
case 'BINARY': case 'VARBINARY': case 'BYTEA': return 'binary';
|
|
69
|
-
case 'BLOB': case 'MEDIUMBLOB': case 'LONGBLOB': case 'TINYBLOB': return 'blob';
|
|
70
|
-
case 'UUID': case 'OBJECTID': return 'uuid';
|
|
71
|
-
case 'OBJECT': case 'MIXED': case 'MAP': case 'HSTORE': return 'mixed';
|
|
72
|
-
case 'ARRAY': return 'array';
|
|
73
|
-
default: return 'string';
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildMongooseFieldDef(col: Record<string, unknown>): MongooseFieldDef {
|
|
78
|
-
const rawType = String(col.type ?? 'string');
|
|
79
|
-
const mongooseType = columnTypeToMongooseType(rawType);
|
|
80
|
-
const def: MongooseFieldDef = { type: mongooseType };
|
|
81
|
-
|
|
82
|
-
const upperType = rawType.toUpperCase().trim();
|
|
83
|
-
if (upperType === 'FLOAT' || upperType === 'REAL' || upperType === 'FLOAT4' || upperType === 'FLOAT8' || upperType === 'DOUBLE PRECISION') def.float = true;
|
|
84
|
-
|
|
85
|
-
// Handle both IColumnDefinition style (primaryKey/unique) and IColumnInfo style (isPrimaryKey/isUnique)
|
|
86
|
-
if (col.nullable === false || col.required === true) def.required = true;
|
|
87
|
-
if (col.unique === true || col.isUnique === true) def.unique = true;
|
|
88
|
-
if (col.primaryKey === true || col.isPrimaryKey === true) def.primaryKey = true;
|
|
89
|
-
if (col.autoIncrement === true || col.isAutoIncrement === true || col.autoGenerate === true) def.autoGenerate = true;
|
|
90
|
-
|
|
91
|
-
// Handle both camelCase (defaultValue) and snake_case (default_value) from different adapters
|
|
92
|
-
const defVal = col.defaultValue ?? col.default_value;
|
|
93
|
-
if (defVal !== undefined && defVal !== null) def.default = defVal as unknown;
|
|
94
|
-
|
|
95
|
-
// Handle both camelCase (length/maxLength) and snake_case (max_length) from different adapters
|
|
96
|
-
const len = col.length ?? col.maxLength ?? col.max_length;
|
|
97
|
-
if (typeof len === 'number') def.maxlength = len;
|
|
98
|
-
|
|
99
|
-
if (Array.isArray(col.enumValues) && (col.enumValues as string[]).length > 0) {
|
|
100
|
-
def.type = 'string';
|
|
101
|
-
def.enum = col.enumValues as string[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return def;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function tryHydrateFromCloud(
|
|
108
|
-
dir: string,
|
|
109
|
-
schemaPath: string,
|
|
110
|
-
opts: SchemaGenerateOpts,
|
|
111
|
-
): Promise<SchemaEntry[]> {
|
|
112
|
-
if (!opts.db) return [];
|
|
113
|
-
|
|
114
|
-
let session: ReturnType<typeof requireSession>;
|
|
115
|
-
try {
|
|
116
|
-
session = requireSession();
|
|
117
|
-
} catch {
|
|
118
|
-
return [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const proxy = getDbProxy(session);
|
|
122
|
-
const envSlug = session.project.env_slug;
|
|
123
|
-
const productTag = session.project.product_tag;
|
|
124
|
-
const dbContext = { database: opts.db, env: envSlug, product: productTag };
|
|
125
|
-
|
|
126
|
-
let tableNames: string[] = [];
|
|
127
|
-
try {
|
|
128
|
-
const raw = await proxy.execute<unknown>('schema.list', [null], dbContext);
|
|
129
|
-
if (Array.isArray(raw)) {
|
|
130
|
-
tableNames = (raw as unknown[]).map((r) =>
|
|
131
|
-
typeof r === 'string' ? r : typeof (r as Record<string, unknown>).name === 'string' ? String((r as Record<string, unknown>).name) : null,
|
|
132
|
-
).filter((n): n is string => n !== null);
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (tableNames.length === 0) return [];
|
|
139
|
-
|
|
140
|
-
const tables: Record<string, Record<string, MongooseFieldDef>> = {};
|
|
141
|
-
|
|
142
|
-
for (const tableName of tableNames) {
|
|
143
|
-
try {
|
|
144
|
-
const tableSchema = await proxy.execute<unknown>('schema.describe', [tableName], dbContext);
|
|
145
|
-
const cols: unknown[] = Array.isArray((tableSchema as Record<string, unknown>)?.columns)
|
|
146
|
-
? ((tableSchema as Record<string, unknown>).columns as unknown[])
|
|
147
|
-
: Array.isArray(tableSchema)
|
|
148
|
-
? (tableSchema as unknown[])
|
|
149
|
-
: [];
|
|
150
|
-
|
|
151
|
-
if (cols.length === 0) {
|
|
152
|
-
tables[tableName] = {};
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const indexedFields = new Set<string>();
|
|
157
|
-
const uniqueIndexFields = new Set<string>();
|
|
158
|
-
const rawIndexes = (tableSchema as Record<string, unknown>)?.indexes;
|
|
159
|
-
if (Array.isArray(rawIndexes)) {
|
|
160
|
-
for (const idx of rawIndexes as Record<string, unknown>[]) {
|
|
161
|
-
const idxCols = Array.isArray(idx.columns) ? (idx.columns as Record<string, unknown>[]) : [];
|
|
162
|
-
if (idxCols.length === 1) {
|
|
163
|
-
const fieldName = String(idxCols[0].name ?? idxCols[0]);
|
|
164
|
-
if (idx.unique) uniqueIndexFields.add(fieldName);
|
|
165
|
-
else indexedFields.add(fieldName);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const fieldMap: Record<string, MongooseFieldDef> = {};
|
|
171
|
-
for (const col of cols as Record<string, unknown>[]) {
|
|
172
|
-
const name = String(col.name ?? '');
|
|
173
|
-
if (!name) continue;
|
|
174
|
-
const def = buildMongooseFieldDef(col);
|
|
175
|
-
if (uniqueIndexFields.has(name)) { def.unique = true; def.index = true; }
|
|
176
|
-
else if (indexedFields.has(name)) def.index = true;
|
|
177
|
-
fieldMap[name] = def;
|
|
178
|
-
}
|
|
179
|
-
tables[tableName] = fieldMap;
|
|
180
|
-
} catch {
|
|
181
|
-
tables[tableName] = {};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const entry: SchemaEntry = { db: opts.db, tables };
|
|
186
|
-
const entries = [entry];
|
|
187
|
-
fs.writeFileSync(schemaPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
188
|
-
console.log(`[${opts.db}] Hydrated schema.json from cloud (${tableNames.length} collection(s)): ${tableNames.join(', ')}`);
|
|
189
|
-
return entries;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function runDbSchemaGenerate(opts: SchemaGenerateOpts): Promise<void> {
|
|
193
|
-
const found = findProjectConfig();
|
|
194
|
-
if (!found) fail('No linked project. Run `ductape link` from your project directory.');
|
|
195
|
-
|
|
196
|
-
const { dir } = found;
|
|
197
|
-
const schemaPath = getSchemaPath(dir);
|
|
198
|
-
|
|
199
|
-
let schemaEntries: SchemaEntry[];
|
|
200
|
-
let wasScaffolded = false;
|
|
201
|
-
|
|
202
|
-
if (!fs.existsSync(schemaPath)) {
|
|
203
|
-
// Create directory and empty schema.json first
|
|
204
|
-
fs.mkdirSync(path.dirname(schemaPath), { recursive: true });
|
|
205
|
-
fs.writeFileSync(schemaPath, '[]', 'utf8');
|
|
206
|
-
wasScaffolded = true;
|
|
207
|
-
console.log(`Created ${path.relative(dir, schemaPath)}`);
|
|
208
|
-
|
|
209
|
-
// Try to hydrate from the cloud (requires --db and an active session)
|
|
210
|
-
schemaEntries = await tryHydrateFromCloud(dir, schemaPath, opts);
|
|
211
|
-
|
|
212
|
-
if (schemaEntries.length === 0) {
|
|
213
|
-
const hint = opts.db
|
|
214
|
-
? `(no existing collections found in "${opts.db}" for env "${requireSilent()?.project?.env_slug ?? 'default'}")`
|
|
215
|
-
: '(pass --db <tag> to auto-import existing collections from the cloud)';
|
|
216
|
-
console.log(`schema.json is empty ${hint}. Add your collection definitions and run \`ductape db schema generate\` again.`);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
schemaEntries = loadSchemaFile(dir);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const targets = opts.db
|
|
224
|
-
? schemaEntries.filter((e) => e.db === opts.db)
|
|
225
|
-
: schemaEntries;
|
|
226
|
-
|
|
227
|
-
if (targets.length === 0) {
|
|
228
|
-
if (wasScaffolded) return; // already printed a message above
|
|
229
|
-
fail(opts.db ? `No db entry found for tag "${opts.db}".` : 'schema.json is empty.');
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let totalGenerated = 0;
|
|
233
|
-
|
|
234
|
-
for (const entry of targets) {
|
|
235
|
-
const { db, tables } = entry;
|
|
236
|
-
const existing = loadMigrationFiles(dir, db);
|
|
237
|
-
const current = replayMigrations(existing);
|
|
238
|
-
const newMigrations: IMigration[] = [];
|
|
239
|
-
let counter = existing.length + 1;
|
|
240
|
-
|
|
241
|
-
for (const [tableName, fieldDefs] of Object.entries(tables)) {
|
|
242
|
-
const parsed = parseTableDef(tableName, fieldDefs);
|
|
243
|
-
const currentTable = current.get(tableName);
|
|
244
|
-
|
|
245
|
-
if (!currentTable) {
|
|
246
|
-
const up: IMigrationOperation[] = [
|
|
247
|
-
{
|
|
248
|
-
type: 'createCollection',
|
|
249
|
-
name: tableName,
|
|
250
|
-
fields: parsed.fields,
|
|
251
|
-
ifNotExists: true,
|
|
252
|
-
} as ICreateCollectionOp,
|
|
253
|
-
];
|
|
254
|
-
for (const { fieldName, unique } of parsed.indexFields) {
|
|
255
|
-
up.push({
|
|
256
|
-
type: 'createIndex',
|
|
257
|
-
collection: tableName,
|
|
258
|
-
name: `${tableName}_${fieldName}_idx`,
|
|
259
|
-
fields: [{ name: fieldName }],
|
|
260
|
-
unique,
|
|
261
|
-
ifNotExists: true,
|
|
262
|
-
} as ICreateIndexOp);
|
|
263
|
-
}
|
|
264
|
-
const down: IMigrationOperation[] = [
|
|
265
|
-
{ type: 'dropCollection', name: tableName, ifExists: true } as IDropCollectionOp,
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
newMigrations.push({
|
|
269
|
-
tag: `create_${tableName}`,
|
|
270
|
-
name: `Create collection ${tableName}`,
|
|
271
|
-
up,
|
|
272
|
-
down,
|
|
273
|
-
createdAt: new Date().toISOString(),
|
|
274
|
-
});
|
|
275
|
-
} else {
|
|
276
|
-
const addOps: IMigrationOperation[] = [];
|
|
277
|
-
const dropOps: IMigrationOperation[] = [];
|
|
278
|
-
|
|
279
|
-
for (const field of parsed.fields) {
|
|
280
|
-
if (!currentTable.fields.has(field.name)) {
|
|
281
|
-
addOps.push({ type: 'addField', collection: tableName, field } as IAddFieldOp);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const desiredFields = new Set(parsed.fields.map((f) => f.name));
|
|
286
|
-
const removedFields: string[] = [];
|
|
287
|
-
for (const existingField of currentTable.fields) {
|
|
288
|
-
if (!desiredFields.has(existingField)) {
|
|
289
|
-
removedFields.push(existingField);
|
|
290
|
-
if (opts.destructive) {
|
|
291
|
-
dropOps.push({ type: 'dropField', collection: tableName, fieldName: existingField });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (!opts.destructive && removedFields.length > 0) {
|
|
296
|
-
console.warn(
|
|
297
|
-
` [${db}] Warning: field(s) removed from schema.json for "${tableName}": ${removedFields.join(', ')}. Pass --destructive to generate drop migrations.`,
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const allOps = [...addOps, ...dropOps];
|
|
302
|
-
if (allOps.length > 0) {
|
|
303
|
-
const down: IMigrationOperation[] = addOps.map((op) => ({
|
|
304
|
-
type: 'dropField' as const,
|
|
305
|
-
collection: tableName,
|
|
306
|
-
fieldName: (op as IAddFieldOp).field.name,
|
|
307
|
-
}));
|
|
308
|
-
|
|
309
|
-
newMigrations.push({
|
|
310
|
-
tag: `alter_${tableName}_${Date.now()}`,
|
|
311
|
-
name: `Alter collection ${tableName}`,
|
|
312
|
-
up: allOps,
|
|
313
|
-
down,
|
|
314
|
-
createdAt: new Date().toISOString(),
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (newMigrations.length === 0) {
|
|
321
|
-
console.log(`[${db}] Schema is up to date. No migrations generated.`);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
for (const migration of newMigrations) {
|
|
326
|
-
const filePath = writeMigrationFile(dir, db, migration, counter++);
|
|
327
|
-
console.log(`[${db}] Generated ${path.basename(filePath)}`);
|
|
328
|
-
totalGenerated++;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (totalGenerated > 0) {
|
|
333
|
-
console.log(`\nGenerated ${totalGenerated} migration file(s). Run \`ductape db migrate\` to apply them.`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function requireSilent(): ReturnType<typeof requireSession> | null {
|
|
338
|
-
try { return requireSession(); } catch { return null; }
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
interface SchemaPushOpts {
|
|
342
|
-
db: string;
|
|
343
|
-
env: string;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function adaptColumn(col: Record<string, unknown>): TableColumnRecord {
|
|
347
|
-
return {
|
|
348
|
-
name: String(col.name ?? ''),
|
|
349
|
-
type: String(col.type ?? 'string'),
|
|
350
|
-
nullable: col.nullable !== false && col.nullable !== 'NO',
|
|
351
|
-
primaryKey: Boolean(col.primaryKey ?? col.isPrimaryKey),
|
|
352
|
-
unique: Boolean(col.unique ?? col.isUnique),
|
|
353
|
-
autoIncrement: Boolean(col.autoIncrement ?? col.isAutoIncrement ?? col.autoGenerate),
|
|
354
|
-
...(col.defaultValue != null ? { defaultValue: col.defaultValue } : {}),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export async function runDbSchemaPush(opts: SchemaPushOpts): Promise<void> {
|
|
359
|
-
const session = requireSession();
|
|
360
|
-
const proxy = getDbProxy(session);
|
|
361
|
-
const envSlug = opts.env;
|
|
362
|
-
const productTag = session.project.product_tag;
|
|
363
|
-
const dbContext = { database: opts.db, env: envSlug, product: productTag };
|
|
364
|
-
|
|
365
|
-
console.log(`Connecting to database "${opts.db}" (env: ${envSlug})...`);
|
|
366
|
-
|
|
367
|
-
let tableNames: string[] = [];
|
|
368
|
-
try {
|
|
369
|
-
const raw = await proxy.execute<unknown>('schema.list', [null], dbContext);
|
|
370
|
-
if (Array.isArray(raw)) {
|
|
371
|
-
tableNames = (raw as unknown[]).map((r) =>
|
|
372
|
-
typeof r === 'string' ? r : String((r as Record<string, unknown>).name ?? ''),
|
|
373
|
-
).filter((n) => Boolean(n) && !n.startsWith('_ductape_'));
|
|
374
|
-
}
|
|
375
|
-
} catch (e) {
|
|
376
|
-
throw new Error(`Failed to list tables: ${(e as Error).message}. Make sure the database is connected.`);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (tableNames.length === 0) {
|
|
380
|
-
console.log('No tables found in the database. Nothing to push.');
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
console.log(`Found ${tableNames.length} table(s): ${tableNames.join(', ')}`);
|
|
385
|
-
|
|
386
|
-
const now = new Date().toISOString();
|
|
387
|
-
const tableSchemas: TableSchemaRecord[] = [];
|
|
388
|
-
|
|
389
|
-
for (const tableName of tableNames) {
|
|
390
|
-
try {
|
|
391
|
-
const tableSchema = await proxy.execute<unknown>('schema.describe', [tableName], dbContext);
|
|
392
|
-
const cols: unknown[] = Array.isArray((tableSchema as Record<string, unknown>)?.columns)
|
|
393
|
-
? ((tableSchema as Record<string, unknown>).columns as unknown[])
|
|
394
|
-
: Array.isArray(tableSchema) ? (tableSchema as unknown[]) : [];
|
|
395
|
-
|
|
396
|
-
if (cols.length === 0) {
|
|
397
|
-
console.warn(` [skip] ${tableName}: no columns returned`);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const columns = (cols as Record<string, unknown>[])
|
|
402
|
-
.filter((c) => c.name)
|
|
403
|
-
.map(adaptColumn);
|
|
404
|
-
|
|
405
|
-
tableSchemas.push({ table: tableName, columns, updated_at: now });
|
|
406
|
-
console.log(` ${tableName}: ${columns.length} column(s)`);
|
|
407
|
-
} catch (e) {
|
|
408
|
-
console.warn(` [skip] ${tableName}: ${(e as Error).message}`);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (tableSchemas.length === 0) {
|
|
413
|
-
console.log('No schema data collected. Nothing pushed.');
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
console.log(`\nPushing schema for ${tableSchemas.length} table(s) to Ductape...`);
|
|
418
|
-
|
|
419
|
-
const result = await pushDatabaseSchema(session.apiUrl, session.credentials, {
|
|
420
|
-
workspace_id: session.project.workspace_id,
|
|
421
|
-
product_tag: productTag,
|
|
422
|
-
database_tag: opts.db,
|
|
423
|
-
env_slug: envSlug,
|
|
424
|
-
table_schemas: tableSchemas,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
console.log(`Done. Synced ${result.synced} table(s): ${result.tables.join(', ')}`);
|
|
428
|
-
console.log('The AI payload generator will now use this schema for field guidance.');
|
|
429
|
-
}
|
package/src/commands/db.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { readJsonBody } from '../lib/read-body.js';
|
|
2
|
-
import { setDatabaseContext } from '../lib/context-store.js';
|
|
3
|
-
import { DB_PROXY_METHODS } from '../lib/db-methods.js';
|
|
4
|
-
import { getDbContext, getDbProxy, requireSession } from '../lib/proxy/context.js';
|
|
5
|
-
import { loadRuntimeContext } from '../lib/context-store.js';
|
|
6
|
-
import { printJson } from '../lib/output.js';
|
|
7
|
-
|
|
8
|
-
export async function runDb(
|
|
9
|
-
method: string,
|
|
10
|
-
opts: { file?: string; json?: boolean },
|
|
11
|
-
): Promise<void> {
|
|
12
|
-
const session = requireSession();
|
|
13
|
-
const proxy = getDbProxy(session);
|
|
14
|
-
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
15
|
-
const dbContext = runtime?.database_context ?? null;
|
|
16
|
-
const body = readJsonBody(opts.file) as Record<string, unknown>;
|
|
17
|
-
|
|
18
|
-
const m = method.toLowerCase();
|
|
19
|
-
if (!DB_PROXY_METHODS.includes(m as (typeof DB_PROXY_METHODS)[number]) && !m.includes('.')) {
|
|
20
|
-
console.warn(`Method "${m}" not in known list; sending anyway.`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let params: unknown[] = [];
|
|
24
|
-
if (m === 'connect') {
|
|
25
|
-
const cfg = {
|
|
26
|
-
product: session.project.product_tag,
|
|
27
|
-
env: session.project.env_slug,
|
|
28
|
-
database: body.database ?? body.tag,
|
|
29
|
-
...body,
|
|
30
|
-
};
|
|
31
|
-
params = [cfg];
|
|
32
|
-
} else if (m === 'list') {
|
|
33
|
-
params = [session.project.product_tag];
|
|
34
|
-
} else if (Object.keys(body).length > 0) {
|
|
35
|
-
params = [
|
|
36
|
-
{
|
|
37
|
-
product: session.project.product_tag,
|
|
38
|
-
env: session.project.env_slug,
|
|
39
|
-
...body,
|
|
40
|
-
},
|
|
41
|
-
];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result = await proxy.execute(m, params, dbContext);
|
|
45
|
-
|
|
46
|
-
if (m === 'connect' && params[0] && typeof params[0] === 'object') {
|
|
47
|
-
const cfg = params[0] as { database: string; env: string; product: string };
|
|
48
|
-
setDatabaseContext(session.project.workspace_id, session.project.product_tag, session.project.env_slug, {
|
|
49
|
-
database: String(cfg.database),
|
|
50
|
-
env: String(cfg.env),
|
|
51
|
-
product: String(cfg.product),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
printJson(result, Boolean(opts.json));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function runDbContext(json: boolean): void {
|
|
59
|
-
const session = requireSession();
|
|
60
|
-
const ctx = getDbContext(session);
|
|
61
|
-
const runtime = loadRuntimeContext(session.project.workspace_id, session.project.product_tag);
|
|
62
|
-
printJson({ database_context: runtime?.database_context ?? ctx }, json);
|
|
63
|
-
}
|
package/src/commands/generate.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { requireSession } from '../lib/proxy/context.js';
|
|
3
|
-
import { generateExecutablePayload } from '../lib/integrations.js';
|
|
4
|
-
import {
|
|
5
|
-
buildSnippet,
|
|
6
|
-
ensureSupportedSnippetOperation,
|
|
7
|
-
} from '../lib/snippets.js';
|
|
8
|
-
import { printJson } from '../lib/output.js';
|
|
9
|
-
|
|
10
|
-
export async function runGeneratePayload(opts: {
|
|
11
|
-
family: string;
|
|
12
|
-
method: string;
|
|
13
|
-
file?: string;
|
|
14
|
-
json?: boolean;
|
|
15
|
-
}): Promise<void> {
|
|
16
|
-
const session = requireSession();
|
|
17
|
-
let targets: Record<string, unknown> | undefined;
|
|
18
|
-
if (opts.file) {
|
|
19
|
-
targets = JSON.parse(fs.readFileSync(opts.file, 'utf8')) as Record<string, unknown>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const result = await generateExecutablePayload(session.apiUrl, session.credentials, {
|
|
23
|
-
workspace_id: session.project.workspace_id,
|
|
24
|
-
user_id: session.credentials.user_id,
|
|
25
|
-
public_key: session.credentials.public_key,
|
|
26
|
-
product_tag: session.project.product_tag,
|
|
27
|
-
env_slug: session.project.env_slug,
|
|
28
|
-
operation_family: opts.family,
|
|
29
|
-
method: opts.method,
|
|
30
|
-
targets,
|
|
31
|
-
include_session: true,
|
|
32
|
-
include_cache: false,
|
|
33
|
-
schema_mode: 'best_effort',
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
printJson(result, Boolean(opts.json));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function runGenerateSnippet(opts: {
|
|
40
|
-
family: string;
|
|
41
|
-
method: string;
|
|
42
|
-
language: 'typescript' | 'python' | 'go' | 'dotnet';
|
|
43
|
-
file?: string;
|
|
44
|
-
json?: boolean;
|
|
45
|
-
out?: string;
|
|
46
|
-
}): Promise<void> {
|
|
47
|
-
ensureSupportedSnippetOperation(opts.family, opts.method);
|
|
48
|
-
const session = requireSession();
|
|
49
|
-
let targets: Record<string, unknown> | undefined;
|
|
50
|
-
if (opts.file) {
|
|
51
|
-
targets = JSON.parse(fs.readFileSync(opts.file, 'utf8')) as Record<string, unknown>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const generated = await generateExecutablePayload(session.apiUrl, session.credentials, {
|
|
55
|
-
workspace_id: session.project.workspace_id,
|
|
56
|
-
user_id: session.credentials.user_id,
|
|
57
|
-
public_key: session.credentials.public_key,
|
|
58
|
-
product_tag: session.project.product_tag,
|
|
59
|
-
env_slug: session.project.env_slug,
|
|
60
|
-
operation_family: opts.family,
|
|
61
|
-
method: opts.method,
|
|
62
|
-
targets,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const payload = generated.payload ?? {};
|
|
66
|
-
const snippet = buildSnippet(opts.language, payload, opts.family, opts.method);
|
|
67
|
-
const output = { payload: generated, snippet };
|
|
68
|
-
|
|
69
|
-
if (opts.out) {
|
|
70
|
-
fs.writeFileSync(opts.out, snippet);
|
|
71
|
-
console.log(`Wrote snippet to ${opts.out}`);
|
|
72
|
-
}
|
|
73
|
-
printJson(output, Boolean(opts.json));
|
|
74
|
-
}
|
package/src/commands/graph.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { readJsonBody } from '../lib/read-body.js';
|
|
2
|
-
import { setGraphContext } from '../lib/context-store.js';
|
|
3
|
-
import { GRAPH_PROXY_METHODS } from '../lib/db-methods.js';
|
|
4
|
-
import { getGraphProxy, requireSession } from '../lib/proxy/context.js';
|
|
5
|
-
import { printJson } from '../lib/output.js';
|
|
6
|
-
|
|
7
|
-
export async function runGraph(
|
|
8
|
-
method: string,
|
|
9
|
-
opts: { file?: string; json?: boolean },
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
const session = requireSession();
|
|
12
|
-
const proxy = getGraphProxy(session);
|
|
13
|
-
const body = readJsonBody(opts.file) as Record<string, unknown>;
|
|
14
|
-
|
|
15
|
-
const m = method.includes('.') ? method : method.toLowerCase();
|
|
16
|
-
if (!GRAPH_PROXY_METHODS.includes(m as (typeof GRAPH_PROXY_METHODS)[number]) && !m.includes('.')) {
|
|
17
|
-
console.warn(`Method "${m}" not in known list; sending anyway. Known: ${GRAPH_PROXY_METHODS.join(', ')}`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const params: unknown[] = [];
|
|
21
|
-
if (m === 'connect') {
|
|
22
|
-
const cfg = {
|
|
23
|
-
product: session.project.product_tag,
|
|
24
|
-
env: session.project.env_slug,
|
|
25
|
-
graph: body.graph ?? body.tag,
|
|
26
|
-
...body,
|
|
27
|
-
};
|
|
28
|
-
params.push(cfg);
|
|
29
|
-
} else if (Object.keys(body).length > 0) {
|
|
30
|
-
params.push({
|
|
31
|
-
product: session.project.product_tag,
|
|
32
|
-
env: session.project.env_slug,
|
|
33
|
-
...body,
|
|
34
|
-
});
|
|
35
|
-
} else if (m === 'fetchAll' || m === 'list') {
|
|
36
|
-
params.push(session.project.product_tag);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const result = await proxy.execute(m === 'list' ? 'fetchAll' : m, params);
|
|
40
|
-
|
|
41
|
-
if (m === 'connect' && params[0] && typeof params[0] === 'object') {
|
|
42
|
-
const cfg = params[0] as { graph: string; env: string; product: string };
|
|
43
|
-
setGraphContext(session.project.workspace_id, session.project.product_tag, session.project.env_slug, {
|
|
44
|
-
graph: String(cfg.graph),
|
|
45
|
-
env: String(cfg.env),
|
|
46
|
-
product: String(cfg.product),
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
printJson(result, Boolean(opts.json));
|
|
51
|
-
}
|
package/src/commands/init.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { saveProjectConfig, type ProjectConfig } from '../lib/config.js';
|
|
2
|
-
import { detectLanguage, writeInitArtifacts } from '../lib/templates.js';
|
|
3
|
-
import { success } from '../lib/output.js';
|
|
4
|
-
import { runLink } from './link.js';
|
|
5
|
-
|
|
6
|
-
export async function runInit(opts: {
|
|
7
|
-
link?: boolean;
|
|
8
|
-
workspace?: string;
|
|
9
|
-
product?: string;
|
|
10
|
-
env?: string;
|
|
11
|
-
language?: string;
|
|
12
|
-
}): Promise<void> {
|
|
13
|
-
const dir = process.cwd();
|
|
14
|
-
const lang =
|
|
15
|
-
opts.language && ['typescript', 'python', 'go', 'dotnet'].includes(opts.language)
|
|
16
|
-
? (opts.language as 'typescript' | 'python' | 'go' | 'dotnet')
|
|
17
|
-
: detectLanguage(dir);
|
|
18
|
-
|
|
19
|
-
writeInitArtifacts(dir, lang);
|
|
20
|
-
|
|
21
|
-
if (!opts.link) {
|
|
22
|
-
const stub: ProjectConfig = {
|
|
23
|
-
workspace_tag: '',
|
|
24
|
-
product_tag: '',
|
|
25
|
-
env_slug: opts.env ?? 'dev',
|
|
26
|
-
};
|
|
27
|
-
saveProjectConfig(dir, stub);
|
|
28
|
-
success(`Initialized Ductape (${lang}). Run \`ductape link\` to connect workspace/product.`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await runLink({
|
|
33
|
-
workspace: opts.workspace,
|
|
34
|
-
product: opts.product,
|
|
35
|
-
env: opts.env,
|
|
36
|
-
dir,
|
|
37
|
-
});
|
|
38
|
-
success(`Initialized and linked Ductape project (${lang})`);
|
|
39
|
-
}
|