@carthooks/arcubase-cli 0.1.10 → 0.1.12
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/bundle/arcubase-admin.mjs +1274 -375
- package/bundle/arcubase.mjs +1274 -375
- package/dist/generated/command_registry.generated.d.ts +36 -35
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +37 -36
- package/dist/generated/type_index.generated.d.ts +23 -23
- package/dist/generated/type_index.generated.js +23 -23
- package/dist/runtime/entity_save_schema.d.ts.map +1 -1
- package/dist/runtime/entity_save_schema.js +30 -0
- package/dist/runtime/env.d.ts +2 -1
- package/dist/runtime/env.d.ts.map +1 -1
- package/dist/runtime/env.js +10 -1
- package/dist/runtime/errors.d.ts +2 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts +4 -2
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +944 -134
- package/dist/runtime/paths.d.ts +1 -1
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +9 -2
- package/dist/runtime/upload.d.ts +1 -11
- package/dist/runtime/upload.d.ts.map +1 -1
- package/dist/runtime/upload.js +19 -133
- package/dist/runtime/zod_registry.d.ts +5 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +108 -48
- package/dist/tests/bootstrap.test.js +12 -0
- package/dist/tests/command_registry.test.js +8 -0
- package/dist/tests/docs_readability.test.d.ts +2 -0
- package/dist/tests/docs_readability.test.d.ts.map +1 -0
- package/dist/tests/docs_readability.test.js +87 -0
- package/dist/tests/entity_save_schema.test.js +22 -0
- package/dist/tests/execute_validation.test.js +381 -44
- package/dist/tests/help.test.js +45 -8
- package/dist/tests/upload.test.js +30 -77
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +43 -29
- package/sdk-dist/docs/runtime-reference/access-rule.md +109 -0
- package/sdk-dist/docs/runtime-reference/condition.md +182 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +6 -5
- package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/file.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/number.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/status.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/text.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema.md +28 -21
- package/sdk-dist/docs/runtime-reference/examples/README.md +1 -1
- package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +30 -11
- package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/create-app.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.bulk-update.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.row.insert.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.selection.condition.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +26 -26
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +26 -10
- package/sdk-dist/docs/runtime-reference/examples/wms-01/create-app.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.bulk-update.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.selection.condition.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +35 -8
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +31 -95
- package/sdk-dist/docs/runtime-reference/selection.md +128 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +113 -30
- package/sdk-dist/docs/runtime-reference/uploads.md +30 -59
- package/sdk-dist/docs/runtime-reference/workflow/README.md +129 -13
- package/sdk-dist/generated/command_registry.generated.ts +37 -36
- package/sdk-dist/generated/type_index.generated.ts +23 -23
- package/src/generated/command_registry.generated.ts +37 -36
- package/src/generated/type_index.generated.ts +23 -23
- package/src/runtime/entity_save_schema.ts +30 -0
- package/src/runtime/env.ts +14 -2
- package/src/runtime/errors.ts +2 -0
- package/src/runtime/execute.ts +1166 -134
- package/src/runtime/paths.ts +11 -2
- package/src/runtime/upload.ts +19 -178
- package/src/runtime/zod_registry.ts +113 -48
- package/src/tests/bootstrap.test.ts +16 -0
- package/src/tests/command_registry.test.ts +9 -0
- package/src/tests/docs_readability.test.ts +96 -0
- package/src/tests/entity_save_schema.test.ts +22 -0
- package/src/tests/execute_validation.test.ts +508 -47
- package/src/tests/help.test.ts +49 -8
- package/src/tests/upload.test.ts +34 -91
package/dist/runtime/execute.js
CHANGED
|
@@ -5,10 +5,7 @@ import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http
|
|
|
5
5
|
import { listModules, listModuleCommands, findCommand, findCommandSuggestions } from './command_registry.js';
|
|
6
6
|
import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js';
|
|
7
7
|
import { getBodySchema, getCommandDocHints, getDocHints, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js';
|
|
8
|
-
import {
|
|
9
|
-
function summarizeUploadResult(value) {
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
8
|
+
import { resolveArcubaseSDKPath } from './paths.js';
|
|
12
9
|
export function renderRootHelp(scope) {
|
|
13
10
|
const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase';
|
|
14
11
|
const items = listModules(scope).map((item) => ` - ${item}`);
|
|
@@ -18,19 +15,20 @@ export function renderRootHelp(scope) {
|
|
|
18
15
|
...(scope === 'admin'
|
|
19
16
|
? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
|
|
20
17
|
: ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
|
|
18
|
+
'prefer --body-json <json-string> for normal payloads, including table schema; use --body-file only when JSON is too large for one tool call',
|
|
19
|
+
'',
|
|
21
20
|
'modules:',
|
|
22
21
|
...items,
|
|
23
|
-
...(scope === 'user' ? ['', 'special commands:', ' - upload <local-file>'] : []),
|
|
24
22
|
].join('\n');
|
|
25
23
|
}
|
|
26
|
-
export function renderModuleHelp(scope, moduleName) {
|
|
24
|
+
export function renderModuleHelp(scope, moduleName, env = process.env) {
|
|
27
25
|
const items = listModuleCommands(scope, moduleName);
|
|
28
26
|
if (items.length === 0) {
|
|
29
27
|
const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`);
|
|
30
28
|
throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined);
|
|
31
29
|
}
|
|
32
30
|
if (items.length === 1 && items[0].commandPath.length === 1) {
|
|
33
|
-
return renderCommandHelp(scope, moduleName);
|
|
31
|
+
return renderCommandHelp(scope, moduleName, undefined, env);
|
|
34
32
|
}
|
|
35
33
|
return [
|
|
36
34
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
|
|
@@ -38,6 +36,8 @@ export function renderModuleHelp(scope, moduleName) {
|
|
|
38
36
|
...(scope === 'admin'
|
|
39
37
|
? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
|
|
40
38
|
: ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
|
|
39
|
+
'prefer --body-json <json-string> for normal payloads, including table schema; use --body-file only when JSON is too large for one tool call',
|
|
40
|
+
'',
|
|
41
41
|
'commands:',
|
|
42
42
|
...items.filter((item) => item.commandPath.length > 1).map((item) => ` - ${item.commandPath[1]}`),
|
|
43
43
|
].join('\n');
|
|
@@ -46,30 +46,52 @@ function buildUnknownCommandDetails(scope, moduleName, commandName) {
|
|
|
46
46
|
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`);
|
|
47
47
|
return suggestions.length > 0 ? { suggestions } : undefined;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
function sdkPath(file, env = process.env) {
|
|
50
|
+
return resolveArcubaseSDKPath(file, env);
|
|
51
|
+
}
|
|
52
|
+
export function renderCommandHelp(scope, moduleName, commandName, env = process.env) {
|
|
50
53
|
const command = findCommand(scope, moduleName, commandName);
|
|
51
54
|
if (!command) {
|
|
52
55
|
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''));
|
|
53
56
|
}
|
|
54
57
|
const docHints = [
|
|
55
|
-
...getCommandDocHints(`${scope}.${command.module}.${command.functionName}
|
|
56
|
-
...(command.requestType ? getDocHints(command.requestType) : []),
|
|
58
|
+
...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`, env),
|
|
59
|
+
...(command.requestType ? getDocHints(command.requestType, env) : []),
|
|
57
60
|
].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index);
|
|
61
|
+
const typeHints = command.requestType ? getTypeHints(command.requestType, env) : [];
|
|
58
62
|
const pathFlags = command.pathParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag}`);
|
|
59
|
-
const queryFlags = command
|
|
63
|
+
const queryFlags = visibleQueryParams(command).map((item) => `--${item.flag}`);
|
|
64
|
+
const isTableCreate = scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create';
|
|
65
|
+
const bodyTypeText = isTableCreate ? 'TableCreateWithSchema' : command.requestType;
|
|
66
|
+
const tableCreateExampleBody = JSON.stringify(buildTableCreateExampleBody());
|
|
60
67
|
return [
|
|
61
68
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')}`,
|
|
62
69
|
`method: ${command.method}`,
|
|
63
70
|
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
64
|
-
command.requestType ? `body: ${
|
|
71
|
+
command.requestType ? `body: ${bodyTypeText} via --body-json <json-string> | --body-file <file>.json` : 'body: none',
|
|
65
72
|
queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
|
|
66
73
|
pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
|
|
74
|
+
...(isTableCreate
|
|
75
|
+
? [
|
|
76
|
+
'body-json example:',
|
|
77
|
+
` - ${tableCreateExampleBody}`,
|
|
78
|
+
'body rules:',
|
|
79
|
+
' - fields must be non-empty; empty table shell creation is not allowed',
|
|
80
|
+
' - field_id_seq must be greater than every field.id',
|
|
81
|
+
' - use text, not string; use member, not user',
|
|
82
|
+
' - for select/radio/checkbox/status, use options.options.items',
|
|
83
|
+
'schema refs:',
|
|
84
|
+
` - Table schema cheatsheet: ${sdkPath('docs/runtime-reference/entity-schema.md', env)}`,
|
|
85
|
+
` - Field type index: ${sdkPath('docs/runtime-reference/entity-schema/README.md', env)}`,
|
|
86
|
+
]
|
|
87
|
+
: []),
|
|
67
88
|
...(scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'update-schema'
|
|
68
89
|
? [
|
|
69
90
|
'next step:',
|
|
91
|
+
' - prefer --body-json for schema save to avoid file creation and JSON file corruption',
|
|
70
92
|
' - switch to arcubase for row operations',
|
|
71
|
-
' - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-file insert.json',
|
|
72
|
-
' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
|
|
93
|
+
' - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-json <json-string> | --body-file insert.json',
|
|
94
|
+
' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-json <json-string> | --body-file query.json',
|
|
73
95
|
]
|
|
74
96
|
: []),
|
|
75
97
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
|
|
@@ -77,13 +99,23 @@ export function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
77
99
|
'body-json examples:',
|
|
78
100
|
' - enable: {"update":["enabled"],"enabled":true}',
|
|
79
101
|
' - rename: {"update":["name"],"name":"Sales read only"}',
|
|
80
|
-
' - update
|
|
102
|
+
' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
|
|
106
|
+
? [
|
|
107
|
+
'body-json example:',
|
|
108
|
+
' - {"name":"Sales read write","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":":1001","read":true,"write":true,"report":false},{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
|
|
109
|
+
'success requires:',
|
|
110
|
+
' - result.enabled must be true',
|
|
111
|
+
' - options.user_scope[].type is "user" for tenant users',
|
|
112
|
+
' - hidden fields use colon field keys such as ":1002"',
|
|
81
113
|
]
|
|
82
114
|
: []),
|
|
83
115
|
...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
|
|
84
116
|
? [
|
|
85
117
|
'body-json example:',
|
|
86
|
-
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
118
|
+
' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
|
|
87
119
|
'success requires:',
|
|
88
120
|
' - result.enabled must be true',
|
|
89
121
|
' - result.options.user_scope must contain the assigned numeric TenantUser.ID values',
|
|
@@ -95,6 +127,9 @@ export function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
95
127
|
...(docHints.length > 0
|
|
96
128
|
? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
|
|
97
129
|
: []),
|
|
130
|
+
...(typeHints.length > 0
|
|
131
|
+
? ['types:', ...typeHints.map((item) => ` - ${item.symbol}: ${item.file}`)]
|
|
132
|
+
: []),
|
|
98
133
|
].join('\n');
|
|
99
134
|
}
|
|
100
135
|
function coerceScalar(value, typeText) {
|
|
@@ -138,7 +173,7 @@ function buildScalarQuery(command, flags) {
|
|
|
138
173
|
return undefined;
|
|
139
174
|
}
|
|
140
175
|
const out = {};
|
|
141
|
-
for (const scalar of command
|
|
176
|
+
for (const scalar of effectiveQueryParams(command)) {
|
|
142
177
|
const value = getFixedValue(scalar) ?? flagValue(flags, scalar.flag);
|
|
143
178
|
if (value === undefined)
|
|
144
179
|
continue;
|
|
@@ -146,9 +181,125 @@ function buildScalarQuery(command, flags) {
|
|
|
146
181
|
}
|
|
147
182
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
148
183
|
}
|
|
184
|
+
function validateKnownFlags(command, flags, env = process.env) {
|
|
185
|
+
const allowed = new Set(['help', 'body-file', 'body-json', 'query-file']);
|
|
186
|
+
for (const mapping of command.pathParams) {
|
|
187
|
+
if (!getFixedValue(mapping)) {
|
|
188
|
+
allowed.add(mapping.flag);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const query of visibleQueryParams(command)) {
|
|
192
|
+
if (!getFixedValue(query)) {
|
|
193
|
+
allowed.add(query.flag);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const flag of Object.keys(flags)) {
|
|
197
|
+
if (!allowed.has(flag)) {
|
|
198
|
+
throw new CLIError('UNKNOWN_FLAG', `unknown flag --${flag} for ${command.commandPath.join(' ')}`, 2, {
|
|
199
|
+
...buildUnknownFlagDetails(command, flag, env),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function buildUnknownFlagDetails(command, flag, env = process.env) {
|
|
205
|
+
const operation = command.commandPath.join(' ');
|
|
206
|
+
const details = {
|
|
207
|
+
operation,
|
|
208
|
+
hint: `run ${operation} --help for the supported flags`,
|
|
209
|
+
};
|
|
210
|
+
if (command.requestType) {
|
|
211
|
+
details.requestType = command.requestType;
|
|
212
|
+
details.tsHints = getTypeHints(command.requestType, env);
|
|
213
|
+
details.docHints = getDocHints(command.requestType, env);
|
|
214
|
+
details.commonMistakes = [
|
|
215
|
+
`do not pass request body fields as --${flag}`,
|
|
216
|
+
'put request body fields inside --body-json <json-string>',
|
|
217
|
+
];
|
|
218
|
+
details.suggestions = [`retry with: ${command.commandPath.join(' ')} ${command.pathParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag} <${item.flag.replace(/-/g, '_')}>`).join(' ')} --body-json '<json>'`.replace(/\s+/g, ' ').trim()];
|
|
219
|
+
}
|
|
220
|
+
if (command.commandPath[0] === 'row' && command.commandPath[1] === 'query' && ['limit', 'offset', 'search', 'sorts', 'view-mode'].includes(flag)) {
|
|
221
|
+
details.shapeHint = { limit: 20, offset: 0, search: { text_search: 'SO-1001' } };
|
|
222
|
+
details.suggestions = ['retry with: row query --app-id <app_id> --table-id <table_id> --body-json \'{"limit":20,"offset":0}\''];
|
|
223
|
+
}
|
|
224
|
+
if (command.commandPath[0] === 'table' && command.commandPath[1] === 'dataset' && ['app-id', 'table-id'].includes(flag)) {
|
|
225
|
+
details.hint = 'table dataset uses --dataset-id and --node-id; use table get for app/table metadata';
|
|
226
|
+
details.suggestions = [
|
|
227
|
+
'retry metadata read with: table get --app-id <app_id> --table-id <table_id>',
|
|
228
|
+
'retry dataset read with: table dataset --dataset-id <dataset_id> --node-id <node_id>',
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
return details;
|
|
232
|
+
}
|
|
233
|
+
function visibleQueryParams(command) {
|
|
234
|
+
if (!command)
|
|
235
|
+
return [];
|
|
236
|
+
return effectiveQueryParams(command).filter((item) => !getFixedValue(item));
|
|
237
|
+
}
|
|
238
|
+
function effectiveQueryParams(command) {
|
|
239
|
+
if (!command)
|
|
240
|
+
return [];
|
|
241
|
+
return command.queryParams.filter((item) => {
|
|
242
|
+
if (rowCommandNeedsPolicy(command) && item.key === 'policy_id') {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
149
248
|
function isRecord(value) {
|
|
150
249
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
151
250
|
}
|
|
251
|
+
function stringifyUpstreamBody(value) {
|
|
252
|
+
if (typeof value === 'string')
|
|
253
|
+
return value;
|
|
254
|
+
if (value == null)
|
|
255
|
+
return '';
|
|
256
|
+
try {
|
|
257
|
+
return JSON.stringify(value);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return String(value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function extractTraceID(value) {
|
|
264
|
+
if (!isRecord(value))
|
|
265
|
+
return undefined;
|
|
266
|
+
if (typeof value.trace_id === 'string')
|
|
267
|
+
return value.trace_id;
|
|
268
|
+
if (typeof value.request_id === 'string')
|
|
269
|
+
return value.request_id;
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
function upstreamErrorMessage(value) {
|
|
273
|
+
if (isRecord(value) && typeof value.message === 'string')
|
|
274
|
+
return value.message;
|
|
275
|
+
if (typeof value === 'string')
|
|
276
|
+
return value;
|
|
277
|
+
return stringifyUpstreamBody(value);
|
|
278
|
+
}
|
|
279
|
+
function throwUpstreamHTTPError(endpoint, method, response, parsedResponse) {
|
|
280
|
+
const upstreamError = isRecord(parsedResponse) && parsedResponse.error ? parsedResponse.error : undefined;
|
|
281
|
+
const body = stringifyUpstreamBody(parsedResponse);
|
|
282
|
+
throw new CLIError('UPSTREAM_REQUEST_FAILED', upstreamError ? upstreamErrorMessage(upstreamError) : body || `HTTP ${response.status}`, 1, {
|
|
283
|
+
endpoint,
|
|
284
|
+
method,
|
|
285
|
+
status: response.status,
|
|
286
|
+
traceId: extractTraceID(parsedResponse),
|
|
287
|
+
responseBody: body,
|
|
288
|
+
...(upstreamError ? { upstreamError } : {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function fetchUpstream(url, init, endpoint, method, fetchImpl) {
|
|
292
|
+
try {
|
|
293
|
+
return await fetchImpl(url, init);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
throw new CLIError('UPSTREAM_NETWORK_ERROR', error instanceof Error ? error.message : String(error), 1, {
|
|
297
|
+
endpoint,
|
|
298
|
+
method,
|
|
299
|
+
reason: 'network or DNS failure before Arcubase returned an HTTP response',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
152
303
|
function extractEntityFields(payload) {
|
|
153
304
|
const candidate = isRecord(payload) && isRecord(payload.data) ? payload.data : payload;
|
|
154
305
|
if (!isRecord(candidate) || !Array.isArray(candidate.fields)) {
|
|
@@ -164,10 +315,12 @@ function extractEntityFields(payload) {
|
|
|
164
315
|
}));
|
|
165
316
|
}
|
|
166
317
|
async function fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl) {
|
|
167
|
-
const
|
|
318
|
+
const endpoint = `/api/entity/${appId}/${entityId}`;
|
|
319
|
+
const encodedEndpoint = `/api/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`;
|
|
320
|
+
const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, encodedEndpoint), {
|
|
168
321
|
method: 'GET',
|
|
169
322
|
headers: buildRequestHeaders(runtimeEnv),
|
|
170
|
-
});
|
|
323
|
+
}, endpoint, 'GET', fetchImpl);
|
|
171
324
|
const raw = await response.text();
|
|
172
325
|
let parsedResponse = raw;
|
|
173
326
|
try {
|
|
@@ -177,18 +330,15 @@ async function fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl) {
|
|
|
177
330
|
parsedResponse = raw;
|
|
178
331
|
}
|
|
179
332
|
if (!response.ok) {
|
|
180
|
-
|
|
333
|
+
throwUpstreamHTTPError(endpoint, 'GET', response, parsedResponse);
|
|
181
334
|
}
|
|
182
335
|
if (isRecord(parsedResponse) && parsedResponse.error) {
|
|
183
336
|
const upstreamError = parsedResponse.error;
|
|
184
|
-
const upstreamMessage =
|
|
185
|
-
? upstreamError.message
|
|
186
|
-
: typeof upstreamError === 'string'
|
|
187
|
-
? upstreamError
|
|
188
|
-
: JSON.stringify(upstreamError);
|
|
337
|
+
const upstreamMessage = upstreamErrorMessage(upstreamError);
|
|
189
338
|
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
190
|
-
endpoint
|
|
339
|
+
endpoint,
|
|
191
340
|
method: 'GET',
|
|
341
|
+
traceId: extractTraceID(parsedResponse),
|
|
192
342
|
upstreamError,
|
|
193
343
|
});
|
|
194
344
|
}
|
|
@@ -237,76 +387,161 @@ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body
|
|
|
237
387
|
const fieldName = field.label ? `${field.label} (${field.type})` : `${field.type} field ${fieldId}`;
|
|
238
388
|
if (typeof value === 'string') {
|
|
239
389
|
const guidance = looksLikeLocalFilePath(value)
|
|
240
|
-
? `do not put a local file path directly into ${fieldName};
|
|
241
|
-
: `${fieldName} must be an
|
|
390
|
+
? `do not put a local file path directly into ${fieldName}; use arcubase upload token flow and pass the returned upload value array`
|
|
391
|
+
: `${fieldName} must be an upload value array, not a string`;
|
|
242
392
|
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
243
393
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
244
394
|
requestType: command.requestType ?? undefined,
|
|
245
395
|
issues: [{ path: fieldPath, message: guidance }],
|
|
246
396
|
docHints: [
|
|
247
|
-
{ title: 'Uploads', file: '
|
|
248
|
-
{ title: 'Row CRUD', file: '
|
|
397
|
+
{ title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
|
|
398
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
249
399
|
],
|
|
250
400
|
});
|
|
251
401
|
}
|
|
252
402
|
if (isRecord(value)) {
|
|
253
|
-
const guidance = `${fieldName} must be an array.
|
|
403
|
+
const guidance = `${fieldName} must be an upload value array. Use the upload token flow and pass the returned array directly`;
|
|
254
404
|
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
255
405
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
256
406
|
requestType: command.requestType ?? undefined,
|
|
257
407
|
issues: [{ path: fieldPath, message: guidance }],
|
|
258
408
|
docHints: [
|
|
259
|
-
{ title: 'Uploads', file: '
|
|
260
|
-
{ title: 'Row CRUD', file: '
|
|
409
|
+
{ title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
|
|
410
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
261
411
|
],
|
|
262
412
|
});
|
|
263
413
|
}
|
|
264
414
|
if (!Array.isArray(value)) {
|
|
265
|
-
const guidance = `${fieldName} must be an
|
|
415
|
+
const guidance = `${fieldName} must be an upload value array`;
|
|
266
416
|
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
267
417
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
268
418
|
requestType: command.requestType ?? undefined,
|
|
269
419
|
issues: [{ path: fieldPath, message: guidance }],
|
|
270
420
|
docHints: [
|
|
271
|
-
{ title: 'Uploads', file: '
|
|
272
|
-
{ title: 'Row CRUD', file: '
|
|
421
|
+
{ title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
|
|
422
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
273
423
|
],
|
|
274
424
|
});
|
|
275
425
|
}
|
|
276
426
|
for (const [index, item] of value.entries()) {
|
|
277
427
|
if (!isValidUploadEntry(item)) {
|
|
278
|
-
const guidance = `${fieldName} entries must include upload_id or assets_id plus file and file_name. Use
|
|
428
|
+
const guidance = `${fieldName} entries must include upload_id or assets_id plus file and file_name. Use the upload token flow and copy the returned array item as-is`;
|
|
279
429
|
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
280
430
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
281
431
|
requestType: command.requestType ?? undefined,
|
|
282
432
|
issues: [{ path: `${fieldPath}.${index}`, message: guidance }],
|
|
283
433
|
docHints: [
|
|
284
|
-
{ title: 'Uploads', file: '
|
|
285
|
-
{ title: 'Row CRUD', file: '
|
|
434
|
+
{ title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
|
|
435
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
286
436
|
],
|
|
287
437
|
});
|
|
288
438
|
}
|
|
289
439
|
}
|
|
290
440
|
}
|
|
291
441
|
}
|
|
442
|
+
function buildTableCreateExampleBody() {
|
|
443
|
+
return {
|
|
444
|
+
name: '投票项目',
|
|
445
|
+
parent: 0,
|
|
446
|
+
schema_version: 0,
|
|
447
|
+
field_id_seq: 1003,
|
|
448
|
+
layout: [[1001], [1002]],
|
|
449
|
+
fields: [
|
|
450
|
+
{
|
|
451
|
+
id: 1001,
|
|
452
|
+
label: '投票名称',
|
|
453
|
+
type: 'text',
|
|
454
|
+
required: true,
|
|
455
|
+
unique: false,
|
|
456
|
+
editable: true,
|
|
457
|
+
visible: true,
|
|
458
|
+
show_label: true,
|
|
459
|
+
scannable: false,
|
|
460
|
+
default_value_mode: 0,
|
|
461
|
+
description: '',
|
|
462
|
+
value: null,
|
|
463
|
+
number_decimal: 0,
|
|
464
|
+
depends: [],
|
|
465
|
+
key: 'vote_name',
|
|
466
|
+
code_index: false,
|
|
467
|
+
options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 },
|
|
468
|
+
transfers: null,
|
|
469
|
+
children: null,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 1002,
|
|
473
|
+
label: '投票描述',
|
|
474
|
+
type: 'textarea',
|
|
475
|
+
required: false,
|
|
476
|
+
unique: false,
|
|
477
|
+
editable: true,
|
|
478
|
+
visible: true,
|
|
479
|
+
show_label: true,
|
|
480
|
+
scannable: false,
|
|
481
|
+
default_value_mode: 0,
|
|
482
|
+
description: '',
|
|
483
|
+
value: null,
|
|
484
|
+
number_decimal: 0,
|
|
485
|
+
depends: [],
|
|
486
|
+
key: 'vote_description',
|
|
487
|
+
code_index: false,
|
|
488
|
+
options: { placeholder: '', html: false, size: 'default', lengthLimit: false, lengthMin: 0, lengthMax: 2000 },
|
|
489
|
+
transfers: null,
|
|
490
|
+
children: null,
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
494
|
+
workflow_enabled: false,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
292
497
|
function isAssignUsersCommand(scope, command) {
|
|
293
498
|
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users';
|
|
294
499
|
}
|
|
295
|
-
function
|
|
500
|
+
function isTableCreateCommand(scope, command) {
|
|
501
|
+
return scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create';
|
|
502
|
+
}
|
|
503
|
+
function isTableSchemaCommand(scope, command) {
|
|
504
|
+
return scope === 'admin' && command.commandPath[0] === 'table' && (command.commandPath[1] === 'create' || command.commandPath[1] === 'update-schema');
|
|
505
|
+
}
|
|
506
|
+
function isAccessRuleCommand(scope, command) {
|
|
507
|
+
return scope === 'admin' && command.commandPath[0] === 'access-rule';
|
|
508
|
+
}
|
|
509
|
+
function isAccessRuleUpdateCommand(scope, command) {
|
|
510
|
+
return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update';
|
|
511
|
+
}
|
|
512
|
+
function buildAccessRuleShapeHint(userId = 2188889901) {
|
|
513
|
+
return {
|
|
514
|
+
name: 'Sales read write',
|
|
515
|
+
enabled: true,
|
|
516
|
+
type: 'form',
|
|
517
|
+
options: {
|
|
518
|
+
user_scope: [{ type: 'user', id: userId }],
|
|
519
|
+
policy: {
|
|
520
|
+
key: 'custom',
|
|
521
|
+
actions: ['view', 'add', 'edit'],
|
|
522
|
+
all_fields_read: true,
|
|
523
|
+
all_fields_write: true,
|
|
524
|
+
fields: [{ key: ':1002', read: false, write: false, report: false }],
|
|
525
|
+
},
|
|
526
|
+
list_options: {},
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function buildAssignUsersValidationDetails(scope, command, requestType, path, message, env = process.env) {
|
|
296
531
|
return {
|
|
297
532
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
298
533
|
requestType,
|
|
299
534
|
issues: [{ path, message }],
|
|
300
535
|
tsHints: [
|
|
301
|
-
{ symbol: 'AppIngressUpdateReqVO', file: '
|
|
302
|
-
{ symbol: 'EntityIngressOptions', file: '
|
|
303
|
-
{ symbol: 'PermitUserScope', file: '
|
|
536
|
+
{ symbol: 'AppIngressUpdateReqVO', file: sdkPath('types/ingress.ts', env) },
|
|
537
|
+
{ symbol: 'EntityIngressOptions', file: sdkPath('types/ingress.ts', env) },
|
|
538
|
+
{ symbol: 'PermitUserScope', file: sdkPath('types/common.ts', env) },
|
|
304
539
|
],
|
|
305
|
-
docHints: getDocHints(requestType),
|
|
540
|
+
docHints: getDocHints(requestType, env),
|
|
306
541
|
shapeHint: {
|
|
307
542
|
update: ['user_scope'],
|
|
308
543
|
options: {
|
|
309
|
-
user_scope: [{ type: '
|
|
544
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
310
545
|
},
|
|
311
546
|
},
|
|
312
547
|
commonMistakes: [
|
|
@@ -317,76 +552,355 @@ function buildAssignUsersValidationDetails(scope, command, requestType, path, me
|
|
|
317
552
|
],
|
|
318
553
|
};
|
|
319
554
|
}
|
|
320
|
-
function
|
|
321
|
-
if (
|
|
322
|
-
|
|
555
|
+
function buildBodyGuidance(requestType) {
|
|
556
|
+
if (requestType === 'EntitySaveReqVO' || requestType === 'AppCreateEntityReqVO') {
|
|
557
|
+
return {
|
|
558
|
+
shapeHint: {
|
|
559
|
+
id: 2188889845,
|
|
560
|
+
app_id: 2188889844,
|
|
561
|
+
name: '订单',
|
|
562
|
+
schema_version: 0,
|
|
563
|
+
field_id_seq: 1004,
|
|
564
|
+
layout: [[1001], [1002], [1003]],
|
|
565
|
+
fields: [
|
|
566
|
+
{
|
|
567
|
+
id: 1001,
|
|
568
|
+
label: '订单号',
|
|
569
|
+
type: 'text',
|
|
570
|
+
required: true,
|
|
571
|
+
unique: false,
|
|
572
|
+
editable: true,
|
|
573
|
+
visible: true,
|
|
574
|
+
show_label: true,
|
|
575
|
+
scannable: false,
|
|
576
|
+
default_value_mode: 0,
|
|
577
|
+
description: '',
|
|
578
|
+
value: null,
|
|
579
|
+
number_decimal: 0,
|
|
580
|
+
depends: [],
|
|
581
|
+
key: 'order_no',
|
|
582
|
+
code_index: false,
|
|
583
|
+
options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 },
|
|
584
|
+
transfers: null,
|
|
585
|
+
children: null,
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: 1002,
|
|
589
|
+
label: '描述',
|
|
590
|
+
type: 'textarea',
|
|
591
|
+
required: false,
|
|
592
|
+
unique: false,
|
|
593
|
+
editable: true,
|
|
594
|
+
visible: true,
|
|
595
|
+
show_label: true,
|
|
596
|
+
scannable: false,
|
|
597
|
+
default_value_mode: 0,
|
|
598
|
+
description: '',
|
|
599
|
+
value: null,
|
|
600
|
+
number_decimal: 0,
|
|
601
|
+
depends: [],
|
|
602
|
+
key: 'description',
|
|
603
|
+
code_index: false,
|
|
604
|
+
options: { placeholder: '', html: false, size: 'default', lengthLimit: false, lengthMin: 0, lengthMax: 2000 },
|
|
605
|
+
transfers: null,
|
|
606
|
+
children: null,
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
id: 1003,
|
|
610
|
+
label: '状态',
|
|
611
|
+
type: 'select',
|
|
612
|
+
required: true,
|
|
613
|
+
unique: false,
|
|
614
|
+
editable: true,
|
|
615
|
+
visible: true,
|
|
616
|
+
show_label: true,
|
|
617
|
+
scannable: false,
|
|
618
|
+
default_value_mode: 1,
|
|
619
|
+
description: '',
|
|
620
|
+
value: 'draft',
|
|
621
|
+
number_decimal: 0,
|
|
622
|
+
depends: [],
|
|
623
|
+
key: 'status',
|
|
624
|
+
code_index: false,
|
|
625
|
+
options: {
|
|
626
|
+
horizontal: false,
|
|
627
|
+
numberMax: '',
|
|
628
|
+
numberMin: '',
|
|
629
|
+
numberRange: false,
|
|
630
|
+
repeatRemind: 'This value already exists and duplicate submission is not allowed',
|
|
631
|
+
options: {
|
|
632
|
+
mode: 'custom',
|
|
633
|
+
colorful: false,
|
|
634
|
+
items: [
|
|
635
|
+
{ key: 1, value: 'draft' },
|
|
636
|
+
{ key: 2, value: 'active' },
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
transfers: null,
|
|
641
|
+
children: null,
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
645
|
+
workflow_enabled: false,
|
|
646
|
+
},
|
|
647
|
+
commonMistakes: [
|
|
648
|
+
'table create must include fields and field_id_seq; do not create an empty table shell',
|
|
649
|
+
'prefer --body-json for table create/update-schema; do not create a body file unless JSON is too large for one tool call',
|
|
650
|
+
'fields must be an array of full FieldVO objects, not an object map',
|
|
651
|
+
'get table shell first: arcubase-admin table get --app-id <app_id> --table-id <table_id>',
|
|
652
|
+
'save the full returned schema after editing; do not submit only changed fields',
|
|
653
|
+
'field_id_seq must be greater than every field.id',
|
|
654
|
+
'layout must reference existing field ids',
|
|
655
|
+
'use type "text", not "string"',
|
|
656
|
+
'use type "member", not "user"',
|
|
657
|
+
'for select/radio/checkbox/status, options must contain options.items',
|
|
658
|
+
'for linkto/relation/query, options must contain relation.app_id, relation.entity_id, relation.type',
|
|
659
|
+
'do not put per-user permissions in FieldVO; use access-rule for read/write and hidden fields',
|
|
660
|
+
],
|
|
661
|
+
suggestions: [
|
|
662
|
+
`retry with: arcubase-admin table create --app-id <app_id> --body-json '${JSON.stringify(buildTableCreateExampleBody())}'`,
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
|
|
667
|
+
return {
|
|
668
|
+
shapeHint: buildAccessRuleShapeHint(),
|
|
669
|
+
commonMistakes: [
|
|
670
|
+
'use options.policy, not top-level permissions',
|
|
671
|
+
'use options.policy.fields, not field_visibility',
|
|
672
|
+
'actions must use Arcubase action codes: view, add, edit; do not use read, write, or insert',
|
|
673
|
+
'field permission keys must be strings prefixed with colon, for example ":1002"; do not use FieldVO.key values such as "vote_name"',
|
|
674
|
+
'for access-rule update policy changes, use {"update":["options"],"options":{"policy":...,"list_options":{}}}; do not use update:["options.policy"]',
|
|
675
|
+
'use options.user_scope with type user/department/actor and numeric id; do not use FieldVO type "member" here',
|
|
676
|
+
'use assign-users only for updating user_scope later',
|
|
677
|
+
'assign-users preserves existing policy when body.update=["user_scope"]',
|
|
678
|
+
],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (requestType === 'InsertReqVO') {
|
|
682
|
+
return {
|
|
683
|
+
shapeHint: {
|
|
684
|
+
fields: {
|
|
685
|
+
'1001': 'SO-1001',
|
|
686
|
+
'1002': 199,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
commonMistakes: [
|
|
690
|
+
'row fields must use numeric field ids as JSON object keys',
|
|
691
|
+
'do not use field labels such as "标题" or "投票标题" as keys',
|
|
692
|
+
'run table get first when field ids are unclear',
|
|
693
|
+
'do not add access policy fields; the CLI resolves access policy internally',
|
|
694
|
+
],
|
|
695
|
+
suggestions: [
|
|
696
|
+
'run: arcubase table get --app-id <app_id> --table-id <table_id>',
|
|
697
|
+
'retry with: arcubase row create --app-id <app_id> --table-id <table_id> --body-json \'{"fields":{"1001":"今天吃什么"}}\'',
|
|
698
|
+
],
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (requestType === 'EntityQueryReqVO') {
|
|
702
|
+
return {
|
|
703
|
+
shapeHint: {
|
|
704
|
+
limit: 20,
|
|
705
|
+
offset: 0,
|
|
706
|
+
search: { text_search: 'SO-1001' },
|
|
707
|
+
},
|
|
708
|
+
commonMistakes: [
|
|
709
|
+
'limit, offset, search, sorts, and view_mode belong inside --body-json',
|
|
710
|
+
'do not pass --limit or --offset flags',
|
|
711
|
+
'do not add access policy fields; the CLI resolves access policy internally',
|
|
712
|
+
],
|
|
713
|
+
suggestions: [
|
|
714
|
+
'retry with: arcubase row query --app-id <app_id> --table-id <table_id> --body-json \'{"limit":20,"offset":0}\'',
|
|
715
|
+
],
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (requestType === 'EntityUpdateReqVO') {
|
|
719
|
+
return {
|
|
720
|
+
shapeHint: {
|
|
721
|
+
data: { '1002': 299 },
|
|
722
|
+
changed_fields: [1002],
|
|
723
|
+
},
|
|
724
|
+
commonMistakes: [
|
|
725
|
+
'use data, not fields',
|
|
726
|
+
'changed_fields is required and must match data keys',
|
|
727
|
+
'do not add access policy fields; the CLI resolves access policy internally',
|
|
728
|
+
],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
if (requestType === 'EntityBulkUpdateReqVO') {
|
|
732
|
+
return {
|
|
733
|
+
shapeHint: {
|
|
734
|
+
selection: {
|
|
735
|
+
type: 'ids',
|
|
736
|
+
ids: [2188890411, 2188890412],
|
|
737
|
+
length: 2,
|
|
738
|
+
},
|
|
739
|
+
fields: [
|
|
740
|
+
{
|
|
741
|
+
id: 1003,
|
|
742
|
+
action: {
|
|
743
|
+
type: 'set',
|
|
744
|
+
value: 2,
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
commonMistakes: [
|
|
750
|
+
'bulk update fields[].action must be an object with type',
|
|
751
|
+
'use {"type":"set","value":2} to assign a scalar value',
|
|
752
|
+
'do not use value_set, setValue, or a top-level value field',
|
|
753
|
+
'use {"type":"number_add","number_add":10} only for numeric increments',
|
|
754
|
+
'selection.type=ids requires ids and length',
|
|
755
|
+
],
|
|
756
|
+
suggestions: [
|
|
757
|
+
'retry with: arcubase row bulk-update --app-id <app_id> --table-id <table_id> --body-json \'{"selection":{"type":"ids","ids":[2188890411],"length":1},"fields":[{"id":1003,"action":{"type":"set","value":2}}]}\'',
|
|
758
|
+
],
|
|
759
|
+
};
|
|
323
760
|
}
|
|
324
|
-
|
|
761
|
+
return {};
|
|
762
|
+
}
|
|
763
|
+
function buildBodyValidationDetails(scope, command, requestType, issues, env = process.env) {
|
|
764
|
+
return {
|
|
325
765
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
326
766
|
requestType,
|
|
327
|
-
issues
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
767
|
+
issues,
|
|
768
|
+
tsHints: getTypeHints(requestType, env),
|
|
769
|
+
docHints: getDocHints(requestType, env),
|
|
770
|
+
...buildBodyGuidance(requestType),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function throwBodyValidationFailure(message, requestType, path, scope, command, env = process.env) {
|
|
774
|
+
if (isAssignUsersCommand(scope, command)) {
|
|
775
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path, message, env));
|
|
776
|
+
}
|
|
777
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails(scope, command, requestType, [{ path, message }], env));
|
|
778
|
+
}
|
|
779
|
+
function requireTableCreateSchemaBody(scope, command, body, env) {
|
|
780
|
+
if (!isRecord(body)) {
|
|
781
|
+
throwBodyValidationFailure('table create requires a JSON object with name, fields, field_id_seq, layout, options, and workflow_enabled', 'AppCreateEntityReqVO', 'body', scope, command, env);
|
|
782
|
+
}
|
|
783
|
+
if (!Array.isArray(body.fields) || body.fields.length === 0) {
|
|
784
|
+
throwBodyValidationFailure('table create must include non-empty body.fields; empty table shell creation is not allowed', 'AppCreateEntityReqVO', 'body.fields', scope, command, env);
|
|
785
|
+
}
|
|
786
|
+
if (typeof body.field_id_seq !== 'number') {
|
|
787
|
+
throwBodyValidationFailure('table create must include body.field_id_seq greater than every field.id', 'AppCreateEntityReqVO', 'body.field_id_seq', scope, command, env);
|
|
788
|
+
}
|
|
336
789
|
}
|
|
337
790
|
function stringArray(value) {
|
|
338
791
|
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
339
792
|
}
|
|
340
|
-
function
|
|
793
|
+
function validateAccessRuleUserScope(scope, command, requestType, userScope, env, pathPrefix = 'body.options.user_scope') {
|
|
794
|
+
if (userScope === undefined)
|
|
795
|
+
return;
|
|
796
|
+
if (!Array.isArray(userScope)) {
|
|
797
|
+
throwBodyValidationFailure('access-rule user_scope must be an array of {"type":"user","id":2188889901}', requestType, pathPrefix, scope, command, env);
|
|
798
|
+
}
|
|
799
|
+
const allowedTypes = new Set(['user', 'department', 'actor']);
|
|
800
|
+
for (const [index, item] of userScope.entries()) {
|
|
801
|
+
if (!isRecord(item)) {
|
|
802
|
+
throwBodyValidationFailure('access-rule user_scope item must be an object', requestType, `${pathPrefix}.${index}`, scope, command, env);
|
|
803
|
+
}
|
|
804
|
+
if (typeof item.type !== 'string' || !allowedTypes.has(item.type)) {
|
|
805
|
+
throwBodyValidationFailure('access-rule user_scope[].type must be "user", "department", or "actor"; do not use FieldVO type "member" here', requestType, `${pathPrefix}.${index}.type`, scope, command, env);
|
|
806
|
+
}
|
|
807
|
+
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
808
|
+
throwBodyValidationFailure('access-rule user_scope[].id must be numeric TenantUser.ID, department ID, or actor ID', requestType, `${pathPrefix}.${index}.id`, scope, command, env);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function requireUpdateContains(scope, command, body, field, env) {
|
|
341
813
|
if (!(field in body))
|
|
342
814
|
return;
|
|
343
815
|
const update = stringArray(body.update);
|
|
344
816
|
if (!update.includes(field)) {
|
|
345
|
-
throwBodyValidationFailure(`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`, command.requestType ?? 'AppIngressUpdateReqVO', 'body.update', scope, command);
|
|
817
|
+
throwBodyValidationFailure(`body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`, command.requestType ?? 'AppIngressUpdateReqVO', 'body.update', scope, command, env);
|
|
346
818
|
}
|
|
347
819
|
}
|
|
348
|
-
function validateActionSpecificRawBody(scope, command, body) {
|
|
820
|
+
function validateActionSpecificRawBody(scope, command, body, env) {
|
|
349
821
|
if (!isRecord(body) || !command.requestType) {
|
|
350
822
|
return;
|
|
351
823
|
}
|
|
352
824
|
if (isAssignUsersCommand(scope, command)) {
|
|
353
825
|
if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
|
|
354
|
-
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
826
|
+
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (isTableSchemaCommand(scope, command)) {
|
|
830
|
+
const fields = Array.isArray(body.fields) ? body.fields : [];
|
|
831
|
+
const hasPermissionPatch = fields.some((field) => isRecord(field) && ('permission' in field || 'permissions' in field || 'field_visibility' in field));
|
|
832
|
+
if (hasPermissionPatch) {
|
|
833
|
+
throwBodyValidationFailure('table schema cannot configure per-user permissions. Use access-rule create/update with options.policy.fields for hidden fields', command.requestType, 'body.fields', scope, command, env);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (isAccessRuleCommand(scope, command)) {
|
|
837
|
+
const update = stringArray(body.update);
|
|
838
|
+
if (isAccessRuleUpdateCommand(scope, command) && update.some((item) => item.startsWith('options.'))) {
|
|
839
|
+
throwBodyValidationFailure('access-rule update cannot patch nested options paths. Use {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}', command.requestType, 'body.update', scope, command, env);
|
|
840
|
+
}
|
|
841
|
+
if ('permissions' in body || 'field_visibility' in body || 'description' in body) {
|
|
842
|
+
throwBodyValidationFailure('access-rule body only supports name, enabled, type, and options; use options.policy.actions and options.policy.fields', command.requestType, 'body', scope, command, env);
|
|
843
|
+
}
|
|
844
|
+
const options = isRecord(body.options) ? body.options : undefined;
|
|
845
|
+
if (options && ('permissions' in options || 'field_visibility' in options)) {
|
|
846
|
+
throwBodyValidationFailure('access-rule options only supports user_scope, policy, list_options, i18n_name; use options.policy.actions and options.policy.fields', command.requestType, 'body.options', scope, command, env);
|
|
847
|
+
}
|
|
848
|
+
const policy = options && isRecord(options.policy) ? options.policy : undefined;
|
|
849
|
+
if (isAccessRuleUpdateCommand(scope, command) && update.includes('options') && options?.user_scope !== undefined && !policy) {
|
|
850
|
+
throwBodyValidationFailure('access-rule update with update:["options"] cannot change only options.user_scope because it overwrites policy. Use access-rule assign-users for user assignment, or include the full options.policy and options.list_options.', command.requestType, 'body.options.policy', scope, command, env);
|
|
851
|
+
}
|
|
852
|
+
validateAccessRuleUserScope(scope, command, command.requestType, options?.user_scope, env);
|
|
853
|
+
const actions = policy && Array.isArray(policy.actions) ? policy.actions : [];
|
|
854
|
+
if (actions.includes('read') || actions.includes('write')) {
|
|
855
|
+
throwBodyValidationFailure('access-rule policy.actions must use Arcubase action codes: use "view" for read and "add","edit" for write; do not use "read", "write", or "insert"', command.requestType, 'body.options.policy.actions', scope, command, env);
|
|
856
|
+
}
|
|
857
|
+
const fields = policy && Array.isArray(policy.fields) ? policy.fields : [];
|
|
858
|
+
const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator']);
|
|
859
|
+
for (const [index, field] of fields.entries()) {
|
|
860
|
+
if (!isRecord(field))
|
|
861
|
+
continue;
|
|
862
|
+
if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
|
|
863
|
+
throwBodyValidationFailure('access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers', command.requestType, `body.options.policy.fields.${index}.key`, scope, command, env);
|
|
864
|
+
}
|
|
865
|
+
if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
|
|
866
|
+
throwBodyValidationFailure('access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"', command.requestType, `body.options.policy.fields.${index}.key`, scope, command, env);
|
|
867
|
+
}
|
|
355
868
|
}
|
|
356
869
|
}
|
|
357
870
|
}
|
|
358
|
-
function validateActionSpecificBody(scope, command, flags, body) {
|
|
871
|
+
function validateActionSpecificBody(scope, command, flags, body, env) {
|
|
359
872
|
if (!isRecord(body) || !command.requestType) {
|
|
360
873
|
return;
|
|
361
874
|
}
|
|
362
875
|
if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
|
|
363
|
-
requireUpdateContains(scope, command, body, 'enabled');
|
|
364
|
-
requireUpdateContains(scope, command, body, 'name');
|
|
365
|
-
requireUpdateContains(scope, command, body, 'options');
|
|
876
|
+
requireUpdateContains(scope, command, body, 'enabled', env);
|
|
877
|
+
requireUpdateContains(scope, command, body, 'name', env);
|
|
878
|
+
requireUpdateContains(scope, command, body, 'options', env);
|
|
366
879
|
}
|
|
367
880
|
if (isAssignUsersCommand(scope, command)) {
|
|
368
881
|
const update = stringArray(body.update);
|
|
369
882
|
const options = isRecord(body.options) ? body.options : undefined;
|
|
370
883
|
const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined;
|
|
371
884
|
if ('users' in body || 'allowedUsers' in body) {
|
|
372
|
-
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
885
|
+
throwBodyValidationFailure('access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
373
886
|
}
|
|
374
887
|
if (!update.includes('user_scope')) {
|
|
375
|
-
throwBodyValidationFailure('access-rule assign-users requires body.update to include "user_scope"', command.requestType, 'body.update', scope, command);
|
|
888
|
+
throwBodyValidationFailure('access-rule assign-users requires body.update to include "user_scope"', command.requestType, 'body.update', scope, command, env);
|
|
376
889
|
}
|
|
377
890
|
if (!userScope || userScope.length === 0) {
|
|
378
|
-
throwBodyValidationFailure('access-rule assign-users requires non-empty body.options.user_scope', command.requestType, 'body.options.user_scope', scope, command);
|
|
891
|
+
throwBodyValidationFailure('access-rule assign-users requires non-empty body.options.user_scope', command.requestType, 'body.options.user_scope', scope, command, env);
|
|
379
892
|
}
|
|
893
|
+
validateAccessRuleUserScope(scope, command, command.requestType, userScope, env);
|
|
380
894
|
const assignedUserScope = userScope;
|
|
381
895
|
for (const [index, item] of assignedUserScope.entries()) {
|
|
382
896
|
if (!isRecord(item)) {
|
|
383
|
-
throwBodyValidationFailure('access-rule assign-users requires each body.options.user_scope item to be an object', command.requestType, `body.options.user_scope.${index}`, scope, command);
|
|
897
|
+
throwBodyValidationFailure('access-rule assign-users requires each body.options.user_scope item to be an object', command.requestType, `body.options.user_scope.${index}`, scope, command, env);
|
|
384
898
|
}
|
|
385
|
-
if (item.type !== '
|
|
386
|
-
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].type to be "
|
|
899
|
+
if (item.type !== 'user') {
|
|
900
|
+
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].type to be "user"', command.requestType, `body.options.user_scope.${index}.type`, scope, command, env);
|
|
387
901
|
}
|
|
388
902
|
if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
|
|
389
|
-
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID', command.requestType, `body.options.user_scope.${index}.id`, scope, command);
|
|
903
|
+
throwBodyValidationFailure('access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID', command.requestType, `body.options.user_scope.${index}.id`, scope, command, env);
|
|
390
904
|
}
|
|
391
905
|
}
|
|
392
906
|
}
|
|
@@ -401,17 +915,314 @@ function validateActionSpecificBody(scope, command, flags, body) {
|
|
|
401
915
|
const conditionKeys = condition ? Object.keys(condition).filter((key) => key !== 'selectAll') : [];
|
|
402
916
|
if (action === 'query') {
|
|
403
917
|
if (type === 'condition') {
|
|
404
|
-
throwBodyValidationFailure('selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use row query for search conditions', command.requestType, 'body.selection.type', scope, command);
|
|
918
|
+
throwBodyValidationFailure('selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use row query for search conditions', command.requestType, 'body.selection.type', scope, command, env);
|
|
405
919
|
}
|
|
406
920
|
return;
|
|
407
921
|
}
|
|
408
922
|
if ((action === 'archive' || action === 'delete' || action === 'batch_print') && type === 'all') {
|
|
409
|
-
throwBodyValidationFailure(`selection.type=all is not supported for action=${action}; use ids or condition`, command.requestType, 'body.selection.type', scope, command);
|
|
923
|
+
throwBodyValidationFailure(`selection.type=all is not supported for action=${action}; use ids or condition`, command.requestType, 'body.selection.type', scope, command, env);
|
|
410
924
|
}
|
|
411
925
|
if ((action === 'archive' || action === 'delete') && type === 'condition' && conditionKeys.length === 0) {
|
|
412
|
-
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);
|
|
926
|
+
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, env);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function unwrapData(value) {
|
|
931
|
+
let current = value;
|
|
932
|
+
for (let i = 0; i < 3; i += 1) {
|
|
933
|
+
if (isRecord(current) && isRecord(current.data)) {
|
|
934
|
+
current = current.data;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
return current;
|
|
940
|
+
}
|
|
941
|
+
function requiredRowAction(command, flags) {
|
|
942
|
+
if (command.commandPath[0] !== 'row')
|
|
943
|
+
return undefined;
|
|
944
|
+
switch (command.commandPath[1]) {
|
|
945
|
+
case 'query':
|
|
946
|
+
case 'get':
|
|
947
|
+
return 'view';
|
|
948
|
+
case 'create':
|
|
949
|
+
return 'add';
|
|
950
|
+
case 'update':
|
|
951
|
+
return 'edit';
|
|
952
|
+
case 'delete':
|
|
953
|
+
return 'delete';
|
|
954
|
+
case 'bulk-update':
|
|
955
|
+
return 'bulk_update';
|
|
956
|
+
case 'selection-action':
|
|
957
|
+
return flagValue(flags, 'action') || undefined;
|
|
958
|
+
default:
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function rowCommandNeedsPolicy(command) {
|
|
963
|
+
return command.scope === 'user' && command.commandPath[0] === 'row';
|
|
964
|
+
}
|
|
965
|
+
function extractIngressItems(appDetail) {
|
|
966
|
+
const appData = unwrapData(appDetail);
|
|
967
|
+
const ingress = isRecord(appData) ? appData.ingress : undefined;
|
|
968
|
+
const groups = isRecord(ingress) && Array.isArray(ingress.groups) ? ingress.groups : [];
|
|
969
|
+
const items = [];
|
|
970
|
+
for (const group of groups) {
|
|
971
|
+
const groupItems = isRecord(group) && Array.isArray(group.items) ? group.items : [];
|
|
972
|
+
for (const item of groupItems) {
|
|
973
|
+
if (!isRecord(item))
|
|
974
|
+
continue;
|
|
975
|
+
const id = typeof item.id === 'string' ? item.id : typeof item.ID === 'string' ? item.ID : '';
|
|
976
|
+
if (id) {
|
|
977
|
+
items.push({ id });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return items;
|
|
982
|
+
}
|
|
983
|
+
function ingressMatchesTableAndAction(ingressDetail, tableId, action) {
|
|
984
|
+
const ingressData = unwrapData(ingressDetail);
|
|
985
|
+
if (!isRecord(ingressData))
|
|
986
|
+
return undefined;
|
|
987
|
+
const id = typeof ingressData.id === 'number' || typeof ingressData.id === 'string'
|
|
988
|
+
? ingressData.id
|
|
989
|
+
: typeof ingressData.ID === 'number' || typeof ingressData.ID === 'string'
|
|
990
|
+
? ingressData.ID
|
|
991
|
+
: undefined;
|
|
992
|
+
if (String(id ?? '') !== tableId)
|
|
993
|
+
return undefined;
|
|
994
|
+
const policyID = typeof ingressData.hash_id === 'string'
|
|
995
|
+
? ingressData.hash_id
|
|
996
|
+
: typeof ingressData.hashId === 'string'
|
|
997
|
+
? ingressData.hashId
|
|
998
|
+
: undefined;
|
|
999
|
+
const policy = isRecord(ingressData.policy) ? ingressData.policy : undefined;
|
|
1000
|
+
if (!action)
|
|
1001
|
+
return { policyID: policyID || '', policy };
|
|
1002
|
+
const actions = policy && Array.isArray(policy.actions) ? policy.actions.filter((item) => typeof item === 'string') : [];
|
|
1003
|
+
if (!actions.includes(action))
|
|
1004
|
+
return undefined;
|
|
1005
|
+
return { policyID: policyID || '', policy };
|
|
1006
|
+
}
|
|
1007
|
+
function writtenRowFieldIds(command, body) {
|
|
1008
|
+
if (!isRecord(body))
|
|
1009
|
+
return [];
|
|
1010
|
+
const source = command.commandPath[1] === 'create'
|
|
1011
|
+
? body.fields
|
|
1012
|
+
: command.commandPath[1] === 'update'
|
|
1013
|
+
? body.data
|
|
1014
|
+
: undefined;
|
|
1015
|
+
if (!isRecord(source))
|
|
1016
|
+
return [];
|
|
1017
|
+
return Object.keys(source)
|
|
1018
|
+
.filter((key) => /^\d+$/.test(key))
|
|
1019
|
+
.map((key) => Number(key));
|
|
1020
|
+
}
|
|
1021
|
+
function fieldWriteAllowedByPolicy(policy, fieldId) {
|
|
1022
|
+
if (!policy)
|
|
1023
|
+
return true;
|
|
1024
|
+
const fields = Array.isArray(policy.fields) ? policy.fields : [];
|
|
1025
|
+
const explicit = fields.find((item) => isRecord(item) && item.key === `:${fieldId}`);
|
|
1026
|
+
if (isRecord(explicit) && explicit.write === false)
|
|
1027
|
+
return false;
|
|
1028
|
+
if (isRecord(explicit) && explicit.write === true)
|
|
1029
|
+
return true;
|
|
1030
|
+
if (policy.all_fields_write === false)
|
|
1031
|
+
return false;
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
function assertWritableFieldsForRowCommand(command, runtimeEnv, body, policy) {
|
|
1035
|
+
if (command.commandPath[1] !== 'create' && command.commandPath[1] !== 'update') {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const denied = writtenRowFieldIds(command, body).filter((fieldId) => !fieldWriteAllowedByPolicy(policy, fieldId));
|
|
1039
|
+
if (denied.length === 0) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const payloadKey = command.commandPath[1] === 'create' ? 'fields' : 'data';
|
|
1043
|
+
throw new CLIError('FIELD_PERMISSION_REQUIRED', `row ${command.commandPath[1]} includes field ids without write permission: ${denied.join(', ')}`, 2, {
|
|
1044
|
+
operation: `user.${command.module}.${command.functionName}`,
|
|
1045
|
+
requestType: command.requestType ?? undefined,
|
|
1046
|
+
issues: denied.map((fieldId) => ({
|
|
1047
|
+
path: `body.${payloadKey}.${fieldId}`,
|
|
1048
|
+
message: 'field is not writable for the resolved Arcubase access rule',
|
|
1049
|
+
})),
|
|
1050
|
+
docHints: [
|
|
1051
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
1052
|
+
{ title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
|
|
1053
|
+
],
|
|
1054
|
+
suggestions: [
|
|
1055
|
+
'remove non-writable field ids from the row body and retry',
|
|
1056
|
+
'run arcubase table get to map visible business fields to numeric field ids',
|
|
1057
|
+
'ask an admin to update the access-rule if this field should be writable',
|
|
1058
|
+
],
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
async function resolvePolicyIDForRowCommand(command, runtimeEnv, flags, fetchImpl) {
|
|
1062
|
+
const appId = flagValue(flags, 'app-id');
|
|
1063
|
+
const tableId = flagValue(flags, 'table-id');
|
|
1064
|
+
const action = requiredRowAction(command, flags);
|
|
1065
|
+
if (!appId || !tableId) {
|
|
1066
|
+
throw new CLIError('POLICY_ID_RESOLUTION_FAILED', 'row command requires --app-id and --table-id to resolve Arcubase policy_id', 2, {
|
|
1067
|
+
operation: `user.${command.module}.${command.functionName}`,
|
|
1068
|
+
docHints: getDocHints(command.requestType ?? '', runtimeEnv),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
const appDetail = await requestJSON(runtimeEnv, 'GET', `/api/apps/${encodeURIComponent(appId)}`, undefined, fetchImpl);
|
|
1072
|
+
const ingressItems = extractIngressItems(appDetail.data);
|
|
1073
|
+
for (const item of ingressItems) {
|
|
1074
|
+
const ingressDetail = await requestJSON(runtimeEnv, 'GET', `/api/ingress/${encodeURIComponent(appId)}/${encodeURIComponent(item.id)}`, undefined, fetchImpl);
|
|
1075
|
+
const match = ingressMatchesTableAndAction(ingressDetail.data, tableId, action);
|
|
1076
|
+
if (match) {
|
|
1077
|
+
return { ...match, policyID: match.policyID || item.id };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
throw new CLIError('POLICY_ID_RESOLUTION_FAILED', `no accessible ingress policy found for app ${appId}, table ${tableId}${action ? `, action ${action}` : ''}`, 2, {
|
|
1081
|
+
operation: `user.${command.module}.${command.functionName}`,
|
|
1082
|
+
requestType: command.requestType ?? undefined,
|
|
1083
|
+
issues: [{ path: 'policy_id', message: 'CLI could not resolve policy_id from accessible ingress' }],
|
|
1084
|
+
docHints: [
|
|
1085
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
1086
|
+
{ title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
|
|
1087
|
+
],
|
|
1088
|
+
suggestions: [
|
|
1089
|
+
'run arcubase entry and confirm the app/table is visible',
|
|
1090
|
+
'ask an admin to create an enabled access-rule with the required action for this table',
|
|
1091
|
+
],
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
async function injectResolvedPolicyID(command, runtimeEnv, flags, query, body, fetchImpl) {
|
|
1095
|
+
if (!rowCommandNeedsPolicy(command)) {
|
|
1096
|
+
return { query, body };
|
|
1097
|
+
}
|
|
1098
|
+
if (typeof query.policy_id === 'string' && query.policy_id) {
|
|
1099
|
+
return { query, body };
|
|
1100
|
+
}
|
|
1101
|
+
if (isRecord(body) && typeof body.policy_id === 'string' && body.policy_id) {
|
|
1102
|
+
return { query, body };
|
|
1103
|
+
}
|
|
1104
|
+
const resolvedPolicy = await resolvePolicyIDForRowCommand(command, runtimeEnv, flags, fetchImpl);
|
|
1105
|
+
const policyID = resolvedPolicy.policyID;
|
|
1106
|
+
assertWritableFieldsForRowCommand(command, runtimeEnv, body, resolvedPolicy.policy);
|
|
1107
|
+
if (command.commandPath[1] === 'get') {
|
|
1108
|
+
return { query: { ...query, policy_id: policyID }, body };
|
|
1109
|
+
}
|
|
1110
|
+
if (isRecord(body)) {
|
|
1111
|
+
return { query, body: { ...body, policy_id: policyID } };
|
|
1112
|
+
}
|
|
1113
|
+
return { query, body };
|
|
1114
|
+
}
|
|
1115
|
+
function assertNoUserPolicyIDInput(command, runtimeEnv, query, body) {
|
|
1116
|
+
if (!rowCommandNeedsPolicy(command)) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if ('policy_id' in query || (isRecord(body) && 'policy_id' in body)) {
|
|
1120
|
+
throw new CLIError('INTERNAL_POLICY_ID_NOT_ALLOWED', 'policy_id is an internal Arcubase ingress parameter; use app/table/row ids only', 2, {
|
|
1121
|
+
operation: `user.${command.module}.${command.functionName}`,
|
|
1122
|
+
issues: [{ path: 'policy_id', message: 'do not provide policy_id in flags, query file, or body JSON' }],
|
|
1123
|
+
docHints: [
|
|
1124
|
+
{ title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
|
|
1125
|
+
{ title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
|
|
1126
|
+
],
|
|
1127
|
+
suggestions: ['run arcubase entry to find the app/table ids, then retry without policy_id'],
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function requestJSON(runtimeEnv, method, endpoint, body, fetchImpl) {
|
|
1132
|
+
const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint), {
|
|
1133
|
+
method,
|
|
1134
|
+
headers: {
|
|
1135
|
+
...buildRequestHeaders(runtimeEnv),
|
|
1136
|
+
'Content-Type': 'application/json',
|
|
1137
|
+
},
|
|
1138
|
+
body: body === undefined || method === 'GET' ? undefined : JSON.stringify(body),
|
|
1139
|
+
}, endpoint, method, fetchImpl);
|
|
1140
|
+
const raw = await response.text();
|
|
1141
|
+
let parsedResponse = raw;
|
|
1142
|
+
try {
|
|
1143
|
+
parsedResponse = raw ? JSON.parse(raw) : null;
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
// keep raw text
|
|
1147
|
+
}
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
throwUpstreamHTTPError(endpoint, method, response, parsedResponse);
|
|
1150
|
+
}
|
|
1151
|
+
if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
|
|
1152
|
+
const upstreamError = parsedResponse.error;
|
|
1153
|
+
const upstreamMessage = upstreamErrorMessage(upstreamError);
|
|
1154
|
+
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, { upstreamError });
|
|
1155
|
+
}
|
|
1156
|
+
return { status: response.status, data: parsedResponse };
|
|
1157
|
+
}
|
|
1158
|
+
function extractCreatedTableID(value) {
|
|
1159
|
+
const candidates = [
|
|
1160
|
+
value,
|
|
1161
|
+
isRecord(value) ? value.data : undefined,
|
|
1162
|
+
isRecord(value) && isRecord(value.data) ? value.data.data : undefined,
|
|
1163
|
+
];
|
|
1164
|
+
for (const candidate of candidates) {
|
|
1165
|
+
if (isRecord(candidate) && typeof candidate.id === 'number') {
|
|
1166
|
+
return candidate.id;
|
|
413
1167
|
}
|
|
414
1168
|
}
|
|
1169
|
+
throw new CLIError('UPSTREAM_RESPONSE_INVALID', 'table create response did not include numeric table id', 1);
|
|
1170
|
+
}
|
|
1171
|
+
function normalizeTableCreateSchemaBody(body, appId, tableId) {
|
|
1172
|
+
const { parent: _parent, ...schemaBody } = body;
|
|
1173
|
+
return {
|
|
1174
|
+
...schemaBody,
|
|
1175
|
+
id: tableId,
|
|
1176
|
+
app_id: Number.isFinite(Number(appId)) ? Number(appId) : body.app_id,
|
|
1177
|
+
schema_version: typeof body.schema_version === 'number' ? body.schema_version : 0,
|
|
1178
|
+
layout: Array.isArray(body.layout) ? body.layout : [],
|
|
1179
|
+
options: isRecord(body.options)
|
|
1180
|
+
? body.options
|
|
1181
|
+
: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
1182
|
+
workflow_enabled: typeof body.workflow_enabled === 'boolean' ? body.workflow_enabled : false,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async function executeTableCreateWithSchema(command, runtimeEnv, flags, body, fetchImpl) {
|
|
1186
|
+
const appId = flagValue(flags, 'app-id');
|
|
1187
|
+
if (!appId) {
|
|
1188
|
+
throw new CLIError('MISSING_PATH_FLAG', 'missing required flag --app-id', 2);
|
|
1189
|
+
}
|
|
1190
|
+
if (!isRecord(body)) {
|
|
1191
|
+
throw new CLIError('BODY_VALIDATION_FAILED', 'table create body must be an object', 2);
|
|
1192
|
+
}
|
|
1193
|
+
const createBody = {
|
|
1194
|
+
name: typeof body.name === 'string' ? body.name : undefined,
|
|
1195
|
+
parent: typeof body.parent === 'number' ? body.parent : 0,
|
|
1196
|
+
};
|
|
1197
|
+
if (!createBody.name) {
|
|
1198
|
+
throw new CLIError('BODY_VALIDATION_FAILED', 'table create requires body.name', 2);
|
|
1199
|
+
}
|
|
1200
|
+
const createResult = await requestJSON(runtimeEnv, command.method, resolveEndpoint(command, flags), createBody, fetchImpl);
|
|
1201
|
+
const tableId = extractCreatedTableID(createResult.data);
|
|
1202
|
+
const saveBody = normalizeTableCreateSchemaBody(body, appId, tableId);
|
|
1203
|
+
const saveSchema = getBodySchema('EntitySaveReqVO');
|
|
1204
|
+
if (!saveSchema) {
|
|
1205
|
+
throw new CLIError('BODY_VALIDATION_FAILED', 'EntitySaveReqVO schema is not available', 2);
|
|
1206
|
+
}
|
|
1207
|
+
const parsedSaveBody = saveSchema.safeParse(saveBody);
|
|
1208
|
+
if (!parsedSaveBody.success) {
|
|
1209
|
+
const message = parsedSaveBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
1210
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails('admin', command, 'EntitySaveReqVO', parsedSaveBody.error.issues.map((issue) => ({
|
|
1211
|
+
path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
|
|
1212
|
+
message: issue.message,
|
|
1213
|
+
})), runtimeEnv));
|
|
1214
|
+
}
|
|
1215
|
+
const saveResult = await requestJSON(runtimeEnv, 'PUT', `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(String(tableId))}`, parsedSaveBody.data, fetchImpl);
|
|
1216
|
+
return {
|
|
1217
|
+
kind: 'result',
|
|
1218
|
+
commandPath: command.commandPath,
|
|
1219
|
+
status: saveResult.status,
|
|
1220
|
+
data: {
|
|
1221
|
+
id: tableId,
|
|
1222
|
+
created: createResult.data,
|
|
1223
|
+
schema: saveResult.data,
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
415
1226
|
}
|
|
416
1227
|
export function summarizeCommand(scope, moduleName, commandName) {
|
|
417
1228
|
const command = findCommand(scope, moduleName, commandName);
|
|
@@ -425,31 +1236,12 @@ export function summarizeCommand(scope, moduleName, commandName) {
|
|
|
425
1236
|
endpoint: command.endpoint,
|
|
426
1237
|
requestType: command.requestType,
|
|
427
1238
|
pathFlags: command.pathParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
|
|
428
|
-
queryFlags: command
|
|
1239
|
+
queryFlags: visibleQueryParams(command).map((item) => item.flag),
|
|
429
1240
|
};
|
|
430
1241
|
}
|
|
431
1242
|
export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
432
1243
|
const parsed = parseCLIArgs(argv);
|
|
433
|
-
|
|
434
|
-
if (hasFlag(parsed.flags, 'help') || parsed.commandTokens.length === 1) {
|
|
435
|
-
return { kind: 'help', text: renderUploadHelp() };
|
|
436
|
-
}
|
|
437
|
-
const sourcePath = parsed.commandTokens[1];
|
|
438
|
-
if (!sourcePath) {
|
|
439
|
-
throw new CLIError('UPLOAD_SOURCE_REQUIRED', 'upload requires a local file path', 2);
|
|
440
|
-
}
|
|
441
|
-
const runtimeEnv = env ?? loadRuntimeEnv(scope);
|
|
442
|
-
const data = await uploadLocalFile(runtimeEnv, sourcePath, {
|
|
443
|
-
filename: flagValue(parsed.flags, 'filename'),
|
|
444
|
-
global: hasFlag(parsed.flags, 'global'),
|
|
445
|
-
}, fetchImpl);
|
|
446
|
-
return {
|
|
447
|
-
kind: 'result',
|
|
448
|
-
commandPath: ['upload', sourcePath],
|
|
449
|
-
status: 200,
|
|
450
|
-
data,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
1244
|
+
const envInput = env ?? process.env;
|
|
453
1245
|
if (parsed.commandTokens.length === 0) {
|
|
454
1246
|
return { kind: 'help', text: renderRootHelp(scope) };
|
|
455
1247
|
}
|
|
@@ -459,30 +1251,58 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
459
1251
|
}
|
|
460
1252
|
const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null;
|
|
461
1253
|
if (!commandName && !singleTokenCommand) {
|
|
462
|
-
return { kind: 'help', text: renderModuleHelp(scope, moduleName) };
|
|
1254
|
+
return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) };
|
|
463
1255
|
}
|
|
464
1256
|
if (hasFlag(parsed.flags, 'help')) {
|
|
465
|
-
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) };
|
|
1257
|
+
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) };
|
|
466
1258
|
}
|
|
467
|
-
const runtimeEnv = env ?? loadRuntimeEnv(scope);
|
|
468
1259
|
const command = findCommand(scope, moduleName, commandName);
|
|
469
1260
|
if (!command) {
|
|
470
1261
|
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''));
|
|
471
1262
|
}
|
|
1263
|
+
validateKnownFlags(command, parsed.flags, envInput);
|
|
1264
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope);
|
|
472
1265
|
const endpoint = resolveEndpoint(command, parsed.flags);
|
|
473
1266
|
const scalarQuery = buildScalarQuery(command, parsed.flags);
|
|
474
1267
|
const fileQuery = loadQueryFromFlags(parsed.flags);
|
|
475
|
-
|
|
476
|
-
|
|
1268
|
+
let query = { ...(fileQuery ?? {}), ...(scalarQuery ?? {}) };
|
|
1269
|
+
let body;
|
|
1270
|
+
try {
|
|
1271
|
+
body = loadBodyFromFlags(parsed.flags);
|
|
1272
|
+
}
|
|
1273
|
+
catch (error) {
|
|
1274
|
+
if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
|
|
1275
|
+
throw new CLIError('INVALID_BODY_JSON', `body json is invalid for ${scope}.${command.commandPath.join(' ')}; fix JSON syntax, then use the canonical ${command.requestType} shape from docs/types`, 2, buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv));
|
|
1276
|
+
}
|
|
1277
|
+
throw error;
|
|
1278
|
+
}
|
|
477
1279
|
let validatedBody = body;
|
|
478
1280
|
if (!command.requestType && body !== undefined) {
|
|
479
1281
|
throw new CLIError('BODY_JSON_NOT_SUPPORTED', `command does not accept a request body: ${moduleName} ${commandName}`, 2);
|
|
480
1282
|
}
|
|
481
1283
|
if (command.requestType) {
|
|
482
1284
|
if (body === undefined) {
|
|
483
|
-
throw new CLIError('MISSING_BODY_FILE', `command requires --body-
|
|
1285
|
+
throw new CLIError('MISSING_BODY_FILE', `command requires --body-json or --body-file for ${command.requestType}; prefer --body-json to avoid file creation`, 2);
|
|
484
1286
|
}
|
|
485
|
-
|
|
1287
|
+
if (isTableCreateCommand(scope, command)) {
|
|
1288
|
+
requireTableCreateSchemaBody(scope, command, body, runtimeEnv);
|
|
1289
|
+
validateActionSpecificRawBody(scope, command, body, runtimeEnv);
|
|
1290
|
+
const { parent: _parent, ...schemaBody } = isRecord(body) ? body : {};
|
|
1291
|
+
const parsedBody = getBodySchema('EntitySaveReqVO')?.safeParse(schemaBody);
|
|
1292
|
+
if (!parsedBody?.success) {
|
|
1293
|
+
const message = parsedBody
|
|
1294
|
+
? parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
|
|
1295
|
+
: 'EntitySaveReqVO schema is not available';
|
|
1296
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails(scope, command, 'EntitySaveReqVO', parsedBody && !parsedBody.success
|
|
1297
|
+
? parsedBody.error.issues.map((issue) => ({
|
|
1298
|
+
path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
|
|
1299
|
+
message: issue.message,
|
|
1300
|
+
}))
|
|
1301
|
+
: [{ path: 'body', message }], runtimeEnv));
|
|
1302
|
+
}
|
|
1303
|
+
return executeTableCreateWithSchema(command, runtimeEnv, parsed.flags, parsedBody.data, fetchImpl);
|
|
1304
|
+
}
|
|
1305
|
+
validateActionSpecificRawBody(scope, command, body, runtimeEnv);
|
|
486
1306
|
const schema = getBodySchema(command.requestType);
|
|
487
1307
|
if (schema) {
|
|
488
1308
|
const parsedBody = schema.safeParse(body);
|
|
@@ -490,21 +1310,15 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
490
1310
|
const message = parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
491
1311
|
if (isAssignUsersCommand(scope, command)) {
|
|
492
1312
|
const hasUserScopeIDIssue = parsedBody.error.issues.some((issue) => issue.path.join('.') === 'options.user_scope.0.id' || issue.path.join('.').startsWith('options.user_scope.'));
|
|
493
|
-
throw new CLIError('BODY_VALIDATION_FAILED', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message, 2, buildAssignUsersValidationDetails(scope, command, command.requestType, hasUserScopeIDIssue ? 'body.options.user_scope' : 'body', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message));
|
|
1313
|
+
throw new CLIError('BODY_VALIDATION_FAILED', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message, 2, buildAssignUsersValidationDetails(scope, command, command.requestType, hasUserScopeIDIssue ? 'body.options.user_scope' : 'body', hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message, runtimeEnv));
|
|
494
1314
|
}
|
|
495
|
-
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
|
|
500
|
-
message: issue.message,
|
|
501
|
-
})),
|
|
502
|
-
tsHints: getTypeHints(command.requestType),
|
|
503
|
-
docHints: getDocHints(command.requestType),
|
|
504
|
-
});
|
|
1315
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails(scope, command, command.requestType, parsedBody.error.issues.map((issue) => ({
|
|
1316
|
+
path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
|
|
1317
|
+
message: issue.message,
|
|
1318
|
+
})), runtimeEnv));
|
|
505
1319
|
}
|
|
506
1320
|
validatedBody = parsedBody.data;
|
|
507
|
-
validateActionSpecificBody(scope, command, parsed.flags, validatedBody);
|
|
1321
|
+
validateActionSpecificBody(scope, command, parsed.flags, validatedBody, runtimeEnv);
|
|
508
1322
|
}
|
|
509
1323
|
else {
|
|
510
1324
|
const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType);
|
|
@@ -513,23 +1327,27 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
513
1327
|
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
514
1328
|
requestType: command.requestType,
|
|
515
1329
|
reason: unsupportedReason,
|
|
516
|
-
tsHints: getTypeHints(command.requestType),
|
|
517
|
-
docHints: getDocHints(command.requestType),
|
|
1330
|
+
tsHints: getTypeHints(command.requestType, runtimeEnv),
|
|
1331
|
+
docHints: getDocHints(command.requestType, runtimeEnv),
|
|
518
1332
|
});
|
|
519
1333
|
}
|
|
520
1334
|
}
|
|
521
1335
|
}
|
|
1336
|
+
assertNoUserPolicyIDInput(command, runtimeEnv, query, validatedBody);
|
|
522
1337
|
if (scope === 'user' && (command.functionName === 'insertEntity' || command.functionName === 'updateEntityRow')) {
|
|
523
1338
|
await validateFileFieldPayloads(scope, command, runtimeEnv, parsed.flags, validatedBody, fetchImpl);
|
|
524
1339
|
}
|
|
525
|
-
const
|
|
1340
|
+
const policyInjected = await injectResolvedPolicyID(command, runtimeEnv, parsed.flags, query, validatedBody, fetchImpl);
|
|
1341
|
+
query = policyInjected.query;
|
|
1342
|
+
validatedBody = policyInjected.body;
|
|
1343
|
+
const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
|
|
526
1344
|
method: command.method,
|
|
527
1345
|
headers: {
|
|
528
1346
|
...buildRequestHeaders(runtimeEnv),
|
|
529
1347
|
'Content-Type': 'application/json',
|
|
530
1348
|
},
|
|
531
1349
|
body: validatedBody === undefined || command.method === 'GET' ? undefined : JSON.stringify(validatedBody),
|
|
532
|
-
});
|
|
1350
|
+
}, endpoint, command.method, fetchImpl);
|
|
533
1351
|
const raw = await response.text();
|
|
534
1352
|
let parsedResponse = raw;
|
|
535
1353
|
try {
|
|
@@ -539,20 +1357,12 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
539
1357
|
// keep raw text
|
|
540
1358
|
}
|
|
541
1359
|
if (!response.ok) {
|
|
542
|
-
|
|
1360
|
+
throwUpstreamHTTPError(endpoint, command.method, response, parsedResponse);
|
|
543
1361
|
}
|
|
544
1362
|
if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
|
|
545
1363
|
const upstreamError = parsedResponse.error;
|
|
546
|
-
const upstreamMessage =
|
|
547
|
-
|
|
548
|
-
: typeof upstreamError === 'string'
|
|
549
|
-
? upstreamError
|
|
550
|
-
: JSON.stringify(upstreamError);
|
|
551
|
-
const traceId = typeof parsedResponse.trace_id === 'string'
|
|
552
|
-
? parsedResponse.trace_id
|
|
553
|
-
: typeof parsedResponse.request_id === 'string'
|
|
554
|
-
? parsedResponse.request_id
|
|
555
|
-
: undefined;
|
|
1364
|
+
const upstreamMessage = upstreamErrorMessage(upstreamError);
|
|
1365
|
+
const traceId = extractTraceID(parsedResponse);
|
|
556
1366
|
const uploadBindFailure = upstreamMessage.includes('failed to bind upload to assets');
|
|
557
1367
|
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
558
1368
|
endpoint,
|
|
@@ -562,10 +1372,10 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
|
|
|
562
1372
|
...(uploadBindFailure
|
|
563
1373
|
? {
|
|
564
1374
|
reason: 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record',
|
|
565
|
-
hint: 'verify the upload storage host is reachable from the Arcubase server, then
|
|
1375
|
+
hint: 'verify the upload storage host is reachable from the Arcubase server, then use the upload token flow again and retry the row insert/update',
|
|
566
1376
|
docHints: [
|
|
567
|
-
{ title: 'Uploads', file: '
|
|
568
|
-
{ title: 'Row CRUD', file: '
|
|
1377
|
+
{ title: 'Uploads', file: 'docs/runtime-reference/uploads.md' },
|
|
1378
|
+
{ title: 'Row CRUD', file: 'docs/runtime-reference/row-crud.md' },
|
|
569
1379
|
],
|
|
570
1380
|
}
|
|
571
1381
|
: {}),
|