@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.
- package/bundle/arcubase-admin.mjs +1238 -16
- package/bundle/arcubase.mjs +1238 -16
- package/dist/bin/arcubase-admin.js +0 -0
- package/dist/bin/arcubase.js +0 -0
- package/dist/runtime/command_registry.d.ts +8 -0
- package/dist/runtime/command_registry.d.ts.map +1 -1
- package/dist/runtime/command_registry.js +35 -0
- package/dist/runtime/entity_save_schema.d.ts +15 -0
- package/dist/runtime/entity_save_schema.d.ts.map +1 -0
- package/dist/runtime/entity_save_schema.js +641 -0
- package/dist/runtime/errors.d.ts +11 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +294 -12
- package/dist/runtime/upload.d.ts +13 -0
- package/dist/runtime/upload.d.ts.map +1 -0
- package/dist/runtime/upload.js +148 -0
- package/dist/runtime/zod_registry.d.ts +6 -0
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +136 -0
- package/dist/tests/command_registry.test.js +5 -0
- package/dist/tests/entity_save_schema.test.d.ts +2 -0
- package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
- package/dist/tests/entity_save_schema.test.js +221 -0
- package/dist/tests/execute_validation.test.js +603 -0
- package/dist/tests/help.test.js +49 -0
- package/dist/tests/upload.test.d.ts +2 -0
- package/dist/tests/upload.test.d.ts.map +1 -0
- package/dist/tests/upload.test.js +107 -0
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +187 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/file.md +53 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +52 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
- package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
- package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
- package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +183 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +188 -0
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +135 -0
- package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
- package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
- package/src/runtime/command_registry.ts +47 -0
- package/src/runtime/entity_save_schema.ts +708 -0
- package/src/runtime/errors.ts +12 -0
- package/src/runtime/execute.ts +367 -12
- package/src/runtime/upload.ts +196 -0
- package/src/runtime/zod_registry.ts +156 -0
- package/src/tests/command_registry.test.ts +6 -0
- package/src/tests/entity_save_schema.test.ts +236 -0
- package/src/tests/execute_validation.test.ts +781 -0
- package/src/tests/help.test.ts +60 -0
- package/src/tests/upload.test.ts +133 -0
package/src/runtime/errors.ts
CHANGED
|
@@ -8,12 +8,24 @@ export type CLITSTypeHint = {
|
|
|
8
8
|
file: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export type CLIDocHint = {
|
|
12
|
+
title: string
|
|
13
|
+
file: string
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export type CLIErrorDetails = {
|
|
12
17
|
operation?: string
|
|
13
18
|
requestType?: string
|
|
14
19
|
issues?: CLIValidationIssue[]
|
|
15
20
|
tsHints?: CLITSTypeHint[]
|
|
21
|
+
docHints?: CLIDocHint[]
|
|
16
22
|
reason?: string
|
|
23
|
+
hint?: string
|
|
24
|
+
endpoint?: string
|
|
25
|
+
method?: string
|
|
26
|
+
traceId?: string
|
|
27
|
+
upstreamError?: unknown
|
|
28
|
+
suggestions?: string[]
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export class CLIError extends Error {
|
package/src/runtime/execute.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { parseCLIArgs, hasFlag, flagValue } from './argv.js'
|
|
|
2
2
|
import { loadRuntimeEnv, type RuntimeEnv } from './env.js'
|
|
3
3
|
import { CLIError } from './errors.js'
|
|
4
4
|
import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http.js'
|
|
5
|
-
import { listModules, listModuleCommands, findCommand, type CommandScope } from './command_registry.js'
|
|
5
|
+
import { listModules, listModuleCommands, findCommand, findCommandSuggestions, type CommandScope } 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'
|
|
8
9
|
|
|
9
10
|
export type CommandExecutionSummary = {
|
|
10
11
|
scope: CommandScope
|
|
@@ -16,41 +17,88 @@ export type CommandExecutionSummary = {
|
|
|
16
17
|
scalarFlags: readonly string[]
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
type CustomUploadSummary = {
|
|
21
|
+
kind: 'upload'
|
|
22
|
+
status: number
|
|
23
|
+
data: ReturnType<typeof summarizeUploadResult>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarizeUploadResult(value: Awaited<ReturnType<typeof uploadLocalFile>>) {
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
export function renderRootHelp(scope: CommandScope): string {
|
|
20
31
|
const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
|
|
32
|
+
const items = listModules(scope).map((item) => ` - ${item}`)
|
|
21
33
|
return [
|
|
22
34
|
`${binary} <module> <command> [flags]`,
|
|
23
35
|
'',
|
|
36
|
+
...(scope === 'admin'
|
|
37
|
+
? ['use arcubase-admin for app, entity shell, and schema management', '']
|
|
38
|
+
: ['use arcubase for row CRUD, search, selection actions, and bulk actions', '']),
|
|
24
39
|
'modules:',
|
|
25
|
-
...
|
|
40
|
+
...items,
|
|
41
|
+
...(scope === 'user' ? ['','special commands:',' - upload <local-file>'] : []),
|
|
26
42
|
].join('\n')
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
|
|
30
46
|
const items = listModuleCommands(scope, moduleName)
|
|
31
47
|
if (items.length === 0) {
|
|
32
|
-
|
|
48
|
+
const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
49
|
+
throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
|
|
33
50
|
}
|
|
34
51
|
return [
|
|
35
52
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
|
|
36
53
|
'',
|
|
54
|
+
...(scope === 'admin'
|
|
55
|
+
? ['use arcubase-admin for app, entity shell, and schema management', '']
|
|
56
|
+
: ['use arcubase for row CRUD, search, selection actions, and bulk actions', '']),
|
|
37
57
|
'commands:',
|
|
38
58
|
...items.map((item) => ` - ${item.commandPath[1]}`),
|
|
39
59
|
].join('\n')
|
|
40
60
|
}
|
|
41
61
|
|
|
62
|
+
function buildUnknownCommandDetails(scope: CommandScope, moduleName: string, commandName: string): { suggestions?: string[]; reason?: string; hint?: string } | undefined {
|
|
63
|
+
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
64
|
+
if (scope === 'admin' && moduleName === 'workflow' && suggestions.some((item) => item.startsWith('arcubase workflow '))) {
|
|
65
|
+
return {
|
|
66
|
+
suggestions,
|
|
67
|
+
reason: `${moduleName} ${commandName} is a row operation, not an arcubase-admin command`,
|
|
68
|
+
hint: `use: arcubase ${moduleName} ${commandName}`,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return suggestions.length > 0 ? { suggestions } : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
42
74
|
export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName: string): string {
|
|
43
75
|
const command = findCommand(scope, moduleName, commandName)
|
|
44
76
|
if (!command) {
|
|
45
|
-
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
|
|
77
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
|
|
46
78
|
}
|
|
79
|
+
const docHints = [
|
|
80
|
+
...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
|
|
81
|
+
...(command.requestType ? getDocHints(command.requestType) : []),
|
|
82
|
+
].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index)
|
|
47
83
|
return [
|
|
48
84
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
|
|
49
85
|
`method: ${command.method}`,
|
|
50
86
|
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
51
87
|
command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
|
|
88
|
+
'query flags: --query-file',
|
|
52
89
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
|
|
53
90
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
|
|
91
|
+
...(scope === 'admin' && moduleName === 'entity' && commandName === 'admin-save-entity'
|
|
92
|
+
? [
|
|
93
|
+
'next step:',
|
|
94
|
+
' - switch to arcubase for row operations',
|
|
95
|
+
' - row insert: arcubase workflow insert-entity --app-id <app_id> --entity-id <entity_id> --body-file insert.json',
|
|
96
|
+
' - row query: arcubase workflow query-entity --app-id <app_id> --entity-id <entity_id> --body-file query.json',
|
|
97
|
+
]
|
|
98
|
+
: []),
|
|
99
|
+
...(docHints.length > 0
|
|
100
|
+
? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
|
|
101
|
+
: []),
|
|
54
102
|
].join('\n')
|
|
55
103
|
}
|
|
56
104
|
|
|
@@ -98,10 +146,250 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
|
|
|
98
146
|
return Object.keys(out).length > 0 ? out : undefined
|
|
99
147
|
}
|
|
100
148
|
|
|
149
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
150
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type EntityFieldInfo = {
|
|
154
|
+
id: number
|
|
155
|
+
type: string
|
|
156
|
+
label?: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractEntityFields(payload: unknown): EntityFieldInfo[] {
|
|
160
|
+
const candidate = isRecord(payload) && isRecord(payload.data) ? payload.data : payload
|
|
161
|
+
if (!isRecord(candidate) || !Array.isArray(candidate.fields)) {
|
|
162
|
+
return []
|
|
163
|
+
}
|
|
164
|
+
return candidate.fields
|
|
165
|
+
.filter((item): item is Record<string, unknown> => isRecord(item))
|
|
166
|
+
.filter((item) => typeof item.id === 'number' && typeof item.type === 'string')
|
|
167
|
+
.map((item) => ({
|
|
168
|
+
id: item.id as number,
|
|
169
|
+
type: item.type as string,
|
|
170
|
+
label: typeof item.label === 'string' ? item.label : undefined,
|
|
171
|
+
}))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function fetchEntityFields(
|
|
175
|
+
runtimeEnv: RuntimeEnv,
|
|
176
|
+
appId: string,
|
|
177
|
+
entityId: string,
|
|
178
|
+
fetchImpl: typeof fetch,
|
|
179
|
+
): Promise<Map<number, EntityFieldInfo>> {
|
|
180
|
+
const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
|
|
181
|
+
method: 'GET',
|
|
182
|
+
headers: buildRequestHeaders(runtimeEnv),
|
|
183
|
+
})
|
|
184
|
+
const raw = await response.text()
|
|
185
|
+
let parsedResponse: unknown = raw
|
|
186
|
+
try {
|
|
187
|
+
parsedResponse = raw ? JSON.parse(raw) : null
|
|
188
|
+
} catch {
|
|
189
|
+
parsedResponse = raw
|
|
190
|
+
}
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
|
|
193
|
+
}
|
|
194
|
+
if (isRecord(parsedResponse) && parsedResponse.error) {
|
|
195
|
+
const upstreamError = parsedResponse.error
|
|
196
|
+
const upstreamMessage =
|
|
197
|
+
isRecord(upstreamError) && typeof upstreamError.message === 'string'
|
|
198
|
+
? upstreamError.message
|
|
199
|
+
: typeof upstreamError === 'string'
|
|
200
|
+
? upstreamError
|
|
201
|
+
: JSON.stringify(upstreamError)
|
|
202
|
+
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
203
|
+
endpoint: `/entity/${appId}/${entityId}`,
|
|
204
|
+
method: 'GET',
|
|
205
|
+
upstreamError,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
const fields = extractEntityFields(parsedResponse)
|
|
209
|
+
return new Map(fields.map((field) => [field.id, field]))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function looksLikeLocalFilePath(value: string): boolean {
|
|
213
|
+
return (
|
|
214
|
+
value.startsWith('./') ||
|
|
215
|
+
value.startsWith('../') ||
|
|
216
|
+
value.startsWith('/') ||
|
|
217
|
+
value.startsWith('~/') ||
|
|
218
|
+
/\.(pdf|png|jpe?g|gif|webp|bmp|svg|heic|txt|csv|xlsx?|docx?|pptx?|zip)$/i.test(value)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isValidUploadEntry(value: unknown): boolean {
|
|
223
|
+
if (!isRecord(value)) {
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
226
|
+
const hasUploadId = typeof value.upload_id === 'number' || typeof value.upload_id === 'string'
|
|
227
|
+
const hasAssetsId = typeof value.assets_id === 'number' || typeof value.assets_id === 'string'
|
|
228
|
+
return (hasUploadId || hasAssetsId) && typeof value.file === 'string' && typeof value.file_name === 'string'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function validateFileFieldPayloads(
|
|
232
|
+
scope: CommandScope,
|
|
233
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
234
|
+
runtimeEnv: RuntimeEnv,
|
|
235
|
+
flags: Record<string, string | boolean>,
|
|
236
|
+
body: unknown,
|
|
237
|
+
fetchImpl: typeof fetch,
|
|
238
|
+
) {
|
|
239
|
+
if (!isRecord(body)) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
const container = command.functionName === 'updateEntityRow' ? body.data : body.fields
|
|
243
|
+
if (!isRecord(container)) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
const appId = flagValue(flags, 'app-id')
|
|
247
|
+
const entityId = flagValue(flags, 'entity-id')
|
|
248
|
+
if (!appId || !entityId) {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
const fieldsById = await fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl)
|
|
252
|
+
for (const [fieldIdText, value] of Object.entries(container)) {
|
|
253
|
+
if (!/^\d+$/.test(fieldIdText)) {
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
const fieldId = Number(fieldIdText)
|
|
257
|
+
const field = fieldsById.get(fieldId)
|
|
258
|
+
if (!field || (field.type !== 'file' && field.type !== 'image')) {
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
const fieldPath = command.functionName === 'updateEntityRow' ? `body.data.${fieldId}` : `body.fields.${fieldId}`
|
|
262
|
+
const fieldName = field.label ? `${field.label} (${field.type})` : `${field.type} field ${fieldId}`
|
|
263
|
+
if (typeof value === 'string') {
|
|
264
|
+
const guidance = looksLikeLocalFilePath(value)
|
|
265
|
+
? `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`
|
|
266
|
+
: `${fieldName} must be an array returned by "arcubase upload <local-file>", not a string`
|
|
267
|
+
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
268
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
269
|
+
requestType: command.requestType ?? undefined,
|
|
270
|
+
issues: [{ path: fieldPath, message: guidance }],
|
|
271
|
+
docHints: [
|
|
272
|
+
{ title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
|
|
273
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
274
|
+
],
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
if (isRecord(value)) {
|
|
278
|
+
const guidance = `${fieldName} must be an array. Run "arcubase upload <local-file>" and use the returned data array directly as the field value`
|
|
279
|
+
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
280
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
281
|
+
requestType: command.requestType ?? undefined,
|
|
282
|
+
issues: [{ path: fieldPath, message: guidance }],
|
|
283
|
+
docHints: [
|
|
284
|
+
{ title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
|
|
285
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
286
|
+
],
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
if (!Array.isArray(value)) {
|
|
290
|
+
const guidance = `${fieldName} must be an array returned by "arcubase upload <local-file>"`
|
|
291
|
+
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
292
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
293
|
+
requestType: command.requestType ?? undefined,
|
|
294
|
+
issues: [{ path: fieldPath, message: guidance }],
|
|
295
|
+
docHints: [
|
|
296
|
+
{ title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
|
|
297
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
298
|
+
],
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
for (const [index, item] of value.entries()) {
|
|
302
|
+
if (!isValidUploadEntry(item)) {
|
|
303
|
+
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`
|
|
304
|
+
throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
|
|
305
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
306
|
+
requestType: command.requestType ?? undefined,
|
|
307
|
+
issues: [{ path: `${fieldPath}.${index}`, message: guidance }],
|
|
308
|
+
docHints: [
|
|
309
|
+
{ title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
|
|
310
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
311
|
+
],
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function throwBodyValidationFailure(message: string, requestType: string, path: string, scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>) {
|
|
319
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
320
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
321
|
+
requestType,
|
|
322
|
+
issues: [
|
|
323
|
+
{
|
|
324
|
+
path,
|
|
325
|
+
message,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
tsHints: getTypeHints(requestType),
|
|
329
|
+
docHints: getDocHints(requestType),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateActionSpecificBody(
|
|
334
|
+
scope: CommandScope,
|
|
335
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
336
|
+
flags: Record<string, string | boolean>,
|
|
337
|
+
body: unknown,
|
|
338
|
+
) {
|
|
339
|
+
if (!isRecord(body) || !command.requestType) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (command.module === 'workflow' && command.functionName === 'queryEntitySelection') {
|
|
344
|
+
const action = flagValue(flags, 'action')
|
|
345
|
+
const selection = body.selection
|
|
346
|
+
if (!isRecord(selection) || typeof selection.type !== 'string') {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const type = selection.type
|
|
351
|
+
const condition = isRecord(selection.condition) ? selection.condition : undefined
|
|
352
|
+
const conditionKeys = condition ? Object.keys(condition).filter((key) => key !== 'selectAll') : []
|
|
353
|
+
|
|
354
|
+
if (action === 'query') {
|
|
355
|
+
if (type === 'condition') {
|
|
356
|
+
throwBodyValidationFailure(
|
|
357
|
+
'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',
|
|
358
|
+
command.requestType,
|
|
359
|
+
'body.selection.type',
|
|
360
|
+
scope,
|
|
361
|
+
command,
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if ((action === 'archive' || action === 'delete' || action === 'batch_print') && type === 'all') {
|
|
368
|
+
throwBodyValidationFailure(
|
|
369
|
+
`selection.type=all is not supported for action=${action}; use ids or condition`,
|
|
370
|
+
command.requestType,
|
|
371
|
+
'body.selection.type',
|
|
372
|
+
scope,
|
|
373
|
+
command,
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if ((action === 'archive' || action === 'delete') && type === 'condition' && conditionKeys.length === 0) {
|
|
378
|
+
throwBodyValidationFailure(
|
|
379
|
+
`selection.condition for action=${action} requires at least one q, tab, or filter_* key in addition to selectAll`,
|
|
380
|
+
command.requestType,
|
|
381
|
+
'body.selection.condition',
|
|
382
|
+
scope,
|
|
383
|
+
command,
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
101
389
|
export function summarizeCommand(scope: CommandScope, moduleName: string, commandName: string): CommandExecutionSummary {
|
|
102
390
|
const command = findCommand(scope, moduleName, commandName)
|
|
103
391
|
if (!command) {
|
|
104
|
-
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
|
|
392
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
|
|
105
393
|
}
|
|
106
394
|
return {
|
|
107
395
|
scope,
|
|
@@ -114,8 +402,33 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
|
|
|
114
402
|
}
|
|
115
403
|
}
|
|
116
404
|
|
|
117
|
-
export async function executeCLI(scope: CommandScope, argv: string[], env
|
|
405
|
+
export async function executeCLI(scope: CommandScope, argv: string[], env?: RuntimeEnv, fetchImpl: typeof fetch = fetch): Promise<any> {
|
|
118
406
|
const parsed = parseCLIArgs(argv)
|
|
407
|
+
if (scope === 'user' && parsed.commandTokens[0] === 'upload') {
|
|
408
|
+
if (hasFlag(parsed.flags, 'help') || parsed.commandTokens.length === 1) {
|
|
409
|
+
return { kind: 'help', text: renderUploadHelp() }
|
|
410
|
+
}
|
|
411
|
+
const sourcePath = parsed.commandTokens[1]
|
|
412
|
+
if (!sourcePath) {
|
|
413
|
+
throw new CLIError('UPLOAD_SOURCE_REQUIRED', 'upload requires a local file path', 2)
|
|
414
|
+
}
|
|
415
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope)
|
|
416
|
+
const data = await uploadLocalFile(
|
|
417
|
+
runtimeEnv,
|
|
418
|
+
sourcePath,
|
|
419
|
+
{
|
|
420
|
+
filename: flagValue(parsed.flags, 'filename'),
|
|
421
|
+
global: hasFlag(parsed.flags, 'global'),
|
|
422
|
+
},
|
|
423
|
+
fetchImpl,
|
|
424
|
+
)
|
|
425
|
+
return {
|
|
426
|
+
kind: 'result',
|
|
427
|
+
commandPath: ['upload', sourcePath],
|
|
428
|
+
status: 200,
|
|
429
|
+
data,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
119
432
|
if (parsed.commandTokens.length === 0) {
|
|
120
433
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
121
434
|
}
|
|
@@ -130,9 +443,11 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
130
443
|
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) }
|
|
131
444
|
}
|
|
132
445
|
|
|
446
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope)
|
|
447
|
+
|
|
133
448
|
const command = findCommand(scope, moduleName, commandName)
|
|
134
449
|
if (!command) {
|
|
135
|
-
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
|
|
450
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
|
|
136
451
|
}
|
|
137
452
|
|
|
138
453
|
const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags))
|
|
@@ -140,6 +455,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
140
455
|
const fileQuery = loadQueryFromFlags(parsed.flags)
|
|
141
456
|
const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
|
|
142
457
|
const body = loadBodyFromFlags(parsed.flags)
|
|
458
|
+
let validatedBody = body
|
|
143
459
|
|
|
144
460
|
if (command.requestType) {
|
|
145
461
|
if (body === undefined) {
|
|
@@ -161,9 +477,12 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
161
477
|
message: issue.message,
|
|
162
478
|
})),
|
|
163
479
|
tsHints: getTypeHints(command.requestType),
|
|
480
|
+
docHints: getDocHints(command.requestType),
|
|
164
481
|
}
|
|
165
482
|
)
|
|
166
483
|
}
|
|
484
|
+
validatedBody = parsedBody.data
|
|
485
|
+
validateActionSpecificBody(scope, command, parsed.flags, validatedBody)
|
|
167
486
|
} else {
|
|
168
487
|
const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType)
|
|
169
488
|
if (unsupportedReason) {
|
|
@@ -172,19 +491,23 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
172
491
|
requestType: command.requestType,
|
|
173
492
|
reason: unsupportedReason,
|
|
174
493
|
tsHints: getTypeHints(command.requestType),
|
|
494
|
+
docHints: getDocHints(command.requestType),
|
|
175
495
|
})
|
|
176
496
|
}
|
|
177
497
|
}
|
|
178
498
|
}
|
|
179
499
|
|
|
180
|
-
|
|
181
|
-
|
|
500
|
+
if (scope === 'user' && (command.functionName === 'insertEntity' || command.functionName === 'updateEntityRow')) {
|
|
501
|
+
await validateFileFieldPayloads(scope, command, runtimeEnv, parsed.flags, validatedBody, fetchImpl)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
|
|
182
505
|
method: command.method,
|
|
183
506
|
headers: {
|
|
184
|
-
...
|
|
507
|
+
...buildRequestHeaders(runtimeEnv),
|
|
185
508
|
'Content-Type': 'application/json',
|
|
186
509
|
},
|
|
187
|
-
body:
|
|
510
|
+
body: validatedBody === undefined || command.method === 'GET' ? undefined : JSON.stringify(validatedBody),
|
|
188
511
|
})
|
|
189
512
|
|
|
190
513
|
const raw = await response.text()
|
|
@@ -197,6 +520,38 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
197
520
|
if (!response.ok) {
|
|
198
521
|
throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
|
|
199
522
|
}
|
|
523
|
+
if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
|
|
524
|
+
const upstreamError = parsedResponse.error
|
|
525
|
+
const upstreamMessage =
|
|
526
|
+
typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
|
|
527
|
+
? upstreamError.message
|
|
528
|
+
: typeof upstreamError === 'string'
|
|
529
|
+
? upstreamError
|
|
530
|
+
: JSON.stringify(upstreamError)
|
|
531
|
+
const traceId =
|
|
532
|
+
typeof parsedResponse.trace_id === 'string'
|
|
533
|
+
? parsedResponse.trace_id
|
|
534
|
+
: typeof parsedResponse.request_id === 'string'
|
|
535
|
+
? parsedResponse.request_id
|
|
536
|
+
: undefined
|
|
537
|
+
const uploadBindFailure = upstreamMessage.includes('failed to bind upload to assets')
|
|
538
|
+
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
539
|
+
endpoint,
|
|
540
|
+
method: command.method,
|
|
541
|
+
traceId,
|
|
542
|
+
upstreamError,
|
|
543
|
+
...(uploadBindFailure
|
|
544
|
+
? {
|
|
545
|
+
reason: 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record',
|
|
546
|
+
hint: 'verify the upload storage host is reachable from the Arcubase server, then rerun arcubase upload and retry the row insert/update',
|
|
547
|
+
docHints: [
|
|
548
|
+
{ title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
|
|
549
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
550
|
+
],
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
553
|
+
})
|
|
554
|
+
}
|
|
200
555
|
return {
|
|
201
556
|
kind: 'result',
|
|
202
557
|
commandPath: command.commandPath,
|