@carthooks/arcubase-cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/bundle/arcubase-admin.mjs +1238 -16
  2. package/bundle/arcubase.mjs +1238 -16
  3. package/dist/bin/arcubase-admin.js +0 -0
  4. package/dist/bin/arcubase.js +0 -0
  5. package/dist/runtime/command_registry.d.ts +8 -0
  6. package/dist/runtime/command_registry.d.ts.map +1 -1
  7. package/dist/runtime/command_registry.js +35 -0
  8. package/dist/runtime/entity_save_schema.d.ts +15 -0
  9. package/dist/runtime/entity_save_schema.d.ts.map +1 -0
  10. package/dist/runtime/entity_save_schema.js +641 -0
  11. package/dist/runtime/errors.d.ts +11 -0
  12. package/dist/runtime/errors.d.ts.map +1 -1
  13. package/dist/runtime/execute.d.ts.map +1 -1
  14. package/dist/runtime/execute.js +294 -12
  15. package/dist/runtime/upload.d.ts +13 -0
  16. package/dist/runtime/upload.d.ts.map +1 -0
  17. package/dist/runtime/upload.js +148 -0
  18. package/dist/runtime/zod_registry.d.ts +6 -0
  19. package/dist/runtime/zod_registry.d.ts.map +1 -1
  20. package/dist/runtime/zod_registry.js +136 -0
  21. package/dist/tests/command_registry.test.js +5 -0
  22. package/dist/tests/entity_save_schema.test.d.ts +2 -0
  23. package/dist/tests/entity_save_schema.test.d.ts.map +1 -0
  24. package/dist/tests/entity_save_schema.test.js +221 -0
  25. package/dist/tests/execute_validation.test.js +603 -0
  26. package/dist/tests/help.test.js +49 -0
  27. package/dist/tests/upload.test.d.ts +2 -0
  28. package/dist/tests/upload.test.d.ts.map +1 -0
  29. package/dist/tests/upload.test.js +107 -0
  30. package/package.json +1 -1
  31. package/sdk-dist/docs/runtime-reference/README.md +187 -0
  32. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +120 -0
  33. package/sdk-dist/docs/runtime-reference/entity-schema/address.md +34 -0
  34. package/sdk-dist/docs/runtime-reference/entity-schema/boolean.md +37 -0
  35. package/sdk-dist/docs/runtime-reference/entity-schema/button.md +47 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +61 -0
  37. package/sdk-dist/docs/runtime-reference/entity-schema/datasets.md +35 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/datetime.md +40 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/department.md +33 -0
  40. package/sdk-dist/docs/runtime-reference/entity-schema/departments.md +34 -0
  41. package/sdk-dist/docs/runtime-reference/entity-schema/divider.md +39 -0
  42. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +53 -0
  43. package/sdk-dist/docs/runtime-reference/entity-schema/formula.md +44 -0
  44. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +52 -0
  45. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +67 -0
  46. package/sdk-dist/docs/runtime-reference/entity-schema/location.md +32 -0
  47. package/sdk-dist/docs/runtime-reference/entity-schema/lookup.md +41 -0
  48. package/sdk-dist/docs/runtime-reference/entity-schema/member.md +37 -0
  49. package/sdk-dist/docs/runtime-reference/entity-schema/members.md +31 -0
  50. package/sdk-dist/docs/runtime-reference/entity-schema/number.md +70 -0
  51. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +50 -0
  52. package/sdk-dist/docs/runtime-reference/entity-schema/radio.md +53 -0
  53. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +55 -0
  54. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +38 -0
  55. package/sdk-dist/docs/runtime-reference/entity-schema/rollup.md +54 -0
  56. package/sdk-dist/docs/runtime-reference/entity-schema/select.md +63 -0
  57. package/sdk-dist/docs/runtime-reference/entity-schema/selects.md +51 -0
  58. package/sdk-dist/docs/runtime-reference/entity-schema/serialnumber.md +48 -0
  59. package/sdk-dist/docs/runtime-reference/entity-schema/signature.md +31 -0
  60. package/sdk-dist/docs/runtime-reference/entity-schema/status.md +70 -0
  61. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +74 -0
  62. package/sdk-dist/docs/runtime-reference/entity-schema/tags.md +19 -0
  63. package/sdk-dist/docs/runtime-reference/entity-schema/text.md +76 -0
  64. package/sdk-dist/docs/runtime-reference/entity-schema/textarea.md +47 -0
  65. package/sdk-dist/docs/runtime-reference/entity-schema/workflow_status.md +17 -0
  66. package/sdk-dist/docs/runtime-reference/entity-schema.md +197 -0
  67. package/sdk-dist/docs/runtime-reference/examples/README.md +51 -0
  68. package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +20 -0
  69. package/sdk-dist/docs/runtime-reference/examples/mes-01/README.md +21 -0
  70. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +183 -0
  71. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +79 -0
  72. package/sdk-dist/docs/runtime-reference/examples/oms-01/create-app.json +3 -0
  73. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.create-entity.json +3 -0
  74. package/sdk-dist/docs/runtime-reference/examples/oms-01/goods-receipt.schema.json +159 -0
  75. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.create-entity.json +3 -0
  76. package/sdk-dist/docs/runtime-reference/examples/oms-01/item-master.schema.json +179 -0
  77. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.create-entity.json +3 -0
  78. package/sdk-dist/docs/runtime-reference/examples/oms-01/picking-list.schema.json +168 -0
  79. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.create-entity.json +3 -0
  80. package/sdk-dist/docs/runtime-reference/examples/oms-01/purchase-order.schema.json +417 -0
  81. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.bulk-update.json +22 -0
  82. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.create-entity.json +3 -0
  83. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.query.json +10 -0
  84. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +28 -0
  85. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.schema.json +398 -0
  86. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.condition.json +13 -0
  87. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.selection.ids.json +7 -0
  88. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.create-entity.json +3 -0
  89. package/sdk-dist/docs/runtime-reference/examples/oms-01/stock-issue.schema.json +137 -0
  90. package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +21 -0
  91. package/sdk-dist/docs/runtime-reference/row-crud.md +188 -0
  92. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +494 -0
  93. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +135 -0
  94. package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
  95. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  96. package/src/runtime/command_registry.ts +47 -0
  97. package/src/runtime/entity_save_schema.ts +708 -0
  98. package/src/runtime/errors.ts +12 -0
  99. package/src/runtime/execute.ts +367 -12
  100. package/src/runtime/upload.ts +196 -0
  101. package/src/runtime/zod_registry.ts +156 -0
  102. package/src/tests/command_registry.test.ts +6 -0
  103. package/src/tests/entity_save_schema.test.ts +236 -0
  104. package/src/tests/execute_validation.test.ts +781 -0
  105. package/src/tests/help.test.ts +60 -0
  106. package/src/tests/upload.test.ts +133 -0
@@ -0,0 +1,196 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { CLIError } from './errors.js'
4
+ import type { RuntimeEnv } from './env.js'
5
+ import { buildRequestHeaders, buildURL } from './http.js'
6
+
7
+ type UploadTokenResponse = {
8
+ data?: {
9
+ id?: number | string
10
+ mode?: string
11
+ token_data?: {
12
+ dir?: string
13
+ policy?: string
14
+ host?: string
15
+ accessid?: string
16
+ signature?: string
17
+ x_amz_algorithm?: string
18
+ x_amz_credential?: string
19
+ x_amz_date?: string
20
+ x_amz_signature?: string
21
+ }
22
+ }
23
+ error?: unknown
24
+ trace_id?: string
25
+ }
26
+
27
+ export type ArcubaseUploadResult = {
28
+ upload_id: number | string
29
+ file: string
30
+ file_name: string
31
+ meta: Record<string, never>
32
+ }
33
+
34
+ function readUploadSource(sourcePath: string): { absolutePath: string; bytes: Buffer } {
35
+ const absolutePath = path.resolve(sourcePath)
36
+ if (!fs.existsSync(absolutePath)) {
37
+ throw new CLIError('UPLOAD_SOURCE_NOT_FOUND', `upload source file does not exist: ${sourcePath}`, 2)
38
+ }
39
+ const stat = fs.statSync(absolutePath)
40
+ if (!stat.isFile()) {
41
+ throw new CLIError('UPLOAD_SOURCE_INVALID', `upload source path must be a file: ${sourcePath}`, 2)
42
+ }
43
+ return {
44
+ absolutePath,
45
+ bytes: fs.readFileSync(absolutePath),
46
+ }
47
+ }
48
+
49
+ function parseJSONResponse(raw: string): unknown {
50
+ try {
51
+ return raw ? JSON.parse(raw) : null
52
+ } catch {
53
+ return raw
54
+ }
55
+ }
56
+
57
+ function requireTokenPayload(payload: UploadTokenResponse): NonNullable<UploadTokenResponse['data']> {
58
+ if (payload && typeof payload === 'object' && payload.error) {
59
+ const upstreamError = payload.error
60
+ const message =
61
+ typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof (upstreamError as any).message === 'string'
62
+ ? (upstreamError as any).message
63
+ : typeof upstreamError === 'string'
64
+ ? upstreamError
65
+ : JSON.stringify(upstreamError)
66
+ throw new CLIError('UPSTREAM_BODY_ERROR', message, 1, {
67
+ endpoint: '/upload/token',
68
+ method: 'POST',
69
+ traceId: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
70
+ upstreamError,
71
+ })
72
+ }
73
+ if (!payload?.data || typeof payload.data !== 'object') {
74
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response did not include data', 1)
75
+ }
76
+ return payload.data
77
+ }
78
+
79
+ export function renderUploadHelp(): string {
80
+ return [
81
+ 'arcubase upload <local-file> [flags]',
82
+ '',
83
+ 'purpose:',
84
+ ' - upload one local file through Arcubase',
85
+ ' - return a field value that can be used directly in file or image row payloads',
86
+ '',
87
+ 'flags:',
88
+ ' --filename <name> override the uploaded filename stored in Arcubase',
89
+ ' --global request a global upload token instead of a tenant-scoped token',
90
+ '',
91
+ 'result shape:',
92
+ ' - data is a JSON array',
93
+ ' - use that array directly as the value of a file or image field in insert/update payloads',
94
+ '',
95
+ 'example:',
96
+ ' arcubase upload ./contract.pdf',
97
+ ' arcubase upload ./photo.jpg --filename lead-photo.jpg',
98
+ '',
99
+ 'docs:',
100
+ ' - Uploads: /opt/arcubase-sdk/docs/runtime-reference/uploads.md',
101
+ ' - Row CRUD: /opt/arcubase-sdk/docs/runtime-reference/row-crud.md',
102
+ ' - File field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/file.md',
103
+ ' - Image field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/image.md',
104
+ ].join('\n')
105
+ }
106
+
107
+ export async function uploadLocalFile(
108
+ env: RuntimeEnv,
109
+ sourcePath: string,
110
+ options: {
111
+ filename?: string
112
+ global?: boolean
113
+ },
114
+ fetchImpl: typeof fetch = fetch,
115
+ ): Promise<ArcubaseUploadResult[]> {
116
+ const { absolutePath, bytes } = readUploadSource(sourcePath)
117
+ const filename = (options.filename && options.filename.trim()) || path.basename(absolutePath)
118
+
119
+ const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/upload/token'), {
120
+ method: 'POST',
121
+ headers: {
122
+ ...buildRequestHeaders(env),
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ body: JSON.stringify({
126
+ global: Boolean(options.global),
127
+ }),
128
+ })
129
+
130
+ const tokenRaw = await tokenResponse.text()
131
+ const tokenParsed = parseJSONResponse(tokenRaw)
132
+ if (!tokenResponse.ok) {
133
+ throw new CLIError('UPLOAD_TOKEN_REQUEST_FAILED', typeof tokenParsed === 'string' ? tokenParsed : JSON.stringify(tokenParsed), 1, {
134
+ endpoint: '/upload/token',
135
+ method: 'POST',
136
+ })
137
+ }
138
+
139
+ const tokenData = requireTokenPayload(tokenParsed as UploadTokenResponse)
140
+ if (tokenData.mode !== 'aliyun-oss' && tokenData.mode !== 's3-post-policy') {
141
+ throw new CLIError('UPLOAD_MODE_UNSUPPORTED', `unsupported upload mode: ${String(tokenData.mode || '')}`, 1)
142
+ }
143
+ if (!tokenData.token_data?.dir || !tokenData.token_data.policy || !tokenData.token_data.host) {
144
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1)
145
+ }
146
+ if (tokenData.id === undefined || tokenData.id === null || tokenData.id === '') {
147
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing id', 1)
148
+ }
149
+
150
+ const form = new FormData()
151
+ form.append('key', `${tokenData.token_data.dir}${filename}`)
152
+ form.append('policy', tokenData.token_data.policy)
153
+ if (tokenData.mode === 'aliyun-oss') {
154
+ if (!tokenData.token_data.accessid || !tokenData.token_data.signature) {
155
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1)
156
+ }
157
+ form.append('OSSAccessKeyId', tokenData.token_data.accessid)
158
+ form.append('success_action_status', '200')
159
+ form.append('signature', tokenData.token_data.signature)
160
+ } else {
161
+ if (
162
+ !tokenData.token_data.x_amz_algorithm ||
163
+ !tokenData.token_data.x_amz_credential ||
164
+ !tokenData.token_data.x_amz_date ||
165
+ !tokenData.token_data.x_amz_signature
166
+ ) {
167
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required s3 token_data fields', 1)
168
+ }
169
+ form.append('x-amz-algorithm', tokenData.token_data.x_amz_algorithm)
170
+ form.append('x-amz-credential', tokenData.token_data.x_amz_credential)
171
+ form.append('x-amz-date', tokenData.token_data.x_amz_date)
172
+ form.append('x-amz-signature', tokenData.token_data.x_amz_signature)
173
+ }
174
+ form.append('file', new Blob([new Uint8Array(bytes)]), filename)
175
+
176
+ const uploadResponse = await fetchImpl(tokenData.token_data.host, {
177
+ method: 'POST',
178
+ body: form,
179
+ })
180
+ const uploadRaw = await uploadResponse.text()
181
+ if (!uploadResponse.ok) {
182
+ throw new CLIError('UPLOAD_TRANSFER_FAILED', uploadRaw || `upload failed with status ${uploadResponse.status}`, 1, {
183
+ endpoint: tokenData.token_data.host,
184
+ method: 'POST',
185
+ })
186
+ }
187
+
188
+ return [
189
+ {
190
+ upload_id: tokenData.id,
191
+ file: filename,
192
+ file_name: filename,
193
+ meta: {},
194
+ },
195
+ ]
196
+ }
@@ -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,66 @@ 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 docHintIndex: Record<string, RuntimeDocHint[]> = {
71
+ AppCreateByTenantsReqVO: [
72
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
73
+ examplesIndexDocHint,
74
+ ],
75
+ AppCreateEntityReqVO: [
76
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
77
+ examplesIndexDocHint,
78
+ ],
79
+ EntitySaveReqVO: [
80
+ { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
81
+ examplesIndexDocHint,
82
+ ],
83
+ EntityQueryReqVO: [
84
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
85
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
86
+ examplesIndexDocHint,
87
+ ],
88
+ EntityUpdateReqVO: [
89
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
90
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
91
+ examplesIndexDocHint,
92
+ ],
93
+ EntitySelectionActionReqVO: [
94
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
95
+ examplesIndexDocHint,
96
+ ],
97
+ InsertReqVO: [
98
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
99
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
100
+ examplesIndexDocHint,
101
+ ],
102
+ EntityBulkUpdateReqVO: [
103
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
104
+ examplesIndexDocHint,
105
+ ],
106
+ 'EntityInvokeBatchOperatorReqVO & InvokeRequestVO': [
107
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
108
+ examplesIndexDocHint,
109
+ ],
110
+ '{ policy_id: string selection: any }': [
111
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
112
+ examplesIndexDocHint,
113
+ ],
114
+ '{ policy_id: string selection: any tag_names: string[] }': [
115
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
116
+ examplesIndexDocHint,
117
+ ],
118
+ }
119
+
32
120
  export function getTypeHints(typeName: string): RuntimeTypeHint[] {
33
121
  const name = String(typeName || '').trim()
34
122
  if (!name) return []
@@ -51,3 +139,71 @@ export function getTypeHints(typeName: string): RuntimeTypeHint[] {
51
139
  }
52
140
  return out
53
141
  }
142
+
143
+ export function getDocHints(typeName: string): RuntimeDocHint[] {
144
+ const name = String(typeName || '').trim()
145
+ if (!name) return []
146
+ return docHintIndex[name] ?? []
147
+ }
148
+
149
+ const commandDocHintIndex: Record<string, RuntimeDocHint[]> = {
150
+ 'admin.app.createAppByTenants': [
151
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
152
+ examplesIndexDocHint,
153
+ ],
154
+ 'admin.app.createEntity': [
155
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
156
+ examplesIndexDocHint,
157
+ ],
158
+ 'admin.entity.adminGetEntityInfo': [
159
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
160
+ examplesIndexDocHint,
161
+ ],
162
+ 'admin.entity.adminSaveEntity': [
163
+ { title: 'Entity schema', file: '/opt/arcubase-sdk/docs/runtime-reference/entity-schema.md' },
164
+ { title: 'Table lifecycle', file: '/opt/arcubase-sdk/docs/runtime-reference/table-lifecycle.md' },
165
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
166
+ examplesIndexDocHint,
167
+ ],
168
+ 'user.workflow.insertEntity': [
169
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
170
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
171
+ examplesIndexDocHint,
172
+ ],
173
+ 'user.workflow.queryEntity': [
174
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
175
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
176
+ examplesIndexDocHint,
177
+ ],
178
+ 'user.workflow.getEntityRow': [
179
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
180
+ examplesIndexDocHint,
181
+ ],
182
+ 'user.workflow.updateEntityRow': [
183
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
184
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
185
+ examplesIndexDocHint,
186
+ ],
187
+ 'user.workflow.queryEntitySelection': [
188
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
189
+ examplesIndexDocHint,
190
+ ],
191
+ 'user.workflow.deleteEntityRow': [
192
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
193
+ examplesIndexDocHint,
194
+ ],
195
+ 'user.workflow.restoreEntityRow': [
196
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
197
+ examplesIndexDocHint,
198
+ ],
199
+ 'user.entity.updateEntityBulk': [
200
+ { title: 'Search and bulk actions', file: '/opt/arcubase-sdk/docs/runtime-reference/search-and-bulk-actions.md' },
201
+ examplesIndexDocHint,
202
+ ],
203
+ }
204
+
205
+ export function getCommandDocHints(operation: string): RuntimeDocHint[] {
206
+ const key = String(operation || '').trim()
207
+ if (!key) return []
208
+ return commandDocHintIndex[key] ?? []
209
+ }
@@ -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
+ })