@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.
- package/.env.ductape.example +5 -0
- package/DB_MIGRATE_PLAN.md +223 -0
- package/dist/commands/apply.d.ts +6 -0
- package/dist/commands/apply.js +71 -0
- package/dist/commands/db-migrate.d.ts +19 -0
- package/dist/commands/db-migrate.js +154 -0
- package/dist/commands/db-schema.d.ts +6 -0
- package/dist/commands/db-schema.js +135 -0
- package/dist/commands/install.d.ts +1 -1
- package/dist/commands/install.js +47 -23
- package/dist/commands/platform.js +1 -1
- package/dist/index.js +68 -14
- package/dist/lib/apply-loaders.d.ts +6 -0
- package/dist/lib/apply-loaders.js +24 -0
- package/dist/lib/config.d.ts +2 -1
- package/dist/lib/config.js +9 -4
- package/dist/lib/db-types.d.ts +71 -0
- package/dist/lib/db-types.js +1 -0
- package/dist/lib/migration-files.d.ts +4 -0
- package/dist/lib/migration-files.js +43 -0
- package/dist/lib/platform-api.js +15 -1
- package/dist/lib/resources.js +3 -1
- package/dist/lib/schema-loader.d.ts +30 -0
- package/dist/lib/schema-loader.js +117 -0
- package/dist/lib/templates.js +75 -1
- package/ductape.example.ts +18 -0
- package/package.json +1 -1
- package/src/commands/apply.ts +105 -0
- package/src/commands/db-migrate.ts +211 -0
- package/src/commands/db-schema.ts +163 -0
- package/src/commands/install.ts +52 -24
- package/src/commands/platform.ts +1 -1
- package/src/index.ts +78 -16
- package/src/lib/apply-loaders.ts +30 -0
- package/src/lib/config.ts +9 -4
- package/src/lib/db-types.ts +104 -0
- package/src/lib/migration-files.ts +58 -0
- package/src/lib/platform-api.ts +15 -1
- package/src/lib/resources.ts +3 -1
- package/src/lib/schema-loader.ts +152 -0
- package/src/lib/templates.ts +80 -1
- package/templates/api-gateway.conf +178 -0
- package/templates/docker-compose.release.yml +265 -0
- package/templates/platform.env +16 -0
package/dist/commands/install.js
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { HUB_PLATFORM_DIR } from '../lib/config.js';
|
|
5
6
|
import { success } from '../lib/output.js';
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '../../templates');
|
|
9
|
+
const HUB_IMAGES = [
|
|
10
|
+
'ductape/platform-api:latest',
|
|
11
|
+
'ductape/platform-workbench:latest',
|
|
12
|
+
];
|
|
6
13
|
function checkCommand(cmd, args) {
|
|
7
14
|
const r = spawnSync(cmd, args, { stdio: 'ignore' });
|
|
8
15
|
return r.status === 0;
|
|
9
16
|
}
|
|
10
|
-
|
|
17
|
+
function pull(image) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
console.log(`Pulling ${image} …`);
|
|
20
|
+
const child = spawn('docker', ['pull', image], {
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
shell: process.platform === 'win32',
|
|
23
|
+
});
|
|
24
|
+
child.on('error', reject);
|
|
25
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function copyTemplate(name, dest, overwrite = false) {
|
|
29
|
+
const src = path.join(TEMPLATES_DIR, name);
|
|
30
|
+
if (!overwrite && fs.existsSync(dest))
|
|
31
|
+
return;
|
|
32
|
+
fs.copyFileSync(src, dest);
|
|
33
|
+
}
|
|
34
|
+
export async function runInstall() {
|
|
11
35
|
const issues = [];
|
|
12
36
|
if (!checkCommand('docker', ['--version'])) {
|
|
13
37
|
issues.push('Docker is not installed or not on PATH');
|
|
@@ -15,28 +39,28 @@ export function runInstall() {
|
|
|
15
39
|
else if (!checkCommand('docker', ['compose', 'version'])) {
|
|
16
40
|
issues.push('Docker Compose v2 is required (docker compose)');
|
|
17
41
|
}
|
|
18
|
-
if (!checkCommand('node', ['--version'])) {
|
|
19
|
-
issues.push('Node.js 18+ is recommended for running the CLI');
|
|
20
|
-
}
|
|
21
|
-
let platformDir;
|
|
22
|
-
try {
|
|
23
|
-
platformDir = resolvePlatformDir();
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
issues.push(e instanceof Error ? e.message : String(e));
|
|
27
|
-
platformDir = '';
|
|
28
|
-
}
|
|
29
|
-
if (platformDir) {
|
|
30
|
-
const envExample = path.join(platformDir, '.env.example');
|
|
31
|
-
const envFile = path.join(platformDir, '.env');
|
|
32
|
-
if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
|
|
33
|
-
fs.copyFileSync(envExample, envFile);
|
|
34
|
-
success(`Created ${envFile} from .env.example`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
42
|
if (issues.length > 0) {
|
|
38
43
|
console.error('Install check failed:\n' + issues.map((i) => ` - ${i}`).join('\n'));
|
|
39
44
|
process.exit(1);
|
|
40
45
|
}
|
|
41
|
-
|
|
46
|
+
// Pull platform images from Docker Hub
|
|
47
|
+
for (const image of HUB_IMAGES) {
|
|
48
|
+
const code = await pull(image);
|
|
49
|
+
if (code !== 0) {
|
|
50
|
+
console.error(`Failed to pull ${image} (exit ${code}). Check your internet connection or run \`docker login\`.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Write platform files to ~/.ductape/platform/
|
|
55
|
+
fs.mkdirSync(HUB_PLATFORM_DIR, { recursive: true });
|
|
56
|
+
copyTemplate('docker-compose.release.yml', path.join(HUB_PLATFORM_DIR, 'docker-compose.yml'), true);
|
|
57
|
+
copyTemplate('api-gateway.conf', path.join(HUB_PLATFORM_DIR, 'api-gateway.conf'), true);
|
|
58
|
+
copyTemplate('platform.env', path.join(HUB_PLATFORM_DIR, '.env'), false); // don't overwrite existing .env
|
|
59
|
+
success(`Platform installed to ${HUB_PLATFORM_DIR}`);
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(' Edit ~/.ductape/platform/.env to add your AWS credentials (needed for cloud connections).');
|
|
62
|
+
console.log(' Then run: ductape start');
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(' Workbench: http://localhost:4310');
|
|
65
|
+
console.log(' API: http://localhost:4311');
|
|
42
66
|
}
|
package/dist/index.js
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 { runWorkspacesCurrent, runWorkspacesList, runWorkspacesRefresh, runWorkspacesUse, } from './commands/workspaces.js';
|
|
@@ -23,6 +25,7 @@ import { runCompletion } from './commands/completion.js';
|
|
|
23
25
|
import { runProducts, runProductApps } from './commands/products.js';
|
|
24
26
|
import { runApps } from './commands/apps.js';
|
|
25
27
|
import { runAppsImport } from './commands/apps-import.js';
|
|
28
|
+
import { runApply } from './commands/apply.js';
|
|
26
29
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
28
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -140,6 +143,18 @@ program
|
|
|
140
143
|
env: opts.env,
|
|
141
144
|
language: opts.language,
|
|
142
145
|
})));
|
|
146
|
+
const APPLY_TYPES = ['sessions', 'notifications', 'events'];
|
|
147
|
+
program
|
|
148
|
+
.command('apply')
|
|
149
|
+
.description('Sync declared sessions, notifications and events to your product')
|
|
150
|
+
.argument('[type]', `${APPLY_TYPES.join(' | ')}`)
|
|
151
|
+
.option('--dry-run', 'Print what would be created/updated without applying')
|
|
152
|
+
.action(wrap((type, opts) => {
|
|
153
|
+
if (type && !APPLY_TYPES.includes(type)) {
|
|
154
|
+
throw new Error(`Unknown type "${type}". Use: ${APPLY_TYPES.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
return runApply(type, { dryRun: Boolean(opts.dryRun) });
|
|
157
|
+
}));
|
|
143
158
|
program
|
|
144
159
|
.command('start')
|
|
145
160
|
.description('Start platform via docker compose')
|
|
@@ -155,10 +170,8 @@ program.command('stop').action(wrap(runStop));
|
|
|
155
170
|
program.command('status').option('--json', 'JSON output').action(wrap((opts) => runStatus(Boolean(opts.json))));
|
|
156
171
|
const products = program
|
|
157
172
|
.command('products')
|
|
158
|
-
.description('Workspace products CRUD (REST + sdk-proxy update)')
|
|
159
|
-
|
|
160
|
-
.command('<verb>')
|
|
161
|
-
.description('list | get | create | update | delete')
|
|
173
|
+
.description('Workspace products CRUD (REST + sdk-proxy update)')
|
|
174
|
+
.argument('[verb]', 'list | get | create | update | delete')
|
|
162
175
|
.option('--profile <name>')
|
|
163
176
|
.option('--status <status>', 'Filter for list: all, active, draft, …', 'all')
|
|
164
177
|
.option('--id <id>', 'Product _id (get, delete)')
|
|
@@ -167,7 +180,11 @@ products
|
|
|
167
180
|
.option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
|
|
168
181
|
.option('--no-interactive', 'Require -f JSON for create/update')
|
|
169
182
|
.option('--json', 'JSON output')
|
|
170
|
-
.action(wrap((verb, opts) =>
|
|
183
|
+
.action(wrap((verb, opts) => {
|
|
184
|
+
if (!verb)
|
|
185
|
+
throw new Error('Specify a verb: list | get | create | update | delete');
|
|
186
|
+
return runProducts(verb, opts, []);
|
|
187
|
+
}));
|
|
171
188
|
const productApps = products.command('apps').description('Apps connected to a product');
|
|
172
189
|
productApps
|
|
173
190
|
.command('list')
|
|
@@ -175,10 +192,10 @@ productApps
|
|
|
175
192
|
.option('--product <id>', 'Product _id')
|
|
176
193
|
.option('--json', 'JSON output')
|
|
177
194
|
.action(wrap((opts) => runProductApps('list', opts, [])));
|
|
178
|
-
const apps = program
|
|
179
|
-
apps
|
|
180
|
-
.
|
|
181
|
-
.
|
|
195
|
+
const apps = program
|
|
196
|
+
.command('apps')
|
|
197
|
+
.description('Workspace integration apps CRUD (REST)')
|
|
198
|
+
.argument('[verb]', 'list | get | create | update | delete')
|
|
182
199
|
.option('--profile <name>')
|
|
183
200
|
.option('--status <status>', 'Filter for list', 'all')
|
|
184
201
|
.option('--id <id>', 'App _id')
|
|
@@ -188,7 +205,11 @@ apps
|
|
|
188
205
|
.option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
|
|
189
206
|
.option('--no-interactive', 'Require -f JSON for create/update')
|
|
190
207
|
.option('--json', 'JSON output')
|
|
191
|
-
.action(wrap((verb, opts) =>
|
|
208
|
+
.action(wrap((verb, opts) => {
|
|
209
|
+
if (!verb)
|
|
210
|
+
throw new Error('Specify a verb: list | get | create | update | delete');
|
|
211
|
+
return runApps(verb, opts, []);
|
|
212
|
+
}));
|
|
192
213
|
apps
|
|
193
214
|
.command('import <file>')
|
|
194
215
|
.description('Import Postman v2.1 or OpenAPI 3.0 into a new or existing app')
|
|
@@ -211,7 +232,8 @@ apps
|
|
|
211
232
|
const resources = program.command('resources').description('Component CRUD (sdk-proxy)');
|
|
212
233
|
resources.command('types').option('--json', 'JSON output').action(wrap((opts) => runResourcesList(Boolean(opts.json))));
|
|
213
234
|
resources
|
|
214
|
-
.
|
|
235
|
+
.argument('<type>', 'Resource type (e.g. storage, database, cache …)')
|
|
236
|
+
.argument('<verb>', 'list | get | create | update | delete | connect')
|
|
215
237
|
.option('-t, --tag <tag>')
|
|
216
238
|
.option('-f, --file <path>', 'JSON body (or use interactive prompts)')
|
|
217
239
|
.option('-i, --interactive', 'Prompt for fields (default in TTY when -f omitted)')
|
|
@@ -231,14 +253,46 @@ cloud
|
|
|
231
253
|
.action(wrap((verb, opts) => runCloud('resources', verb, { file: opts.file, json: opts.json }, [])));
|
|
232
254
|
const db = program.command('db').description('Database runtime (db-proxy)');
|
|
233
255
|
db.command('context').option('--json', 'JSON output').action(wrap((opts) => runDbContext(Boolean(opts.json))));
|
|
256
|
+
const dbMigrate = db
|
|
257
|
+
.command('migrate')
|
|
258
|
+
.description('Run pending migrations from ductape/database/migrations/')
|
|
259
|
+
.option('--env <tag>', 'Override environment slug')
|
|
260
|
+
.option('--db <tag>', 'Limit to one db entry from schema.json')
|
|
261
|
+
.option('--dry-run', 'Print what would run without applying')
|
|
262
|
+
.action(wrap((opts) => runDbMigrate({ env: opts.env, db: opts.db, dryRun: Boolean(opts.dryRun) })));
|
|
263
|
+
dbMigrate
|
|
264
|
+
.command('status')
|
|
265
|
+
.description('Show applied vs pending migrations')
|
|
266
|
+
.option('--env <tag>', 'Override environment slug')
|
|
267
|
+
.option('--db <tag>', 'Limit to one db entry')
|
|
268
|
+
.option('--json', 'JSON output')
|
|
269
|
+
.action(wrap((opts) => runDbMigrateStatus({ env: opts.env, db: opts.db, json: Boolean(opts.json) })));
|
|
270
|
+
dbMigrate
|
|
271
|
+
.command('rollback')
|
|
272
|
+
.description('Roll back the last N migrations')
|
|
273
|
+
.option('--env <tag>', 'Override environment slug')
|
|
274
|
+
.option('--db <tag>', 'Limit to one db entry')
|
|
275
|
+
.option('-n <count>', 'Number of migrations to roll back', '1')
|
|
276
|
+
.action(wrap((opts) => runDbMigrateRollback({ env: opts.env, db: opts.db, n: Number(opts.n) })));
|
|
277
|
+
const dbSchema = db.command('schema').description('Schema management for ductape/database/schema.json');
|
|
278
|
+
dbSchema
|
|
279
|
+
.command('generate')
|
|
280
|
+
.description('Diff schema.json vs applied migrations and write new migration files')
|
|
281
|
+
.option('--db <tag>', 'Limit to one db entry from schema.json')
|
|
282
|
+
.option('--destructive', 'Also generate drop migrations for removed fields/tables')
|
|
283
|
+
.action(wrap((opts) => runDbSchemaGenerate({ db: opts.db, destructive: Boolean(opts.destructive) })));
|
|
234
284
|
db
|
|
235
|
-
.
|
|
285
|
+
.argument('[verb]', 'connect | query | …')
|
|
236
286
|
.option('-f, --file <path>')
|
|
237
287
|
.option('--json', 'JSON output')
|
|
238
|
-
.action(wrap((verb, opts) =>
|
|
288
|
+
.action(wrap((verb, opts) => {
|
|
289
|
+
if (!verb)
|
|
290
|
+
throw new Error('Specify a verb: connect | query');
|
|
291
|
+
return runDb(verb, { file: opts.file, json: opts.json });
|
|
292
|
+
}));
|
|
239
293
|
const graph = program.command('graph').description('Graph runtime (graph-proxy)');
|
|
240
294
|
graph
|
|
241
|
-
.
|
|
295
|
+
.argument('<verb>', 'connect | query | …')
|
|
242
296
|
.option('-f, --file <path>')
|
|
243
297
|
.option('--json', 'JSON output')
|
|
244
298
|
.action(wrap((verb, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ApplyItem = Record<string, unknown> & {
|
|
2
|
+
tag: string;
|
|
3
|
+
};
|
|
4
|
+
export declare function loadSessions(projectDir: string): ApplyItem[] | null;
|
|
5
|
+
export declare function loadNotifications(projectDir: string): ApplyItem[] | null;
|
|
6
|
+
export declare function loadEvents(projectDir: string): ApplyItem[] | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PROJECT_CONFIG_DIR } from './config.js';
|
|
4
|
+
function loadArrayFile(filePath) {
|
|
5
|
+
if (!fs.existsSync(filePath))
|
|
6
|
+
return null;
|
|
7
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
8
|
+
if (!Array.isArray(raw)) {
|
|
9
|
+
throw new Error(`${path.basename(filePath)} must be a JSON array.`);
|
|
10
|
+
}
|
|
11
|
+
return raw;
|
|
12
|
+
}
|
|
13
|
+
function declarationPath(projectDir, filename) {
|
|
14
|
+
return path.join(projectDir, PROJECT_CONFIG_DIR, filename);
|
|
15
|
+
}
|
|
16
|
+
export function loadSessions(projectDir) {
|
|
17
|
+
return loadArrayFile(declarationPath(projectDir, 'sessions.json'));
|
|
18
|
+
}
|
|
19
|
+
export function loadNotifications(projectDir) {
|
|
20
|
+
return loadArrayFile(declarationPath(projectDir, 'notifications.json'));
|
|
21
|
+
}
|
|
22
|
+
export function loadEvents(projectDir) {
|
|
23
|
+
return loadArrayFile(declarationPath(projectDir, 'events.json'));
|
|
24
|
+
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export declare const GLOBAL_DIR: string;
|
|
2
2
|
export declare const CREDENTIALS_PATH: string;
|
|
3
3
|
export declare const GLOBAL_CONFIG_PATH: string;
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const HUB_PLATFORM_DIR: string;
|
|
5
|
+
export declare const PROJECT_CONFIG_DIR = "ductape";
|
|
5
6
|
export declare const PROJECT_CONFIG_PATH: string;
|
|
6
7
|
export interface Credentials {
|
|
7
8
|
user_id: string;
|
package/dist/lib/config.js
CHANGED
|
@@ -6,7 +6,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
6
6
|
export const GLOBAL_DIR = path.join(os.homedir(), '.ductape');
|
|
7
7
|
export const CREDENTIALS_PATH = path.join(GLOBAL_DIR, 'credentials.json');
|
|
8
8
|
export const GLOBAL_CONFIG_PATH = path.join(GLOBAL_DIR, 'config.json');
|
|
9
|
-
export const
|
|
9
|
+
export const HUB_PLATFORM_DIR = path.join(GLOBAL_DIR, 'platform');
|
|
10
|
+
export const PROJECT_CONFIG_DIR = 'ductape';
|
|
10
11
|
export const PROJECT_CONFIG_PATH = path.join(PROJECT_CONFIG_DIR, 'config.json');
|
|
11
12
|
const DEFAULT_GLOBAL = {
|
|
12
13
|
default_profile: 'cloud',
|
|
@@ -133,7 +134,7 @@ export function loadProjectConfig() {
|
|
|
133
134
|
const cfg = { ...found.config };
|
|
134
135
|
const workspace_id = resolveWorkspaceId(cfg, creds);
|
|
135
136
|
if (!workspace_id) {
|
|
136
|
-
throw new Error('No workspace resolved. Set workspace_tag in
|
|
137
|
+
throw new Error('No workspace resolved. Set workspace_tag in ductape/config.json or run `ductape workspaces use`.');
|
|
137
138
|
}
|
|
138
139
|
return { ...cfg, workspace_id };
|
|
139
140
|
}
|
|
@@ -146,10 +147,14 @@ export function resolvePlatformDir() {
|
|
|
146
147
|
if (process.env.DUCTAPE_PLATFORM_DIR) {
|
|
147
148
|
return path.resolve(process.env.DUCTAPE_PLATFORM_DIR);
|
|
148
149
|
}
|
|
149
|
-
//
|
|
150
|
+
// Hub install: ~/.ductape/platform/ (written by `ductape install`)
|
|
151
|
+
if (fs.existsSync(path.join(HUB_PLATFORM_DIR, 'docker-compose.yml'))) {
|
|
152
|
+
return HUB_PLATFORM_DIR;
|
|
153
|
+
}
|
|
154
|
+
// Dev/monorepo: cli/ -> repo root -> platform/
|
|
150
155
|
const fromPackage = path.resolve(__dirname, '../../../platform');
|
|
151
156
|
if (fs.existsSync(path.join(fromPackage, 'docker-compose.yml'))) {
|
|
152
157
|
return fromPackage;
|
|
153
158
|
}
|
|
154
|
-
throw new Error('Platform
|
|
159
|
+
throw new Error('Platform not installed. Run `ductape install` to pull the platform from Docker Hub.');
|
|
155
160
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type FieldType = 'string' | 'text' | 'integer' | 'bigint' | 'smallint' | 'decimal' | 'float' | 'double' | 'uuid' | 'boolean' | 'date' | 'time' | 'datetime' | 'timestamp' | 'binary' | 'blob' | 'json' | 'object' | 'array' | 'enum';
|
|
2
|
+
export interface IFieldDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
type: FieldType;
|
|
5
|
+
nullable?: boolean;
|
|
6
|
+
unique?: boolean;
|
|
7
|
+
primaryKey?: boolean;
|
|
8
|
+
autoGenerate?: boolean;
|
|
9
|
+
defaultValue?: unknown;
|
|
10
|
+
maxLength?: number;
|
|
11
|
+
precision?: number;
|
|
12
|
+
scale?: number;
|
|
13
|
+
enumValues?: string[];
|
|
14
|
+
arrayElementType?: FieldType;
|
|
15
|
+
}
|
|
16
|
+
export interface ICreateCollectionOp {
|
|
17
|
+
type: 'createCollection';
|
|
18
|
+
name: string;
|
|
19
|
+
fields: IFieldDefinition[];
|
|
20
|
+
ifNotExists?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface IDropCollectionOp {
|
|
23
|
+
type: 'dropCollection';
|
|
24
|
+
name: string;
|
|
25
|
+
ifExists?: boolean;
|
|
26
|
+
cascade?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface IAddFieldOp {
|
|
29
|
+
type: 'addField';
|
|
30
|
+
collection: string;
|
|
31
|
+
field: IFieldDefinition;
|
|
32
|
+
}
|
|
33
|
+
export interface IDropFieldOp {
|
|
34
|
+
type: 'dropField';
|
|
35
|
+
collection: string;
|
|
36
|
+
fieldName: string;
|
|
37
|
+
cascade?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface ICreateIndexOp {
|
|
40
|
+
type: 'createIndex';
|
|
41
|
+
collection: string;
|
|
42
|
+
name: string;
|
|
43
|
+
fields: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
order?: 'asc' | 'desc';
|
|
46
|
+
}>;
|
|
47
|
+
unique?: boolean;
|
|
48
|
+
ifNotExists?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface IDropIndexOp {
|
|
51
|
+
type: 'dropIndex';
|
|
52
|
+
collection: string;
|
|
53
|
+
name: string;
|
|
54
|
+
ifExists?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export type IMigrationOperation = ICreateCollectionOp | IDropCollectionOp | IAddFieldOp | IDropFieldOp | ICreateIndexOp | IDropIndexOp;
|
|
57
|
+
export interface IMigration {
|
|
58
|
+
tag: string;
|
|
59
|
+
name: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
up: IMigrationOperation[];
|
|
62
|
+
down: IMigrationOperation[];
|
|
63
|
+
dependencies?: string[];
|
|
64
|
+
createdAt?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface IMigrationHistory {
|
|
67
|
+
tag: string;
|
|
68
|
+
name: string;
|
|
69
|
+
checksum?: string;
|
|
70
|
+
appliedAt?: string;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IMigration } from './db-types.js';
|
|
2
|
+
export declare function getMigrationsDir(projectDir: string, dbTag: string): string;
|
|
3
|
+
export declare function loadMigrationFiles(projectDir: string, dbTag: string): IMigration[];
|
|
4
|
+
export declare function writeMigrationFile(projectDir: string, dbTag: string, migration: IMigration, counter: number): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PROJECT_CONFIG_DIR } from './config.js';
|
|
4
|
+
export function getMigrationsDir(projectDir, dbTag) {
|
|
5
|
+
return path.join(projectDir, PROJECT_CONFIG_DIR, 'database', 'migrations', dbTag);
|
|
6
|
+
}
|
|
7
|
+
export function loadMigrationFiles(projectDir, dbTag) {
|
|
8
|
+
const dir = getMigrationsDir(projectDir, dbTag);
|
|
9
|
+
if (!fs.existsSync(dir))
|
|
10
|
+
return [];
|
|
11
|
+
const files = fs
|
|
12
|
+
.readdirSync(dir)
|
|
13
|
+
.filter((f) => f.endsWith('.json'))
|
|
14
|
+
.sort();
|
|
15
|
+
return files.map((file) => {
|
|
16
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
17
|
+
return raw;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function makeTimestamp(counter) {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
23
|
+
const ts = String(now.getFullYear()) +
|
|
24
|
+
pad(now.getMonth() + 1) +
|
|
25
|
+
pad(now.getDate()) +
|
|
26
|
+
pad(now.getHours()) +
|
|
27
|
+
pad(now.getMinutes()) +
|
|
28
|
+
pad(now.getSeconds());
|
|
29
|
+
return `${ts}_${pad(counter, 3)}`;
|
|
30
|
+
}
|
|
31
|
+
export function writeMigrationFile(projectDir, dbTag, migration, counter) {
|
|
32
|
+
const dir = getMigrationsDir(projectDir, dbTag);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
+
const safeName = migration.name
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
37
|
+
.replace(/^_|_$/g, '');
|
|
38
|
+
const ts = makeTimestamp(counter);
|
|
39
|
+
const filename = `${ts}_${safeName}.json`;
|
|
40
|
+
const filePath = path.join(dir, filename);
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify(migration, null, 2) + '\n');
|
|
42
|
+
return filePath;
|
|
43
|
+
}
|
package/dist/lib/platform-api.js
CHANGED
|
@@ -28,9 +28,23 @@ function unwrapOne(result) {
|
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
// —— Products (integrations REST + sdk-proxy for update) ——
|
|
31
|
+
const PRODUCT_LIST_ARRAY_FIELDS = [
|
|
32
|
+
'apps', 'caches', 'quota', 'fallback', 'storage', 'brokers',
|
|
33
|
+
'sessions', 'messageBrokers', 'functions', 'variables', 'auths',
|
|
34
|
+
'databases', 'graphs', 'jobs', 'healthchecks', 'notifications',
|
|
35
|
+
'features', 'vectors', 'agents', 'models', 'steps_data',
|
|
36
|
+
];
|
|
37
|
+
function summarizeProduct(p) {
|
|
38
|
+
if (!p || typeof p !== 'object')
|
|
39
|
+
return p;
|
|
40
|
+
const out = { ...p };
|
|
41
|
+
for (const field of PRODUCT_LIST_ARRAY_FIELDS)
|
|
42
|
+
delete out[field];
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
31
45
|
export async function listProducts(ctx, status = 'all') {
|
|
32
46
|
const result = await client(ctx).getPath(`/integrations/v1/workspace/${ctx.workspaceId}/${status}`, workspaceAuthQuery(ctx));
|
|
33
|
-
return unwrapList(result);
|
|
47
|
+
return unwrapList(result).map(summarizeProduct);
|
|
34
48
|
}
|
|
35
49
|
export async function getProduct(ctx, opts) {
|
|
36
50
|
const q = workspaceAuthQuery(ctx);
|
package/dist/lib/resources.js
CHANGED
|
@@ -17,6 +17,8 @@ export const RESOURCE_MODULES = {
|
|
|
17
17
|
actions: 'actions',
|
|
18
18
|
notification: 'notifications',
|
|
19
19
|
notifications: 'notifications',
|
|
20
|
+
event: 'messageBrokers',
|
|
21
|
+
events: 'messageBrokers',
|
|
20
22
|
broker: 'messageBrokers',
|
|
21
23
|
brokers: 'messageBrokers',
|
|
22
24
|
'message-brokers': 'messageBrokers',
|
|
@@ -135,7 +137,7 @@ export function listResourceTypes() {
|
|
|
135
137
|
'features',
|
|
136
138
|
'actions',
|
|
137
139
|
'notifications',
|
|
138
|
-
'
|
|
140
|
+
'events',
|
|
139
141
|
'sessions',
|
|
140
142
|
'secrets',
|
|
141
143
|
'quotas',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IFieldDefinition } from './db-types.js';
|
|
2
|
+
export interface MongooseFieldDef {
|
|
3
|
+
type?: string | string[];
|
|
4
|
+
required?: boolean;
|
|
5
|
+
unique?: boolean;
|
|
6
|
+
default?: unknown;
|
|
7
|
+
maxlength?: number;
|
|
8
|
+
enum?: string[];
|
|
9
|
+
primaryKey?: boolean;
|
|
10
|
+
autoGenerate?: boolean;
|
|
11
|
+
float?: boolean;
|
|
12
|
+
ref?: string;
|
|
13
|
+
index?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SchemaEntry {
|
|
16
|
+
db: string;
|
|
17
|
+
tables: Record<string, Record<string, MongooseFieldDef>>;
|
|
18
|
+
}
|
|
19
|
+
export declare function getSchemaPath(projectDir: string): string;
|
|
20
|
+
export declare function loadSchemaFile(projectDir: string): SchemaEntry[];
|
|
21
|
+
export declare function parseField(name: string, def: MongooseFieldDef): IFieldDefinition;
|
|
22
|
+
export interface ParsedTable {
|
|
23
|
+
name: string;
|
|
24
|
+
fields: IFieldDefinition[];
|
|
25
|
+
indexFields: Array<{
|
|
26
|
+
fieldName: string;
|
|
27
|
+
unique: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseTableDef(tableName: string, fieldDefs: Record<string, MongooseFieldDef>): ParsedTable;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { PROJECT_CONFIG_DIR } from './config.js';
|
|
4
|
+
export function getSchemaPath(projectDir) {
|
|
5
|
+
return path.join(projectDir, PROJECT_CONFIG_DIR, 'database', 'schema.json');
|
|
6
|
+
}
|
|
7
|
+
export function loadSchemaFile(projectDir) {
|
|
8
|
+
const schemaPath = getSchemaPath(projectDir);
|
|
9
|
+
if (!fs.existsSync(schemaPath)) {
|
|
10
|
+
throw new Error(`No schema.json found at ${schemaPath}. Run \`ductape init\` to scaffold it.`);
|
|
11
|
+
}
|
|
12
|
+
const raw = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
|
|
13
|
+
if (!Array.isArray(raw)) {
|
|
14
|
+
throw new Error('schema.json must be a JSON array of { db, tables } objects.');
|
|
15
|
+
}
|
|
16
|
+
return raw;
|
|
17
|
+
}
|
|
18
|
+
const TYPE_MAP = {
|
|
19
|
+
string: 'string',
|
|
20
|
+
String: 'string',
|
|
21
|
+
text: 'text',
|
|
22
|
+
Text: 'text',
|
|
23
|
+
number: 'integer',
|
|
24
|
+
Number: 'integer',
|
|
25
|
+
integer: 'integer',
|
|
26
|
+
Integer: 'integer',
|
|
27
|
+
int: 'integer',
|
|
28
|
+
Int: 'integer',
|
|
29
|
+
boolean: 'boolean',
|
|
30
|
+
Boolean: 'boolean',
|
|
31
|
+
bool: 'boolean',
|
|
32
|
+
Bool: 'boolean',
|
|
33
|
+
date: 'datetime',
|
|
34
|
+
Date: 'datetime',
|
|
35
|
+
datetime: 'datetime',
|
|
36
|
+
DateTime: 'datetime',
|
|
37
|
+
timestamp: 'timestamp',
|
|
38
|
+
Timestamp: 'timestamp',
|
|
39
|
+
buffer: 'binary',
|
|
40
|
+
Buffer: 'binary',
|
|
41
|
+
binary: 'binary',
|
|
42
|
+
Binary: 'binary',
|
|
43
|
+
mixed: 'object',
|
|
44
|
+
Mixed: 'object',
|
|
45
|
+
object: 'object',
|
|
46
|
+
Object: 'object',
|
|
47
|
+
array: 'array',
|
|
48
|
+
Array: 'array',
|
|
49
|
+
objectid: 'uuid',
|
|
50
|
+
ObjectId: 'uuid',
|
|
51
|
+
uuid: 'uuid',
|
|
52
|
+
UUID: 'uuid',
|
|
53
|
+
decimal128: 'decimal',
|
|
54
|
+
Decimal128: 'decimal',
|
|
55
|
+
decimal: 'decimal',
|
|
56
|
+
Decimal: 'decimal',
|
|
57
|
+
map: 'object',
|
|
58
|
+
Map: 'object',
|
|
59
|
+
bigint: 'bigint',
|
|
60
|
+
BigInt: 'bigint',
|
|
61
|
+
float: 'float',
|
|
62
|
+
Float: 'float',
|
|
63
|
+
double: 'double',
|
|
64
|
+
Double: 'double',
|
|
65
|
+
json: 'json',
|
|
66
|
+
JSON: 'json',
|
|
67
|
+
blob: 'blob',
|
|
68
|
+
Blob: 'blob',
|
|
69
|
+
smallint: 'smallint',
|
|
70
|
+
SmallInt: 'smallint',
|
|
71
|
+
time: 'time',
|
|
72
|
+
Time: 'time',
|
|
73
|
+
};
|
|
74
|
+
function resolveBaseType(raw, def) {
|
|
75
|
+
const t = Array.isArray(raw) ? raw[0] : (raw ?? 'String');
|
|
76
|
+
if (def.enum?.length)
|
|
77
|
+
return 'enum';
|
|
78
|
+
const mapped = TYPE_MAP[t];
|
|
79
|
+
if (!mapped)
|
|
80
|
+
return 'string';
|
|
81
|
+
if (mapped === 'integer' && def.float)
|
|
82
|
+
return 'float';
|
|
83
|
+
return mapped;
|
|
84
|
+
}
|
|
85
|
+
export function parseField(name, def) {
|
|
86
|
+
const type = resolveBaseType(def.type, def);
|
|
87
|
+
const field = { name, type };
|
|
88
|
+
if (def.required === true)
|
|
89
|
+
field.nullable = false;
|
|
90
|
+
if (def.unique === true)
|
|
91
|
+
field.unique = true;
|
|
92
|
+
if (def.primaryKey === true)
|
|
93
|
+
field.primaryKey = true;
|
|
94
|
+
if (def.autoGenerate === true)
|
|
95
|
+
field.autoGenerate = true;
|
|
96
|
+
if (def.maxlength != null)
|
|
97
|
+
field.maxLength = def.maxlength;
|
|
98
|
+
if (def.default !== undefined) {
|
|
99
|
+
field.defaultValue =
|
|
100
|
+
def.default === 'now' || def.default === 'NOW' ? 'CURRENT_TIMESTAMP' : def.default;
|
|
101
|
+
}
|
|
102
|
+
if (type === 'enum' && def.enum?.length) {
|
|
103
|
+
field.enumValues = def.enum;
|
|
104
|
+
}
|
|
105
|
+
return field;
|
|
106
|
+
}
|
|
107
|
+
export function parseTableDef(tableName, fieldDefs) {
|
|
108
|
+
const fields = [];
|
|
109
|
+
const indexFields = [];
|
|
110
|
+
for (const [name, def] of Object.entries(fieldDefs)) {
|
|
111
|
+
fields.push(parseField(name, def));
|
|
112
|
+
if (def.index === true) {
|
|
113
|
+
indexFields.push({ fieldName: name, unique: def.unique === true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { name: tableName, fields, indexFields };
|
|
117
|
+
}
|