@ductape/cli 0.3.20 → 0.3.22

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.
@@ -0,0 +1,8 @@
1
+ export type ComponentActionKind = 'graph' | 'vector';
2
+ export type ComponentActionVerb = 'create' | 'update' | 'get' | 'list' | 'delete' | 'execute';
3
+ export declare function runComponentActions(kind: ComponentActionKind, verb: ComponentActionVerb, opts: {
4
+ resource?: string;
5
+ action?: string;
6
+ file?: string;
7
+ json?: boolean;
8
+ }): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { readJsonBody } from '../lib/read-body.js';
2
+ import { getSdkProxy, requireSession } from '../lib/proxy/context.js';
3
+ import { printJson } from '../lib/output.js';
4
+ export async function runComponentActions(kind, verb, opts) {
5
+ const session = requireSession();
6
+ const body = readJsonBody(opts.file) ?? {};
7
+ const resourceKey = kind === 'graph' ? 'graph' : 'vector';
8
+ const resource = opts.resource ?? String(body[resourceKey] ?? body[`${resourceKey}Tag`] ?? '');
9
+ const action = opts.action ?? String(body.actionTag ?? body.action ?? '');
10
+ const base = { product: session.project.product_tag, ...body };
11
+ let method;
12
+ let params;
13
+ if (verb === 'create') {
14
+ if (!opts.file)
15
+ throw new Error(`${kind} actions create requires --action-file <file.json>`);
16
+ method = kind === 'graph' ? 'action.create' : 'actions.create';
17
+ params = [base];
18
+ }
19
+ else if (verb === 'update') {
20
+ if (!opts.file)
21
+ throw new Error(`${kind} actions update requires --action-file <file.json>`);
22
+ method = kind === 'graph' ? 'action.update' : 'actions.update';
23
+ params = [base];
24
+ }
25
+ else if (verb === 'list') {
26
+ if (!resource)
27
+ throw new Error(`${kind} actions list requires --${kind} <tag>`);
28
+ method = kind === 'graph' ? 'action.fetchAll' : 'actions.fetchAll';
29
+ params = [{ product: session.project.product_tag, [resourceKey]: resource }];
30
+ }
31
+ else if (verb === 'get' || verb === 'delete') {
32
+ if (!resource || !action)
33
+ throw new Error(`${kind} actions ${verb} requires <action> --${kind} <tag>`);
34
+ method = kind === 'graph' ? `action.${verb === 'get' ? 'fetch' : 'delete'}` : `actions.${verb === 'get' ? 'fetch' : 'delete'}`;
35
+ params = [{ product: session.project.product_tag, [resourceKey]: resource, actionTag: action }];
36
+ }
37
+ else {
38
+ if (!opts.file)
39
+ throw new Error(`${kind} actions execute requires --action-file <file.json>`);
40
+ method = kind === 'graph' ? 'action.execute' : 'actions.execute';
41
+ params = [{ product: session.project.product_tag, env: session.project.env_slug, ...body }];
42
+ }
43
+ const result = await getSdkProxy(session).execute(kind, method, params);
44
+ printJson(result, Boolean(opts.json));
45
+ }
@@ -4,11 +4,10 @@ export declare function runDb(method: string, opts: {
4
4
  }): Promise<void>;
5
5
  /**
6
6
  * Database Actions — reusable, parameterized query/mutation definitions saved against a
7
- * database component (databases.action.* in the SDK). This is administrative metadata
8
- * (uses the access-key-backed sdk-proxy, same as `resources <type> create`), not a runtime
9
- * query it never connects to the live database. Required for any Feature step that calls
10
- * ctx.database.query/insert/update/delete, since those take an `event` tag pointing at one
11
- * of these, not a raw {table, where} shape.
7
+ * database component (databases.action.* in the SDK). Definition management is
8
+ * administrative metadata (uses the access-key-backed sdk-proxy, same as
9
+ * `resources <type> create`). Runtime execution uses databases.execute with an `action`
10
+ * tag. Direct query/insert/update/delete calls keep their raw {table, where, ...} shape.
12
11
  *
13
12
  * Tag format is always "product_tag:database_tag:action_tag" — always pass it fully
14
13
  * qualified here (the SDK's 2-part fallback only works inside a single long-lived process
@@ -47,14 +47,29 @@ export async function runDb(method, opts) {
47
47
  }
48
48
  printJson(result, Boolean(opts.json));
49
49
  }
50
- const DB_ACTION_VERBS = ['create', 'update', 'get', 'list', 'delete'];
50
+ const DB_ACTION_VERBS = ['create', 'update', 'get', 'list', 'delete', 'execute', 'dispatch'];
51
+ const DB_ACTION_OPERATIONS = ['query', 'insert', 'update', 'delete', 'aggregate', 'count'];
52
+ function validateDatabaseActionCreateBody(body) {
53
+ const operation = String(body.operation ?? '');
54
+ if (!DB_ACTION_OPERATIONS.includes(operation)) {
55
+ throw new Error(`Invalid database action operation "${operation}". Use one of: ${DB_ACTION_OPERATIONS.join(', ')}. ` +
56
+ 'For MongoDB find/findOne actions use operation "query".');
57
+ }
58
+ const template = body.template;
59
+ if (!template || (typeof template !== 'object' && typeof template !== 'string')) {
60
+ throw new Error('template is required and must be an object for MongoDB or a SQL string.');
61
+ }
62
+ if (JSON.stringify(template).includes('$Input{')) {
63
+ throw new Error('Database action placeholders use {{name}}, not $Input{name}. ' +
64
+ 'Example MongoDB query template: { "where": { "email": "{{email}}" } }.');
65
+ }
66
+ }
51
67
  /**
52
68
  * Database Actions — reusable, parameterized query/mutation definitions saved against a
53
- * database component (databases.action.* in the SDK). This is administrative metadata
54
- * (uses the access-key-backed sdk-proxy, same as `resources <type> create`), not a runtime
55
- * query it never connects to the live database. Required for any Feature step that calls
56
- * ctx.database.query/insert/update/delete, since those take an `event` tag pointing at one
57
- * of these, not a raw {table, where} shape.
69
+ * database component (databases.action.* in the SDK). Definition management is
70
+ * administrative metadata (uses the access-key-backed sdk-proxy, same as
71
+ * `resources <type> create`). Runtime execution uses databases.execute with an `action`
72
+ * tag. Direct query/insert/update/delete calls keep their raw {table, where, ...} shape.
58
73
  *
59
74
  * Tag format is always "product_tag:database_tag:action_tag" — always pass it fully
60
75
  * qualified here (the SDK's 2-part fallback only works inside a single long-lived process
@@ -76,6 +91,7 @@ export async function runDbActions(verb, opts, extraArgs) {
76
91
  throw new Error('create requires a body: { tag: "product:database:action", name, tableName, ' +
77
92
  'operation, template, description?, filterTemplate? } (pass -f <file.json>).');
78
93
  }
94
+ validateDatabaseActionCreateBody(body);
79
95
  method = 'action.create';
80
96
  params = [body];
81
97
  break;
@@ -109,6 +125,22 @@ export async function runDbActions(verb, opts, extraArgs) {
109
125
  method = 'action.delete';
110
126
  params = [tag];
111
127
  break;
128
+ case 'dispatch': {
129
+ const body = readJsonBody(opts.file);
130
+ if (!body || Object.keys(body).length === 0)
131
+ throw new Error('dispatch requires --action-file <file.json>');
132
+ method = 'action.dispatch';
133
+ params = [body];
134
+ break;
135
+ }
136
+ case 'execute': {
137
+ const body = readJsonBody(opts.file);
138
+ if (!body || Object.keys(body).length === 0)
139
+ throw new Error('execute requires --action-file <file.json>');
140
+ method = 'execute';
141
+ params = [body];
142
+ break;
143
+ }
112
144
  }
113
145
  const result = await proxy.execute('databases', method, params);
114
146
  printJson(result, Boolean(opts.json));
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { runStart, runStop, runStatus } from './commands/platform.js';
16
16
  import { runResourceCrud, runResourcesList, runEventTopicCrud, runNotificationMessageCrud } from './commands/resources.js';
17
17
  import { runCloud, runCloudPreflight } from './commands/cloud.js';
18
18
  import { runDb, runDbActions, runDbContext } from './commands/db.js';
19
+ import { runComponentActions } from './commands/component-actions.js';
19
20
  import { runDbMigrate, runDbMigrateStatus, runDbMigrateRollback } from './commands/db-migrate.js';
20
21
  import { runFeaturesSync } from './commands/features-sync.js';
21
22
  import { runDbSchemaGenerate, runDbSchemaPush } from './commands/db-schema.js';
@@ -807,23 +808,23 @@ dbMigrate
807
808
  .action(wrap((opts) => runDbMigrateRollback({ env: opts.env, db: opts.db, n: Number(opts.n) })));
808
809
  const dbActions = db
809
810
  .command('actions')
810
- .description('Reusable database action definitions (saved parameterized queries). Required for any ' +
811
- 'Feature step that uses ctx.database.query/insert/update/delete those take an `event` ' +
812
- 'tag pointing at one of these, not a raw table/where shape. Administrative (access-key), ' +
813
- 'not a runtime query — create/update never touch the live database.');
811
+ .description('Reusable database action definitions (saved parameterized queries). Execute them with ' +
812
+ 'database.execute({ database, action, input }) in Features, databases.execute({ product, ' +
813
+ 'env, database, action, input }) in the SDK, or `db actions execute` in the CLI. Direct ' +
814
+ 'query/insert/update/delete calls keep their raw table/where/data shape.');
814
815
  dbActions
815
816
  .command('create')
816
- .description('Create a database action from a file: { tag: "product:database:action", name, tableName, operation, template }')
817
- .requiredOption('-f, --file <path>', 'JSON file with the action definition')
817
+ .description('Create a saved database action; MongoDB query example uses operation "query" and template { "where": { "email": "{{email}}" } }')
818
+ .requiredOption('--action-file <path>', 'JSON definition with tag, name, description, tableName, operation, and template')
818
819
  .option('--json', 'JSON output')
819
- .action(wrap((opts) => runDbActions('create', { file: opts.file, json: opts.json }, [])));
820
+ .action(wrap((opts) => runDbActions('create', { file: opts.actionFile, json: opts.json }, [])));
820
821
  dbActions
821
822
  .command('update')
822
823
  .description('Update an existing database action')
823
824
  .requiredOption('-t, --tag <tag>', 'Action tag ("product:database:action")')
824
- .requiredOption('-f, --file <path>', 'JSON file with the fields to change')
825
+ .requiredOption('--action-file <path>', 'JSON file with the fields to change')
825
826
  .option('--json', 'JSON output')
826
- .action(wrap((opts) => runDbActions('update', { tag: opts.tag, file: opts.file, json: opts.json }, [])));
827
+ .action(wrap((opts) => runDbActions('update', { tag: opts.tag, file: opts.actionFile, json: opts.json }, [])));
827
828
  dbActions
828
829
  .command('get')
829
830
  .description('Fetch one database action')
@@ -842,6 +843,18 @@ dbActions
842
843
  .argument('<tag>', 'Action tag ("product:database:action")')
843
844
  .option('--json', 'JSON output')
844
845
  .action(wrap((tag, opts) => runDbActions('delete', { tag, json: opts.json }, [])));
846
+ dbActions
847
+ .command('dispatch')
848
+ .description('Dispatch a database action immediately or on a schedule')
849
+ .requiredOption('--action-file <path>', 'JSON body: { product, env, database, action, input, schedule? }')
850
+ .option('--json', 'JSON output')
851
+ .action(wrap((opts) => runDbActions('dispatch', { file: opts.actionFile, json: opts.json }, [])));
852
+ dbActions
853
+ .command('execute')
854
+ .description('Execute a saved database action synchronously')
855
+ .requiredOption('--action-file <path>', 'JSON body: { product, env, database, action, input }')
856
+ .option('--json', 'JSON output')
857
+ .action(wrap((opts) => runDbActions('execute', { file: opts.actionFile, json: opts.json }, [])));
845
858
  const dbSchema = db.command('schema').description('Schema management for ductape/database/schema.json');
846
859
  dbSchema
847
860
  .command('generate')
@@ -873,11 +886,56 @@ features
873
886
  .argument('[filter]', 'Optional substring passed through to the project\'s features:sync script')
874
887
  .action(wrap((filter) => runFeaturesSync({ filter })));
875
888
  const graph = program.command('graph').description('Graph runtime (graph-proxy)');
889
+ const graphActions = graph.command('actions').description('Reusable graph actions');
890
+ for (const verb of ['create', 'update', 'get', 'list', 'delete', 'execute']) {
891
+ const command = graphActions.command(verb);
892
+ if (verb === 'get' || verb === 'delete')
893
+ command.argument('<action>');
894
+ if (verb !== 'create' && verb !== 'update' && verb !== 'execute')
895
+ command.requiredOption('--graph <tag>');
896
+ if (verb === 'create' || verb === 'update' || verb === 'execute')
897
+ command.requiredOption('--action-file <path>');
898
+ command.option('--json').action(wrap((actionOrOpts, maybeOpts) => {
899
+ const hasArgument = verb === 'get' || verb === 'delete';
900
+ const opts = hasArgument ? maybeOpts : actionOrOpts;
901
+ return runComponentActions('graph', verb, {
902
+ action: hasArgument ? actionOrOpts : undefined,
903
+ resource: opts.graph,
904
+ file: opts.actionFile,
905
+ json: opts.json,
906
+ });
907
+ }));
908
+ }
876
909
  graph
877
- .argument('<verb>', 'connect | query | createAction | validateAction | updateAction | …')
910
+ .argument('[verb]', 'connect | query | createAction | validateAction | updateAction | …')
878
911
  .option('-f, --file <path>')
879
912
  .option('--json', 'JSON output')
880
- .action(wrap((verb, opts) => runGraph(verb, { file: opts.file, json: opts.json })));
913
+ .action(wrap((verb, opts) => {
914
+ if (!verb)
915
+ throw new Error('Specify a graph verb or use `graph actions --help`');
916
+ return runGraph(verb, { file: opts.file, json: opts.json });
917
+ }));
918
+ const vector = program.command('vector').description('Vector runtime and reusable actions');
919
+ const vectorActions = vector.command('actions').description('Reusable vector actions');
920
+ for (const verb of ['create', 'update', 'get', 'list', 'delete', 'execute']) {
921
+ const command = vectorActions.command(verb);
922
+ if (verb === 'get' || verb === 'delete')
923
+ command.argument('<action>');
924
+ if (verb !== 'create' && verb !== 'update' && verb !== 'execute')
925
+ command.requiredOption('--vector <tag>');
926
+ if (verb === 'create' || verb === 'update' || verb === 'execute')
927
+ command.requiredOption('--action-file <path>');
928
+ command.option('--json').action(wrap((actionOrOpts, maybeOpts) => {
929
+ const hasArgument = verb === 'get' || verb === 'delete';
930
+ const opts = hasArgument ? maybeOpts : actionOrOpts;
931
+ return runComponentActions('vector', verb, {
932
+ action: hasArgument ? actionOrOpts : undefined,
933
+ resource: opts.vector,
934
+ file: opts.actionFile,
935
+ json: opts.json,
936
+ });
937
+ }));
938
+ }
881
939
  program
882
940
  .command('secrets <verb>')
883
941
  .description('Workspace secrets CRUD')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/cli",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Ductape CLI — local platform, login, link projects, and manage resources via the proxy (Workbench-compatible)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",