@carthooks/arcubase-cli 0.1.2 → 0.1.4

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.
Files changed (106) hide show
  1. package/bundle/arcubase-admin.mjs +1238 -16
  2. package/bundle/arcubase.mjs +1238 -16
  3. package/dist/bin/arcubase-admin.js +0 -0
  4. package/dist/bin/arcubase.js +0 -0
  5. package/dist/runtime/command_registry.d.ts +8 -0
  6. package/dist/runtime/command_registry.d.ts.map +1 -1
  7. package/dist/runtime/command_registry.js +35 -0
  8. package/dist/runtime/entity_save_schema.d.ts +15 -0
  9. package/dist/runtime/entity_save_schema.d.ts.map +1 -0
  10. package/dist/runtime/entity_save_schema.js +641 -0
  11. package/dist/runtime/errors.d.ts +11 -0
  12. package/dist/runtime/errors.d.ts.map +1 -1
  13. package/dist/runtime/execute.d.ts.map +1 -1
  14. package/dist/runtime/execute.js +294 -12
  15. package/dist/runtime/upload.d.ts +13 -0
  16. package/dist/runtime/upload.d.ts.map +1 -0
  17. package/dist/runtime/upload.js +148 -0
  18. package/dist/runtime/zod_registry.d.ts +6 -0
  19. package/dist/runtime/zod_registry.d.ts.map +1 -1
  20. package/dist/runtime/zod_registry.js +136 -0
  21. package/dist/tests/command_registry.test.js +5 -0
  22. package/dist/tests/entity_save_schema.test.d.ts +2 -0
  23. package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
  24. package/dist/tests/entity_save_schema.test.js +221 -0
  25. package/dist/tests/execute_validation.test.js +603 -0
  26. package/dist/tests/help.test.js +49 -0
  27. package/dist/tests/upload.test.d.ts +2 -0
  28. package/dist/tests/upload.test.d.ts.map +1 -0
  29. package/dist/tests/upload.test.js +107 -0
  30. package/package.json +1 -1
  31. package/sdk-dist/docs/runtime-reference/README.md +187 -0
  32. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
  33. package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
  34. package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
  35. package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
  37. package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
  40. package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
  41. package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
  42. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +53 -0
  43. package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
  44. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +52 -0
  45. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
  46. package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
  47. package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
  48. package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
  49. package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
  50. package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
  51. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
  52. package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
  53. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
  54. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
  55. package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
  56. package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
  57. package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
  58. package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
  59. package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
  60. package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
  61. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
  62. package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
  63. package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
  64. package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
  65. package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
  66. package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
  67. package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
  68. package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
  69. package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
  70. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +183 -0
  71. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
  72. package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
  73. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
  74. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
  75. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
  76. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
  77. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
  78. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
  79. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
  80. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
  81. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
  82. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
  83. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
  84. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
  85. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
  86. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
  87. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
  88. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
  89. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
  90. package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
  91. package/sdk-dist/docs/runtime-reference/row-crud.md +188 -0
  92. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
  93. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +135 -0
  94. package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
  95. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  96. package/src/runtime/command_registry.ts +47 -0
  97. package/src/runtime/entity_save_schema.ts +708 -0
  98. package/src/runtime/errors.ts +12 -0
  99. package/src/runtime/execute.ts +367 -12
  100. package/src/runtime/upload.ts +196 -0
  101. package/src/runtime/zod_registry.ts +156 -0
  102. package/src/tests/command_registry.test.ts +6 -0
  103. package/src/tests/entity_save_schema.test.ts +236 -0
  104. package/src/tests/execute_validation.test.ts +781 -0
  105. package/src/tests/help.test.ts +60 -0
  106. package/src/tests/upload.test.ts +133 -0
@@ -2,42 +2,82 @@ import { parseCLIArgs, hasFlag, flagValue } from './argv.js';
2
2
  import { loadRuntimeEnv } from './env.js';
3
3
  import { CLIError } from './errors.js';
4
4
  import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http.js';
5
- import { listModules, listModuleCommands, findCommand } from './command_registry.js';
5
+ import { listModules, listModuleCommands, findCommand, findCommandSuggestions } from './command_registry.js';
6
6
  import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js';
7
- import { getBodySchema, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js';
7
+ import { getBodySchema, getCommandDocHints, getDocHints, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js';
8
+ import { renderUploadHelp, uploadLocalFile } from './upload.js';
9
+ function summarizeUploadResult(value) {
10
+ return value;
11
+ }
8
12
  export function renderRootHelp(scope) {
9
13
  const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase';
14
+ const items = listModules(scope).map((item) => ` - ${item}`);
10
15
  return [
11
16
  `${binary} <module> <command> [flags]`,
12
17
  '',
18
+ ...(scope === 'admin'
19
+ ? ['use arcubase-admin for app, entity shell, and schema management', '']
20
+ : ['use arcubase for row CRUD, search, selection actions, and bulk actions', '']),
13
21
  'modules:',
14
- ...listModules(scope).map((item) => ` - ${item}`),
22
+ ...items,
23
+ ...(scope === 'user' ? ['', 'special commands:', ' - upload <local-file>'] : []),
15
24
  ].join('\n');
16
25
  }
17
26
  export function renderModuleHelp(scope, moduleName) {
18
27
  const items = listModuleCommands(scope, moduleName);
19
28
  if (items.length === 0) {
20
- throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2);
29
+ const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`);
30
+ throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined);
21
31
  }
22
32
  return [
23
33
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
24
34
  '',
35
+ ...(scope === 'admin'
36
+ ? ['use arcubase-admin for app, entity shell, and schema management', '']
37
+ : ['use arcubase for row CRUD, search, selection actions, and bulk actions', '']),
25
38
  'commands:',
26
39
  ...items.map((item) => ` - ${item.commandPath[1]}`),
27
40
  ].join('\n');
28
41
  }
42
+ function buildUnknownCommandDetails(scope, moduleName, commandName) {
43
+ const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`);
44
+ if (scope === 'admin' && moduleName === 'workflow' && suggestions.some((item) => item.startsWith('arcubase workflow '))) {
45
+ return {
46
+ suggestions,
47
+ reason: `${moduleName} ${commandName} is a row operation, not an arcubase-admin command`,
48
+ hint: `use: arcubase ${moduleName} ${commandName}`,
49
+ };
50
+ }
51
+ return suggestions.length > 0 ? { suggestions } : undefined;
52
+ }
29
53
  export function renderCommandHelp(scope, moduleName, commandName) {
30
54
  const command = findCommand(scope, moduleName, commandName);
31
55
  if (!command) {
32
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2);
56
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
33
57
  }
58
+ const docHints = [
59
+ ...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
60
+ ...(command.requestType ? getDocHints(command.requestType) : []),
61
+ ].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index);
34
62
  return [
35
63
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
36
64
  `method: ${command.method}`,
37
65
  `endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
38
66
  command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
67
+ 'query flags: --query-file',
39
68
  command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
40
69
  command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
70
+ ...(scope === 'admin' && moduleName === 'entity' && commandName === 'admin-save-entity'
71
+ ? [
72
+ 'next step:',
73
+ ' - switch to arcubase for row operations',
74
+ ' - row insert: arcubase workflow insert-entity --app-id <app_id> --entity-id <entity_id> --body-file insert.json',
75
+ ' - row query: arcubase workflow query-entity --app-id <app_id> --entity-id <entity_id> --body-file query.json',
76
+ ]
77
+ : []),
78
+ ...(docHints.length > 0
79
+ ? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
80
+ : []),
41
81
  ].join('\n');
42
82
  }
43
83
  function coerceScalar(value, typeText) {
@@ -84,10 +124,194 @@ function buildScalarQuery(command, flags) {
84
124
  }
85
125
  return Object.keys(out).length > 0 ? out : undefined;
86
126
  }
127
+ function isRecord(value) {
128
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
129
+ }
130
+ function extractEntityFields(payload) {
131
+ const candidate = isRecord(payload) && isRecord(payload.data) ? payload.data : payload;
132
+ if (!isRecord(candidate) || !Array.isArray(candidate.fields)) {
133
+ return [];
134
+ }
135
+ return candidate.fields
136
+ .filter((item) => isRecord(item))
137
+ .filter((item) => typeof item.id === 'number' && typeof item.type === 'string')
138
+ .map((item) => ({
139
+ id: item.id,
140
+ type: item.type,
141
+ label: typeof item.label === 'string' ? item.label : undefined,
142
+ }));
143
+ }
144
+ async function fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl) {
145
+ const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
146
+ method: 'GET',
147
+ headers: buildRequestHeaders(runtimeEnv),
148
+ });
149
+ const raw = await response.text();
150
+ let parsedResponse = raw;
151
+ try {
152
+ parsedResponse = raw ? JSON.parse(raw) : null;
153
+ }
154
+ catch {
155
+ parsedResponse = raw;
156
+ }
157
+ if (!response.ok) {
158
+ throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1);
159
+ }
160
+ if (isRecord(parsedResponse) && parsedResponse.error) {
161
+ const upstreamError = parsedResponse.error;
162
+ const upstreamMessage = isRecord(upstreamError) && typeof upstreamError.message === 'string'
163
+ ? upstreamError.message
164
+ : typeof upstreamError === 'string'
165
+ ? upstreamError
166
+ : JSON.stringify(upstreamError);
167
+ throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
168
+ endpoint: `/entity/${appId}/${entityId}`,
169
+ method: 'GET',
170
+ upstreamError,
171
+ });
172
+ }
173
+ const fields = extractEntityFields(parsedResponse);
174
+ return new Map(fields.map((field) => [field.id, field]));
175
+ }
176
+ function looksLikeLocalFilePath(value) {
177
+ return (value.startsWith('./') ||
178
+ value.startsWith('../') ||
179
+ value.startsWith('/') ||
180
+ value.startsWith('~/') ||
181
+ /\.(pdf|png|jpe?g|gif|webp|bmp|svg|heic|txt|csv|xlsx?|docx?|pptx?|zip)$/i.test(value));
182
+ }
183
+ function isValidUploadEntry(value) {
184
+ if (!isRecord(value)) {
185
+ return false;
186
+ }
187
+ const hasUploadId = typeof value.upload_id === 'number' || typeof value.upload_id === 'string';
188
+ const hasAssetsId = typeof value.assets_id === 'number' || typeof value.assets_id === 'string';
189
+ return (hasUploadId || hasAssetsId) && typeof value.file === 'string' && typeof value.file_name === 'string';
190
+ }
191
+ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body, fetchImpl) {
192
+ if (!isRecord(body)) {
193
+ return;
194
+ }
195
+ const container = command.functionName === 'updateEntityRow' ? body.data : body.fields;
196
+ if (!isRecord(container)) {
197
+ return;
198
+ }
199
+ const appId = flagValue(flags, 'app-id');
200
+ const entityId = flagValue(flags, 'entity-id');
201
+ if (!appId || !entityId) {
202
+ return;
203
+ }
204
+ const fieldsById = await fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl);
205
+ for (const [fieldIdText, value] of Object.entries(container)) {
206
+ if (!/^\d+$/.test(fieldIdText)) {
207
+ continue;
208
+ }
209
+ const fieldId = Number(fieldIdText);
210
+ const field = fieldsById.get(fieldId);
211
+ if (!field || (field.type !== 'file' && field.type !== 'image')) {
212
+ continue;
213
+ }
214
+ const fieldPath = command.functionName === 'updateEntityRow' ? `body.data.${fieldId}` : `body.fields.${fieldId}`;
215
+ const fieldName = field.label ? `${field.label} (${field.type})` : `${field.type} field ${fieldId}`;
216
+ if (typeof value === 'string') {
217
+ const guidance = looksLikeLocalFilePath(value)
218
+ ? `do not put a local file path directly into ${fieldName}; run "arcubase upload ${value}" first and use the returned data array as the field value`
219
+ : `${fieldName} must be an array returned by "arcubase upload <local-file>", not a string`;
220
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
221
+ operation: `${scope}.${command.module}.${command.functionName}`,
222
+ requestType: command.requestType ?? undefined,
223
+ issues: [{ path: fieldPath, message: guidance }],
224
+ docHints: [
225
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
226
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
227
+ ],
228
+ });
229
+ }
230
+ if (isRecord(value)) {
231
+ const guidance = `${fieldName} must be an array. Run "arcubase upload <local-file>" and use the returned data array directly as the field value`;
232
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
233
+ operation: `${scope}.${command.module}.${command.functionName}`,
234
+ requestType: command.requestType ?? undefined,
235
+ issues: [{ path: fieldPath, message: guidance }],
236
+ docHints: [
237
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
238
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
239
+ ],
240
+ });
241
+ }
242
+ if (!Array.isArray(value)) {
243
+ const guidance = `${fieldName} must be an array returned by "arcubase upload <local-file>"`;
244
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
245
+ operation: `${scope}.${command.module}.${command.functionName}`,
246
+ requestType: command.requestType ?? undefined,
247
+ issues: [{ path: fieldPath, message: guidance }],
248
+ docHints: [
249
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
250
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
251
+ ],
252
+ });
253
+ }
254
+ for (const [index, item] of value.entries()) {
255
+ if (!isValidUploadEntry(item)) {
256
+ const guidance = `${fieldName} entries must include upload_id or assets_id plus file and file_name. Use "arcubase upload <local-file>" and copy the returned array item as-is`;
257
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
258
+ operation: `${scope}.${command.module}.${command.functionName}`,
259
+ requestType: command.requestType ?? undefined,
260
+ issues: [{ path: `${fieldPath}.${index}`, message: guidance }],
261
+ docHints: [
262
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
263
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
264
+ ],
265
+ });
266
+ }
267
+ }
268
+ }
269
+ }
270
+ function throwBodyValidationFailure(message, requestType, path, scope, command) {
271
+ throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
272
+ operation: `${scope}.${command.module}.${command.functionName}`,
273
+ requestType,
274
+ issues: [
275
+ {
276
+ path,
277
+ message,
278
+ },
279
+ ],
280
+ tsHints: getTypeHints(requestType),
281
+ docHints: getDocHints(requestType),
282
+ });
283
+ }
284
+ function validateActionSpecificBody(scope, command, flags, body) {
285
+ if (!isRecord(body) || !command.requestType) {
286
+ return;
287
+ }
288
+ if (command.module === 'workflow' && command.functionName === 'queryEntitySelection') {
289
+ const action = flagValue(flags, 'action');
290
+ const selection = body.selection;
291
+ if (!isRecord(selection) || typeof selection.type !== 'string') {
292
+ return;
293
+ }
294
+ const type = selection.type;
295
+ const condition = isRecord(selection.condition) ? selection.condition : undefined;
296
+ const conditionKeys = condition ? Object.keys(condition).filter((key) => key !== 'selectAll') : [];
297
+ if (action === 'query') {
298
+ if (type === 'condition') {
299
+ throwBodyValidationFailure('selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use workflow query-entity for search conditions', command.requestType, 'body.selection.type', scope, command);
300
+ }
301
+ return;
302
+ }
303
+ if ((action === 'archive' || action === 'delete' || action === 'batch_print') && type === 'all') {
304
+ throwBodyValidationFailure(`selection.type=all is not supported for action=${action}; use ids or condition`, command.requestType, 'body.selection.type', scope, command);
305
+ }
306
+ if ((action === 'archive' || action === 'delete') && type === 'condition' && conditionKeys.length === 0) {
307
+ throwBodyValidationFailure(`selection.condition for action=${action} requires at least one q, tab, or filter_* key in addition to selectAll`, command.requestType, 'body.selection.condition', scope, command);
308
+ }
309
+ }
310
+ }
87
311
  export function summarizeCommand(scope, moduleName, commandName) {
88
312
  const command = findCommand(scope, moduleName, commandName);
89
313
  if (!command) {
90
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2);
314
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
91
315
  }
92
316
  return {
93
317
  scope,
@@ -99,8 +323,28 @@ export function summarizeCommand(scope, moduleName, commandName) {
99
323
  scalarFlags: command.scalarParams.map((item) => item.name.replace(/([A-Z])/g, '-$1').toLowerCase()),
100
324
  };
101
325
  }
102
- export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetchImpl = fetch) {
326
+ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
103
327
  const parsed = parseCLIArgs(argv);
328
+ if (scope === 'user' && parsed.commandTokens[0] === 'upload') {
329
+ if (hasFlag(parsed.flags, 'help') || parsed.commandTokens.length === 1) {
330
+ return { kind: 'help', text: renderUploadHelp() };
331
+ }
332
+ const sourcePath = parsed.commandTokens[1];
333
+ if (!sourcePath) {
334
+ throw new CLIError('UPLOAD_SOURCE_REQUIRED', 'upload requires a local file path', 2);
335
+ }
336
+ const runtimeEnv = env ?? loadRuntimeEnv(scope);
337
+ const data = await uploadLocalFile(runtimeEnv, sourcePath, {
338
+ filename: flagValue(parsed.flags, 'filename'),
339
+ global: hasFlag(parsed.flags, 'global'),
340
+ }, fetchImpl);
341
+ return {
342
+ kind: 'result',
343
+ commandPath: ['upload', sourcePath],
344
+ status: 200,
345
+ data,
346
+ };
347
+ }
104
348
  if (parsed.commandTokens.length === 0) {
105
349
  return { kind: 'help', text: renderRootHelp(scope) };
106
350
  }
@@ -114,15 +358,17 @@ export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetch
114
358
  if (hasFlag(parsed.flags, 'help')) {
115
359
  return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) };
116
360
  }
361
+ const runtimeEnv = env ?? loadRuntimeEnv(scope);
117
362
  const command = findCommand(scope, moduleName, commandName);
118
363
  if (!command) {
119
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2);
364
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
120
365
  }
121
366
  const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags));
122
367
  const scalarQuery = buildScalarQuery(command, parsed.flags);
123
368
  const fileQuery = loadQueryFromFlags(parsed.flags);
124
369
  const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) };
125
370
  const body = loadBodyFromFlags(parsed.flags);
371
+ let validatedBody = body;
126
372
  if (command.requestType) {
127
373
  if (body === undefined) {
128
374
  throw new CLIError('MISSING_BODY_FILE', `command requires --body-file for ${command.requestType}`, 2);
@@ -139,8 +385,11 @@ export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetch
139
385
  message: issue.message,
140
386
  })),
141
387
  tsHints: getTypeHints(command.requestType),
388
+ docHints: getDocHints(command.requestType),
142
389
  });
143
390
  }
391
+ validatedBody = parsedBody.data;
392
+ validateActionSpecificBody(scope, command, parsed.flags, validatedBody);
144
393
  }
145
394
  else {
146
395
  const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType);
@@ -150,18 +399,21 @@ export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetch
150
399
  requestType: command.requestType,
151
400
  reason: unsupportedReason,
152
401
  tsHints: getTypeHints(command.requestType),
402
+ docHints: getDocHints(command.requestType),
153
403
  });
154
404
  }
155
405
  }
156
406
  }
157
- const headers = buildRequestHeaders(env);
158
- const response = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
407
+ if (scope === 'user' && (command.functionName === 'insertEntity' || command.functionName === 'updateEntityRow')) {
408
+ await validateFileFieldPayloads(scope, command, runtimeEnv, parsed.flags, validatedBody, fetchImpl);
409
+ }
410
+ const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
159
411
  method: command.method,
160
412
  headers: {
161
- ...headers,
413
+ ...buildRequestHeaders(runtimeEnv),
162
414
  'Content-Type': 'application/json',
163
415
  },
164
- body: body === undefined || command.method === 'GET' ? undefined : JSON.stringify(body),
416
+ body: validatedBody === undefined || command.method === 'GET' ? undefined : JSON.stringify(validatedBody),
165
417
  });
166
418
  const raw = await response.text();
167
419
  let parsedResponse = raw;
@@ -174,6 +426,36 @@ export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetch
174
426
  if (!response.ok) {
175
427
  throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1);
176
428
  }
429
+ if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
430
+ const upstreamError = parsedResponse.error;
431
+ const upstreamMessage = typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
432
+ ? upstreamError.message
433
+ : typeof upstreamError === 'string'
434
+ ? upstreamError
435
+ : JSON.stringify(upstreamError);
436
+ const traceId = typeof parsedResponse.trace_id === 'string'
437
+ ? parsedResponse.trace_id
438
+ : typeof parsedResponse.request_id === 'string'
439
+ ? parsedResponse.request_id
440
+ : undefined;
441
+ const uploadBindFailure = upstreamMessage.includes('failed to bind upload to assets');
442
+ throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
443
+ endpoint,
444
+ method: command.method,
445
+ traceId,
446
+ upstreamError,
447
+ ...(uploadBindFailure
448
+ ? {
449
+ reason: 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record',
450
+ hint: 'verify the upload storage host is reachable from the Arcubase server, then rerun arcubase upload and retry the row insert/update',
451
+ docHints: [
452
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
453
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
454
+ ],
455
+ }
456
+ : {}),
457
+ });
458
+ }
177
459
  return {
178
460
  kind: 'result',
179
461
  commandPath: command.commandPath,
@@ -0,0 +1,13 @@
1
+ import type { RuntimeEnv } from './env.js';
2
+ export type ArcubaseUploadResult = {
3
+ upload_id: number | string;
4
+ file: string;
5
+ file_name: string;
6
+ meta: Record<string, never>;
7
+ };
8
+ export declare function renderUploadHelp(): string;
9
+ export declare function uploadLocalFile(env: RuntimeEnv, sourcePath: string, options: {
10
+ filename?: string;
11
+ global?: boolean;
12
+ }, fetchImpl?: typeof fetch): Promise<ArcubaseUploadResult[]>;
13
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/runtime/upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAuB1C,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;CAC5B,CAAA;AA+CD,wBAAgB,gBAAgB,IAAI,MAAM,CA0BzC;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,EACD,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAiFjC"}
@@ -0,0 +1,148 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { CLIError } from './errors.js';
4
+ import { buildRequestHeaders, buildURL } from './http.js';
5
+ function readUploadSource(sourcePath) {
6
+ const absolutePath = path.resolve(sourcePath);
7
+ if (!fs.existsSync(absolutePath)) {
8
+ throw new CLIError('UPLOAD_SOURCE_NOT_FOUND', `upload source file does not exist: ${sourcePath}`, 2);
9
+ }
10
+ const stat = fs.statSync(absolutePath);
11
+ if (!stat.isFile()) {
12
+ throw new CLIError('UPLOAD_SOURCE_INVALID', `upload source path must be a file: ${sourcePath}`, 2);
13
+ }
14
+ return {
15
+ absolutePath,
16
+ bytes: fs.readFileSync(absolutePath),
17
+ };
18
+ }
19
+ function parseJSONResponse(raw) {
20
+ try {
21
+ return raw ? JSON.parse(raw) : null;
22
+ }
23
+ catch {
24
+ return raw;
25
+ }
26
+ }
27
+ function requireTokenPayload(payload) {
28
+ if (payload && typeof payload === 'object' && payload.error) {
29
+ const upstreamError = payload.error;
30
+ const message = typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
31
+ ? upstreamError.message
32
+ : typeof upstreamError === 'string'
33
+ ? upstreamError
34
+ : JSON.stringify(upstreamError);
35
+ throw new CLIError('UPSTREAM_BODY_ERROR', message, 1, {
36
+ endpoint: '/upload/token',
37
+ method: 'POST',
38
+ traceId: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
39
+ upstreamError,
40
+ });
41
+ }
42
+ if (!payload?.data || typeof payload.data !== 'object') {
43
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response did not include data', 1);
44
+ }
45
+ return payload.data;
46
+ }
47
+ export function renderUploadHelp() {
48
+ return [
49
+ 'arcubase upload <local-file> [flags]',
50
+ '',
51
+ 'purpose:',
52
+ ' - upload one local file through Arcubase',
53
+ ' - return a field value that can be used directly in file or image row payloads',
54
+ '',
55
+ 'flags:',
56
+ ' --filename <name> override the uploaded filename stored in Arcubase',
57
+ ' --global request a global upload token instead of a tenant-scoped token',
58
+ '',
59
+ 'result shape:',
60
+ ' - data is a JSON array',
61
+ ' - use that array directly as the value of a file or image field in insert/update payloads',
62
+ '',
63
+ 'example:',
64
+ ' arcubase upload ./contract.pdf',
65
+ ' arcubase upload ./photo.jpg --filename lead-photo.jpg',
66
+ '',
67
+ 'docs:',
68
+ ' - Uploads: /opt/arcubase-sdk/docs/runtime-reference/uploads.md',
69
+ ' - Row CRUD: /opt/arcubase-sdk/docs/runtime-reference/row-crud.md',
70
+ ' - File field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/file.md',
71
+ ' - Image field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/image.md',
72
+ ].join('\n');
73
+ }
74
+ export async function uploadLocalFile(env, sourcePath, options, fetchImpl = fetch) {
75
+ const { absolutePath, bytes } = readUploadSource(sourcePath);
76
+ const filename = (options.filename && options.filename.trim()) || path.basename(absolutePath);
77
+ const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/upload/token'), {
78
+ method: 'POST',
79
+ headers: {
80
+ ...buildRequestHeaders(env),
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify({
84
+ global: Boolean(options.global),
85
+ }),
86
+ });
87
+ const tokenRaw = await tokenResponse.text();
88
+ const tokenParsed = parseJSONResponse(tokenRaw);
89
+ if (!tokenResponse.ok) {
90
+ throw new CLIError('UPLOAD_TOKEN_REQUEST_FAILED', typeof tokenParsed === 'string' ? tokenParsed : JSON.stringify(tokenParsed), 1, {
91
+ endpoint: '/upload/token',
92
+ method: 'POST',
93
+ });
94
+ }
95
+ const tokenData = requireTokenPayload(tokenParsed);
96
+ if (tokenData.mode !== 'aliyun-oss' && tokenData.mode !== 's3-post-policy') {
97
+ throw new CLIError('UPLOAD_MODE_UNSUPPORTED', `unsupported upload mode: ${String(tokenData.mode || '')}`, 1);
98
+ }
99
+ if (!tokenData.token_data?.dir || !tokenData.token_data.policy || !tokenData.token_data.host) {
100
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1);
101
+ }
102
+ if (tokenData.id === undefined || tokenData.id === null || tokenData.id === '') {
103
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing id', 1);
104
+ }
105
+ const form = new FormData();
106
+ form.append('key', `${tokenData.token_data.dir}${filename}`);
107
+ form.append('policy', tokenData.token_data.policy);
108
+ if (tokenData.mode === 'aliyun-oss') {
109
+ if (!tokenData.token_data.accessid || !tokenData.token_data.signature) {
110
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1);
111
+ }
112
+ form.append('OSSAccessKeyId', tokenData.token_data.accessid);
113
+ form.append('success_action_status', '200');
114
+ form.append('signature', tokenData.token_data.signature);
115
+ }
116
+ else {
117
+ if (!tokenData.token_data.x_amz_algorithm ||
118
+ !tokenData.token_data.x_amz_credential ||
119
+ !tokenData.token_data.x_amz_date ||
120
+ !tokenData.token_data.x_amz_signature) {
121
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required s3 token_data fields', 1);
122
+ }
123
+ form.append('x-amz-algorithm', tokenData.token_data.x_amz_algorithm);
124
+ form.append('x-amz-credential', tokenData.token_data.x_amz_credential);
125
+ form.append('x-amz-date', tokenData.token_data.x_amz_date);
126
+ form.append('x-amz-signature', tokenData.token_data.x_amz_signature);
127
+ }
128
+ form.append('file', new Blob([new Uint8Array(bytes)]), filename);
129
+ const uploadResponse = await fetchImpl(tokenData.token_data.host, {
130
+ method: 'POST',
131
+ body: form,
132
+ });
133
+ const uploadRaw = await uploadResponse.text();
134
+ if (!uploadResponse.ok) {
135
+ throw new CLIError('UPLOAD_TRANSFER_FAILED', uploadRaw || `upload failed with status ${uploadResponse.status}`, 1, {
136
+ endpoint: tokenData.token_data.host,
137
+ method: 'POST',
138
+ });
139
+ }
140
+ return [
141
+ {
142
+ upload_id: tokenData.id,
143
+ file: filename,
144
+ file_name: filename,
145
+ meta: {},
146
+ },
147
+ ];
148
+ }
@@ -6,5 +6,11 @@ export type RuntimeTypeHint = {
6
6
  symbol: string;
7
7
  file: string;
8
8
  };
9
+ export type RuntimeDocHint = {
10
+ title: string;
11
+ file: string;
12
+ };
9
13
  export declare function getTypeHints(typeName: string): RuntimeTypeHint[];
14
+ export declare function getDocHints(typeName: string): RuntimeDocHint[];
15
+ export declare function getCommandDocHints(operation: string): RuntimeDocHint[];
10
16
  //# sourceMappingURL=zod_registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAIrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAIjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBhE"}
1
+ {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AA6BrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAyDD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBhE;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAI9D;AA0DD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAItE"}