@carthooks/arcubase-cli 0.1.2 → 0.1.3
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 +907 -10
- package/bundle/arcubase.mjs +907 -10
- 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 +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +89 -10
- 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 +133 -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 +450 -0
- package/dist/tests/help.test.js +33 -0
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +177 -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 +44 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +43 -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 +164 -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 +186 -0
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +123 -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 +11 -0
- package/src/runtime/execute.ts +123 -10
- package/src/runtime/zod_registry.ts +154 -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 +598 -0
- package/src/tests/help.test.ts +39 -0
package/src/runtime/errors.ts
CHANGED
|
@@ -8,12 +8,23 @@ 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
|
+
endpoint?: string
|
|
24
|
+
method?: string
|
|
25
|
+
traceId?: string
|
|
26
|
+
upstreamError?: unknown
|
|
27
|
+
suggestions?: string[]
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export class CLIError extends Error {
|
package/src/runtime/execute.ts
CHANGED
|
@@ -2,9 +2,9 @@ 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
8
|
|
|
9
9
|
export type CommandExecutionSummary = {
|
|
10
10
|
scope: CommandScope
|
|
@@ -29,7 +29,8 @@ export function renderRootHelp(scope: CommandScope): string {
|
|
|
29
29
|
export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
|
|
30
30
|
const items = listModuleCommands(scope, moduleName)
|
|
31
31
|
if (items.length === 0) {
|
|
32
|
-
|
|
32
|
+
const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
33
|
+
throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
|
|
33
34
|
}
|
|
34
35
|
return [
|
|
35
36
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
|
|
@@ -42,15 +43,24 @@ export function renderModuleHelp(scope: CommandScope, moduleName: string): strin
|
|
|
42
43
|
export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName: string): string {
|
|
43
44
|
const command = findCommand(scope, moduleName, commandName)
|
|
44
45
|
if (!command) {
|
|
45
|
-
|
|
46
|
+
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
47
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
|
|
46
48
|
}
|
|
49
|
+
const docHints = [
|
|
50
|
+
...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
|
|
51
|
+
...(command.requestType ? getDocHints(command.requestType) : []),
|
|
52
|
+
].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index)
|
|
47
53
|
return [
|
|
48
54
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
|
|
49
55
|
`method: ${command.method}`,
|
|
50
56
|
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
51
57
|
command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
|
|
58
|
+
'query flags: --query-file',
|
|
52
59
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
|
|
53
60
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
|
|
61
|
+
...(docHints.length > 0
|
|
62
|
+
? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
|
|
63
|
+
: []),
|
|
54
64
|
].join('\n')
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -98,10 +108,86 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
|
|
|
98
108
|
return Object.keys(out).length > 0 ? out : undefined
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
112
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function throwBodyValidationFailure(message: string, requestType: string, path: string, scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>) {
|
|
116
|
+
throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
|
|
117
|
+
operation: `${scope}.${command.module}.${command.functionName}`,
|
|
118
|
+
requestType,
|
|
119
|
+
issues: [
|
|
120
|
+
{
|
|
121
|
+
path,
|
|
122
|
+
message,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
tsHints: getTypeHints(requestType),
|
|
126
|
+
docHints: getDocHints(requestType),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateActionSpecificBody(
|
|
131
|
+
scope: CommandScope,
|
|
132
|
+
command: NonNullable<ReturnType<typeof findCommand>>,
|
|
133
|
+
flags: Record<string, string | boolean>,
|
|
134
|
+
body: unknown,
|
|
135
|
+
) {
|
|
136
|
+
if (!isRecord(body) || !command.requestType) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command.module === 'workflow' && command.functionName === 'queryEntitySelection') {
|
|
141
|
+
const action = flagValue(flags, 'action')
|
|
142
|
+
const selection = body.selection
|
|
143
|
+
if (!isRecord(selection) || typeof selection.type !== 'string') {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const type = selection.type
|
|
148
|
+
const condition = isRecord(selection.condition) ? selection.condition : undefined
|
|
149
|
+
const conditionKeys = condition ? Object.keys(condition).filter((key) => key !== 'selectAll') : []
|
|
150
|
+
|
|
151
|
+
if (action === 'query') {
|
|
152
|
+
if (type === 'condition') {
|
|
153
|
+
throwBodyValidationFailure(
|
|
154
|
+
'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',
|
|
155
|
+
command.requestType,
|
|
156
|
+
'body.selection.type',
|
|
157
|
+
scope,
|
|
158
|
+
command,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ((action === 'archive' || action === 'delete' || action === 'batch_print') && type === 'all') {
|
|
165
|
+
throwBodyValidationFailure(
|
|
166
|
+
`selection.type=all is not supported for action=${action}; use ids or condition`,
|
|
167
|
+
command.requestType,
|
|
168
|
+
'body.selection.type',
|
|
169
|
+
scope,
|
|
170
|
+
command,
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ((action === 'archive' || action === 'delete') && type === 'condition' && conditionKeys.length === 0) {
|
|
175
|
+
throwBodyValidationFailure(
|
|
176
|
+
`selection.condition for action=${action} requires at least one q, tab, or filter_* key in addition to selectAll`,
|
|
177
|
+
command.requestType,
|
|
178
|
+
'body.selection.condition',
|
|
179
|
+
scope,
|
|
180
|
+
command,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
101
186
|
export function summarizeCommand(scope: CommandScope, moduleName: string, commandName: string): CommandExecutionSummary {
|
|
102
187
|
const command = findCommand(scope, moduleName, commandName)
|
|
103
188
|
if (!command) {
|
|
104
|
-
|
|
189
|
+
const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
|
|
190
|
+
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
|
|
105
191
|
}
|
|
106
192
|
return {
|
|
107
193
|
scope,
|
|
@@ -114,7 +200,7 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
|
|
|
114
200
|
}
|
|
115
201
|
}
|
|
116
202
|
|
|
117
|
-
export async function executeCLI(scope: CommandScope, argv: string[], env
|
|
203
|
+
export async function executeCLI(scope: CommandScope, argv: string[], env?: RuntimeEnv, fetchImpl: typeof fetch = fetch): Promise<any> {
|
|
118
204
|
const parsed = parseCLIArgs(argv)
|
|
119
205
|
if (parsed.commandTokens.length === 0) {
|
|
120
206
|
return { kind: 'help', text: renderRootHelp(scope) }
|
|
@@ -130,6 +216,8 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
130
216
|
return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) }
|
|
131
217
|
}
|
|
132
218
|
|
|
219
|
+
const runtimeEnv = env ?? loadRuntimeEnv(scope)
|
|
220
|
+
|
|
133
221
|
const command = findCommand(scope, moduleName, commandName)
|
|
134
222
|
if (!command) {
|
|
135
223
|
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
|
|
@@ -140,6 +228,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
140
228
|
const fileQuery = loadQueryFromFlags(parsed.flags)
|
|
141
229
|
const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
|
|
142
230
|
const body = loadBodyFromFlags(parsed.flags)
|
|
231
|
+
let validatedBody = body
|
|
143
232
|
|
|
144
233
|
if (command.requestType) {
|
|
145
234
|
if (body === undefined) {
|
|
@@ -161,9 +250,12 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
161
250
|
message: issue.message,
|
|
162
251
|
})),
|
|
163
252
|
tsHints: getTypeHints(command.requestType),
|
|
253
|
+
docHints: getDocHints(command.requestType),
|
|
164
254
|
}
|
|
165
255
|
)
|
|
166
256
|
}
|
|
257
|
+
validatedBody = parsedBody.data
|
|
258
|
+
validateActionSpecificBody(scope, command, parsed.flags, validatedBody)
|
|
167
259
|
} else {
|
|
168
260
|
const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType)
|
|
169
261
|
if (unsupportedReason) {
|
|
@@ -172,19 +264,19 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
172
264
|
requestType: command.requestType,
|
|
173
265
|
reason: unsupportedReason,
|
|
174
266
|
tsHints: getTypeHints(command.requestType),
|
|
267
|
+
docHints: getDocHints(command.requestType),
|
|
175
268
|
})
|
|
176
269
|
}
|
|
177
270
|
}
|
|
178
271
|
}
|
|
179
272
|
|
|
180
|
-
const
|
|
181
|
-
const response = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
|
|
273
|
+
const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
|
|
182
274
|
method: command.method,
|
|
183
275
|
headers: {
|
|
184
|
-
...
|
|
276
|
+
...buildRequestHeaders(runtimeEnv),
|
|
185
277
|
'Content-Type': 'application/json',
|
|
186
278
|
},
|
|
187
|
-
body:
|
|
279
|
+
body: validatedBody === undefined || command.method === 'GET' ? undefined : JSON.stringify(validatedBody),
|
|
188
280
|
})
|
|
189
281
|
|
|
190
282
|
const raw = await response.text()
|
|
@@ -197,6 +289,27 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
197
289
|
if (!response.ok) {
|
|
198
290
|
throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
|
|
199
291
|
}
|
|
292
|
+
if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
|
|
293
|
+
const upstreamError = parsedResponse.error
|
|
294
|
+
const upstreamMessage =
|
|
295
|
+
typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
|
|
296
|
+
? upstreamError.message
|
|
297
|
+
: typeof upstreamError === 'string'
|
|
298
|
+
? upstreamError
|
|
299
|
+
: JSON.stringify(upstreamError)
|
|
300
|
+
const traceId =
|
|
301
|
+
typeof parsedResponse.trace_id === 'string'
|
|
302
|
+
? parsedResponse.trace_id
|
|
303
|
+
: typeof parsedResponse.request_id === 'string'
|
|
304
|
+
? parsedResponse.request_id
|
|
305
|
+
: undefined
|
|
306
|
+
throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
|
|
307
|
+
endpoint,
|
|
308
|
+
method: command.method,
|
|
309
|
+
traceId,
|
|
310
|
+
upstreamError,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
200
313
|
return {
|
|
201
314
|
kind: 'result',
|
|
202
315
|
commandPath: command.commandPath,
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import type { ZodTypeAny } from 'zod'
|
|
2
2
|
import { bodySchemas, unsupportedBodySchemas } from '../generated/zod_registry.generated.js'
|
|
3
3
|
import { typeIndex } from '../generated/type_index.generated.js'
|
|
4
|
+
import {
|
|
5
|
+
AppCreateByTenantsReqVOSchemaOverride,
|
|
6
|
+
EntityBulkUpdateReqVOSchemaOverride,
|
|
7
|
+
EntityInvokeBatchOperatorReqVOSchemaOverride,
|
|
8
|
+
EntityQueryReqVOSchemaOverride,
|
|
9
|
+
EntitySaveReqVOSchemaOverride,
|
|
10
|
+
EntitySelectionActionReqVOSchemaOverride,
|
|
11
|
+
EntityTagsBatchReqVOSchemaOverride,
|
|
12
|
+
EntityTagsSelectionReqVOSchemaOverride,
|
|
13
|
+
EntityUpdateReqVOSchemaOverride,
|
|
14
|
+
InsertReqVOSchemaOverride,
|
|
15
|
+
} from './entity_save_schema.js'
|
|
16
|
+
|
|
17
|
+
const schemaOverrides: Record<string, ZodTypeAny> = {
|
|
18
|
+
AppCreateByTenantsReqVO: AppCreateByTenantsReqVOSchemaOverride,
|
|
19
|
+
EntityBulkUpdateReqVO: EntityBulkUpdateReqVOSchemaOverride,
|
|
20
|
+
'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': EntityInvokeBatchOperatorReqVOSchemaOverride,
|
|
21
|
+
EntityQueryReqVO: EntityQueryReqVOSchemaOverride,
|
|
22
|
+
EntitySaveReqVO: EntitySaveReqVOSchemaOverride,
|
|
23
|
+
EntitySelectionActionReqVO: EntitySelectionActionReqVOSchemaOverride,
|
|
24
|
+
'{ policy_id: string selection: any }': EntityTagsSelectionReqVOSchemaOverride,
|
|
25
|
+
'{ policy_id: string selection: any tag_names: string[] }': EntityTagsBatchReqVOSchemaOverride,
|
|
26
|
+
EntityUpdateReqVO: EntityUpdateReqVOSchemaOverride,
|
|
27
|
+
InsertReqVO: InsertReqVOSchemaOverride,
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
export function getBodySchema(typeName: string): ZodTypeAny | null {
|
|
6
31
|
const name = String(typeName || '').trim()
|
|
7
32
|
if (!name) return null
|
|
33
|
+
if (schemaOverrides[name]) {
|
|
34
|
+
return schemaOverrides[name]
|
|
35
|
+
}
|
|
8
36
|
return (bodySchemas as Record<string, ZodTypeAny | undefined>)[name] ?? null
|
|
9
37
|
}
|
|
10
38
|
|
|
@@ -29,6 +57,68 @@ export type RuntimeTypeHint = {
|
|
|
29
57
|
file: string
|
|
30
58
|
}
|
|
31
59
|
|
|
60
|
+
export type RuntimeDocHint = {
|
|
61
|
+
title: string
|
|
62
|
+
file: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const examplesIndexDocHint: RuntimeDocHint = {
|
|
66
|
+
title: 'Examples index',
|
|
67
|
+
file: '/opt/arcubase-sdk/docs/runtime-reference/examples/README.md',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const omsExampleDocHint: RuntimeDocHint = {
|
|
71
|
+
title: 'OMS example 01',
|
|
72
|
+
file: '/opt/arcubase-sdk/docs/runtime-reference/examples/oms-01/README.md',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const docHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
76
|
+
AppCreateByTenantsReqVO: [
|
|
77
|
+
{ title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
|
|
78
|
+
examplesIndexDocHint,
|
|
79
|
+
omsExampleDocHint,
|
|
80
|
+
],
|
|
81
|
+
AppCreateEntityReqVO: [
|
|
82
|
+
{ title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
|
|
83
|
+
examplesIndexDocHint,
|
|
84
|
+
omsExampleDocHint,
|
|
85
|
+
],
|
|
86
|
+
EntitySaveReqVO: [
|
|
87
|
+
{ title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
|
|
88
|
+
omsExampleDocHint,
|
|
89
|
+
],
|
|
90
|
+
EntityQueryReqVO: [
|
|
91
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
92
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
93
|
+
omsExampleDocHint,
|
|
94
|
+
],
|
|
95
|
+
EntityUpdateReqVO: [
|
|
96
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
97
|
+
omsExampleDocHint,
|
|
98
|
+
],
|
|
99
|
+
EntitySelectionActionReqVO: [
|
|
100
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
101
|
+
omsExampleDocHint,
|
|
102
|
+
],
|
|
103
|
+
InsertReqVO: [
|
|
104
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
105
|
+
omsExampleDocHint,
|
|
106
|
+
],
|
|
107
|
+
EntityBulkUpdateReqVO: [
|
|
108
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
109
|
+
omsExampleDocHint,
|
|
110
|
+
],
|
|
111
|
+
'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
|
|
112
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
113
|
+
],
|
|
114
|
+
'{ policy_id: string selection: any }': [
|
|
115
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
116
|
+
],
|
|
117
|
+
'{ policy_id: string selection: any tag_names: string[] }': [
|
|
118
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
32
122
|
export function getTypeHints(typeName: string): RuntimeTypeHint[] {
|
|
33
123
|
const name = String(typeName || '').trim()
|
|
34
124
|
if (!name) return []
|
|
@@ -51,3 +141,67 @@ export function getTypeHints(typeName: string): RuntimeTypeHint[] {
|
|
|
51
141
|
}
|
|
52
142
|
return out
|
|
53
143
|
}
|
|
144
|
+
|
|
145
|
+
export function getDocHints(typeName: string): RuntimeDocHint[] {
|
|
146
|
+
const name = String(typeName || '').trim()
|
|
147
|
+
if (!name) return []
|
|
148
|
+
return docHintIndex[name] ?? []
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
|
|
152
|
+
'admin.app.createAppByTenants': [
|
|
153
|
+
{ title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
|
|
154
|
+
examplesIndexDocHint,
|
|
155
|
+
omsExampleDocHint,
|
|
156
|
+
],
|
|
157
|
+
'admin.app.createEntity': [
|
|
158
|
+
{ title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
|
|
159
|
+
omsExampleDocHint,
|
|
160
|
+
],
|
|
161
|
+
'admin.entity.adminGetEntityInfo': [
|
|
162
|
+
{ title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
|
|
163
|
+
omsExampleDocHint,
|
|
164
|
+
],
|
|
165
|
+
'admin.entity.adminSaveEntity': [
|
|
166
|
+
{ title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
|
|
167
|
+
omsExampleDocHint,
|
|
168
|
+
],
|
|
169
|
+
'user.workflow.insertEntity': [
|
|
170
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
171
|
+
omsExampleDocHint,
|
|
172
|
+
],
|
|
173
|
+
'user.workflow.queryEntity': [
|
|
174
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
175
|
+
omsExampleDocHint,
|
|
176
|
+
],
|
|
177
|
+
'user.workflow.getEntityRow': [
|
|
178
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
179
|
+
omsExampleDocHint,
|
|
180
|
+
],
|
|
181
|
+
'user.workflow.updateEntityRow': [
|
|
182
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
183
|
+
omsExampleDocHint,
|
|
184
|
+
],
|
|
185
|
+
'user.workflow.queryEntitySelection': [
|
|
186
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
187
|
+
omsExampleDocHint,
|
|
188
|
+
],
|
|
189
|
+
'user.workflow.deleteEntityRow': [
|
|
190
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
191
|
+
omsExampleDocHint,
|
|
192
|
+
],
|
|
193
|
+
'user.workflow.restoreEntityRow': [
|
|
194
|
+
{ title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
|
|
195
|
+
omsExampleDocHint,
|
|
196
|
+
],
|
|
197
|
+
'user.entity.updateEntityBulk': [
|
|
198
|
+
{ title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
|
|
199
|
+
omsExampleDocHint,
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getCommandDocHints(operation: string): RuntimeDocHint[] {
|
|
204
|
+
const key = String(operation || '').trim()
|
|
205
|
+
if (!key) return []
|
|
206
|
+
return commandDocHintIndex[key] ?? []
|
|
207
|
+
}
|
|
@@ -2,6 +2,7 @@ import test from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
|
|
4
4
|
import { adminCommands, userCommands } from '../generated/command_registry.generated.js'
|
|
5
|
+
import { findCommandSuggestions } from '../runtime/command_registry.js'
|
|
5
6
|
|
|
6
7
|
test('admin app create-entity command exists', () => {
|
|
7
8
|
assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app create-entity'))
|
|
@@ -25,3 +26,8 @@ test('tag commands no longer emit truncated inline request type', () => {
|
|
|
25
26
|
assert.match(String(typed.requestType), /^\{\s*policy_id:\s*string.*selection:\s*any.*tag_names:\s*string\[\]\s*\}$/)
|
|
26
27
|
assert.equal(typed.scalarParams.length, 0)
|
|
27
28
|
})
|
|
29
|
+
|
|
30
|
+
test('suggestions can point wrong admin delete guess to user workflow delete command', () => {
|
|
31
|
+
const suggestions = findCommandSuggestions('entity', 'admin-delete-entity-row')
|
|
32
|
+
assert.ok(suggestions.some((item) => item.binary === 'arcubase' && item.moduleName === 'workflow' && item.commandName === 'delete-entity-row'))
|
|
33
|
+
})
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getBodySchema } from '../runtime/zod_registry.js'
|
|
4
|
+
|
|
5
|
+
const schema = getBodySchema('EntitySaveReqVO')
|
|
6
|
+
|
|
7
|
+
function buildMinimalField(id = 1001) {
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
label: '订单号',
|
|
11
|
+
type: 'text',
|
|
12
|
+
required: true,
|
|
13
|
+
unique: false,
|
|
14
|
+
editable: true,
|
|
15
|
+
visible: true,
|
|
16
|
+
show_label: true,
|
|
17
|
+
scannable: false,
|
|
18
|
+
default_value_mode: 0,
|
|
19
|
+
description: '',
|
|
20
|
+
value: null,
|
|
21
|
+
number_decimal: 0,
|
|
22
|
+
depends: [],
|
|
23
|
+
key: 'order_id',
|
|
24
|
+
code_index: false,
|
|
25
|
+
options: {},
|
|
26
|
+
transfers: null,
|
|
27
|
+
children: null,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('EntitySaveReqVO accepts fields as array', () => {
|
|
32
|
+
assert.ok(schema)
|
|
33
|
+
const parsed = schema!.safeParse({
|
|
34
|
+
id: 2188889845,
|
|
35
|
+
app_id: 2188889844,
|
|
36
|
+
name: '订单',
|
|
37
|
+
schema_version: 1,
|
|
38
|
+
field_id_seq: 1002,
|
|
39
|
+
layout: [[1001]],
|
|
40
|
+
fields: [buildMinimalField()],
|
|
41
|
+
workflow_enabled: false,
|
|
42
|
+
})
|
|
43
|
+
assert.equal(parsed.success, true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('EntitySaveReqVO rejects fields as object', () => {
|
|
47
|
+
assert.ok(schema)
|
|
48
|
+
const parsed = schema!.safeParse({
|
|
49
|
+
name: '订单',
|
|
50
|
+
fields: {
|
|
51
|
+
order_id: {
|
|
52
|
+
id: 1001,
|
|
53
|
+
label: '订单号',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
assert.equal(parsed.success, false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('EntitySaveReqVO rejects field_id_seq not greater than max field id', () => {
|
|
61
|
+
assert.ok(schema)
|
|
62
|
+
const parsed = schema!.safeParse({
|
|
63
|
+
field_id_seq: 1001,
|
|
64
|
+
layout: [[1001]],
|
|
65
|
+
fields: [buildMinimalField(1001)],
|
|
66
|
+
})
|
|
67
|
+
assert.equal(parsed.success, false)
|
|
68
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join('.') === 'field_id_seq'))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('EntitySaveReqVO rejects layout unknown top-level field id', () => {
|
|
72
|
+
assert.ok(schema)
|
|
73
|
+
const parsed = schema!.safeParse({
|
|
74
|
+
field_id_seq: 1002,
|
|
75
|
+
layout: [[9999]],
|
|
76
|
+
fields: [buildMinimalField(1001)],
|
|
77
|
+
})
|
|
78
|
+
assert.equal(parsed.success, false)
|
|
79
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join('.') === 'layout.0.0'))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('EntitySaveReqVO rejects non-subform field children', () => {
|
|
83
|
+
assert.ok(schema)
|
|
84
|
+
const parsed = schema!.safeParse({
|
|
85
|
+
field_id_seq: 1003,
|
|
86
|
+
layout: [[1001]],
|
|
87
|
+
fields: [{
|
|
88
|
+
...buildMinimalField(1001),
|
|
89
|
+
children: [buildMinimalField(1002)],
|
|
90
|
+
}],
|
|
91
|
+
})
|
|
92
|
+
assert.equal(parsed.success, false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('EntitySaveReqVO requires status options items', () => {
|
|
96
|
+
assert.ok(schema)
|
|
97
|
+
const parsed = schema!.safeParse({
|
|
98
|
+
field_id_seq: 1002,
|
|
99
|
+
layout: [[1001]],
|
|
100
|
+
fields: [{
|
|
101
|
+
...buildMinimalField(1001),
|
|
102
|
+
type: 'status',
|
|
103
|
+
options: {},
|
|
104
|
+
}],
|
|
105
|
+
})
|
|
106
|
+
assert.equal(parsed.success, false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('EntitySaveReqVO requires serialnumber parts', () => {
|
|
110
|
+
assert.ok(schema)
|
|
111
|
+
const parsed = schema!.safeParse({
|
|
112
|
+
field_id_seq: 1002,
|
|
113
|
+
layout: [[1001]],
|
|
114
|
+
fields: [{
|
|
115
|
+
...buildMinimalField(1001),
|
|
116
|
+
type: 'serialnumber',
|
|
117
|
+
options: {},
|
|
118
|
+
}],
|
|
119
|
+
})
|
|
120
|
+
assert.equal(parsed.success, false)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('EntitySaveReqVO requires lookup relation structure', () => {
|
|
124
|
+
assert.ok(schema)
|
|
125
|
+
const parsed = schema!.safeParse({
|
|
126
|
+
field_id_seq: 1002,
|
|
127
|
+
layout: [[1001]],
|
|
128
|
+
fields: [{
|
|
129
|
+
...buildMinimalField(1001),
|
|
130
|
+
type: 'lookup',
|
|
131
|
+
options: {},
|
|
132
|
+
}],
|
|
133
|
+
})
|
|
134
|
+
assert.equal(parsed.success, false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('EntitySaveReqVO requires relationfield relation app and entity ids', () => {
|
|
138
|
+
assert.ok(schema)
|
|
139
|
+
const parsed = schema!.safeParse({
|
|
140
|
+
field_id_seq: 1002,
|
|
141
|
+
layout: [[1001]],
|
|
142
|
+
fields: [{
|
|
143
|
+
...buildMinimalField(1001),
|
|
144
|
+
type: 'relationfield',
|
|
145
|
+
options: {
|
|
146
|
+
relation: {},
|
|
147
|
+
},
|
|
148
|
+
}],
|
|
149
|
+
})
|
|
150
|
+
assert.equal(parsed.success, false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('EntitySaveReqVO requires rollup relation and aggregation formula', () => {
|
|
154
|
+
assert.ok(schema)
|
|
155
|
+
const parsed = schema!.safeParse({
|
|
156
|
+
field_id_seq: 1002,
|
|
157
|
+
layout: [[1001]],
|
|
158
|
+
fields: [{
|
|
159
|
+
...buildMinimalField(1001),
|
|
160
|
+
type: 'rollup',
|
|
161
|
+
options: {
|
|
162
|
+
relation: {
|
|
163
|
+
local_linkto_field_id: 1009,
|
|
164
|
+
target_field_id: 1010,
|
|
165
|
+
target_field_type: 'number',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}],
|
|
169
|
+
})
|
|
170
|
+
assert.equal(parsed.success, false)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('EntitySaveReqVO rejects subform layout that references non-child field id', () => {
|
|
174
|
+
assert.ok(schema)
|
|
175
|
+
const parsed = schema!.safeParse({
|
|
176
|
+
field_id_seq: 1004,
|
|
177
|
+
layout: [[1001]],
|
|
178
|
+
fields: [{
|
|
179
|
+
...buildMinimalField(1001),
|
|
180
|
+
type: 'subform',
|
|
181
|
+
children: [buildMinimalField(1002)],
|
|
182
|
+
layout: [[1003]],
|
|
183
|
+
}],
|
|
184
|
+
})
|
|
185
|
+
assert.equal(parsed.success, false)
|
|
186
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join('.') === 'fields.0.layout.0.0'))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('EntitySaveReqVO rejects lookup local_linkto_field_id that does not point to linkto field', () => {
|
|
190
|
+
assert.ok(schema)
|
|
191
|
+
const parsed = schema!.safeParse({
|
|
192
|
+
field_id_seq: 1004,
|
|
193
|
+
layout: [[1001], [1002]],
|
|
194
|
+
fields: [
|
|
195
|
+
buildMinimalField(1001),
|
|
196
|
+
{
|
|
197
|
+
...buildMinimalField(1002),
|
|
198
|
+
type: 'lookup',
|
|
199
|
+
options: {
|
|
200
|
+
relation: {
|
|
201
|
+
local_linkto_field_id: 1001,
|
|
202
|
+
target_field_id: 2001,
|
|
203
|
+
target_field_type: 'text',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
})
|
|
209
|
+
assert.equal(parsed.success, false)
|
|
210
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join('.') === 'fields.1.options.relation.local_linkto_field_id'))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('EntitySaveReqVO rejects formula references to unknown field ids', () => {
|
|
214
|
+
assert.ok(schema)
|
|
215
|
+
const parsed = schema!.safeParse({
|
|
216
|
+
field_id_seq: 1003,
|
|
217
|
+
layout: [[1001]],
|
|
218
|
+
fields: [
|
|
219
|
+
buildMinimalField(1001),
|
|
220
|
+
{
|
|
221
|
+
...buildMinimalField(1002),
|
|
222
|
+
key: 'calc',
|
|
223
|
+
label: '计算值',
|
|
224
|
+
type: 'formula',
|
|
225
|
+
options: {
|
|
226
|
+
formula: {
|
|
227
|
+
code: '[[订单号:9999]]',
|
|
228
|
+
jsonlogic: { var: '9999' },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
assert.equal(parsed.success, false)
|
|
235
|
+
assert.ok(parsed.error.issues.some((issue) => issue.message.includes('unknown field id 9999')))
|
|
236
|
+
})
|