@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.
Files changed (95) hide show
  1. package/bundle/arcubase-admin.mjs +907 -10
  2. package/bundle/arcubase.mjs +907 -10
  3. package/dist/runtime/command_registry.d.ts +8 -0
  4. package/dist/runtime/command_registry.d.ts.map +1 -1
  5. package/dist/runtime/command_registry.js +35 -0
  6. package/dist/runtime/entity_save_schema.d.ts +15 -0
  7. package/dist/runtime/entity_save_schema.d.ts.map +1 -0
  8. package/dist/runtime/entity_save_schema.js +641 -0
  9. package/dist/runtime/errors.d.ts +10 -0
  10. package/dist/runtime/errors.d.ts.map +1 -1
  11. package/dist/runtime/execute.d.ts.map +1 -1
  12. package/dist/runtime/execute.js +89 -10
  13. package/dist/runtime/zod_registry.d.ts +6 -0
  14. package/dist/runtime/zod_registry.d.ts.map +1 -1
  15. package/dist/runtime/zod_registry.js +133 -0
  16. package/dist/tests/command_registry.test.js +5 -0
  17. package/dist/tests/entity_save_schema.test.d.ts +2 -0
  18. package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
  19. package/dist/tests/entity_save_schema.test.js +221 -0
  20. package/dist/tests/execute_validation.test.js +450 -0
  21. package/dist/tests/help.test.js +33 -0
  22. package/package.json +1 -1
  23. package/sdk-dist/docs/runtime-reference/README.md +177 -0
  24. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
  25. package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
  26. package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
  27. package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
  28. package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
  29. package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
  30. package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
  31. package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
  32. package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
  33. package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
  34. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +44 -0
  35. package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +43 -0
  37. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
  40. package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
  41. package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
  42. package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
  43. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
  44. package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
  45. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
  46. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
  47. package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
  48. package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
  49. package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
  50. package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
  51. package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
  52. package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
  53. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
  54. package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
  55. package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
  56. package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
  57. package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
  58. package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
  59. package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
  60. package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
  61. package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
  62. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +164 -0
  63. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
  64. package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
  65. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
  66. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
  67. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
  68. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
  69. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
  70. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
  71. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
  72. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
  73. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
  74. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
  75. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
  76. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
  77. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
  78. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
  79. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
  80. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
  81. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
  82. package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
  83. package/sdk-dist/docs/runtime-reference/row-crud.md +186 -0
  84. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
  85. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +123 -0
  86. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  87. package/src/runtime/command_registry.ts +47 -0
  88. package/src/runtime/entity_save_schema.ts +708 -0
  89. package/src/runtime/errors.ts +11 -0
  90. package/src/runtime/execute.ts +123 -10
  91. package/src/runtime/zod_registry.ts +154 -0
  92. package/src/tests/command_registry.test.ts +6 -0
  93. package/src/tests/entity_save_schema.test.ts +236 -0
  94. package/src/tests/execute_validation.test.ts +598 -0
  95. package/src/tests/help.test.ts +39 -0
@@ -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 {
@@ -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
- throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2)
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
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
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
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
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: RuntimeEnv = loadRuntimeEnv(scope), fetchImpl: typeof fetch = fetch): Promise<any> {
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 headers = buildRequestHeaders(env)
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
- ...headers,
276
+ ...buildRequestHeaders(runtimeEnv),
185
277
  'Content-Type': 'application/json',
186
278
  },
187
- body: body === undefined || command.method === 'GET' ? undefined : JSON.stringify(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
+ })