@carthooks/arcubase-cli 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/arcubase-admin.mjs +1274 -375
- package/bundle/arcubase.mjs +1274 -375
- package/dist/generated/command_registry.generated.d.ts +36 -35
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +37 -36
- package/dist/generated/type_index.generated.d.ts +23 -23
- package/dist/generated/type_index.generated.js +23 -23
- package/dist/runtime/entity_save_schema.d.ts.map +1 -1
- package/dist/runtime/entity_save_schema.js +30 -0
- package/dist/runtime/env.d.ts +2 -1
- package/dist/runtime/env.d.ts.map +1 -1
- package/dist/runtime/env.js +10 -1
- package/dist/runtime/errors.d.ts +2 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/execute.d.ts +4 -2
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +944 -134
- package/dist/runtime/paths.d.ts +1 -1
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +9 -2
- package/dist/runtime/upload.d.ts +1 -11
- package/dist/runtime/upload.d.ts.map +1 -1
- package/dist/runtime/upload.js +19 -133
- package/dist/runtime/zod_registry.d.ts +5 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +108 -48
- package/dist/tests/bootstrap.test.js +12 -0
- package/dist/tests/command_registry.test.js +8 -0
- package/dist/tests/docs_readability.test.d.ts +2 -0
- package/dist/tests/docs_readability.test.d.ts.map +1 -0
- package/dist/tests/docs_readability.test.js +87 -0
- package/dist/tests/entity_save_schema.test.js +22 -0
- package/dist/tests/execute_validation.test.js +381 -44
- package/dist/tests/help.test.js +45 -8
- package/dist/tests/upload.test.js +30 -77
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +43 -29
- package/sdk-dist/docs/runtime-reference/access-rule.md +109 -0
- package/sdk-dist/docs/runtime-reference/condition.md +182 -0
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +6 -5
- package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/file.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema/image.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/number.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
- package/sdk-dist/docs/runtime-reference/entity-schema/status.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema/text.md +1 -1
- package/sdk-dist/docs/runtime-reference/entity-schema.md +28 -21
- package/sdk-dist/docs/runtime-reference/examples/README.md +1 -1
- package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +30 -11
- package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/create-app.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.bulk-update.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.row.insert.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.selection.condition.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +26 -26
- package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +1 -1
- package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +26 -10
- package/sdk-dist/docs/runtime-reference/examples/wms-01/create-app.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.bulk-update.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.selection.condition.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.create-entity.json +1 -0
- package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.schema.json +1 -0
- package/sdk-dist/docs/runtime-reference/row-crud.md +35 -8
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +31 -95
- package/sdk-dist/docs/runtime-reference/selection.md +128 -0
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +113 -30
- package/sdk-dist/docs/runtime-reference/uploads.md +30 -59
- package/sdk-dist/docs/runtime-reference/workflow/README.md +129 -13
- package/sdk-dist/generated/command_registry.generated.ts +37 -36
- package/sdk-dist/generated/type_index.generated.ts +23 -23
- package/src/generated/command_registry.generated.ts +37 -36
- package/src/generated/type_index.generated.ts +23 -23
- package/src/runtime/entity_save_schema.ts +30 -0
- package/src/runtime/env.ts +14 -2
- package/src/runtime/errors.ts +2 -0
- package/src/runtime/execute.ts +1166 -134
- package/src/runtime/paths.ts +11 -2
- package/src/runtime/upload.ts +19 -178
- package/src/runtime/zod_registry.ts +113 -48
- package/src/tests/bootstrap.test.ts +16 -0
- package/src/tests/command_registry.test.ts +9 -0
- package/src/tests/docs_readability.test.ts +96 -0
- package/src/tests/entity_save_schema.test.ts +22 -0
- package/src/tests/execute_validation.test.ts +508 -47
- package/src/tests/help.test.ts +49 -8
- package/src/tests/upload.test.ts +34 -91
|
@@ -9,6 +9,7 @@ import { CLIError } from '../runtime/errors.js'
|
|
|
9
9
|
const env = {
|
|
10
10
|
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
11
11
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
12
|
+
ARCUBASE_SDK_ROOT: '/runtime/arcubase-sdk',
|
|
12
13
|
ARCUBASE_TRACE_ID: '',
|
|
13
14
|
ARCUBASE_SUBJECT_TYPE: '',
|
|
14
15
|
ARCUBASE_SUBJECT_ID: '',
|
|
@@ -22,6 +23,28 @@ function tempJSONFile(payload: unknown): string {
|
|
|
22
23
|
return filePath
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function withRowPolicyResolver(
|
|
27
|
+
tableId = 'table_1',
|
|
28
|
+
actions = ['view', 'add', 'edit', 'delete', 'bulk_update', 'archive'],
|
|
29
|
+
policyOverridesOrFinal: Record<string, unknown> | typeof fetch = {},
|
|
30
|
+
final?: typeof fetch,
|
|
31
|
+
): typeof fetch {
|
|
32
|
+
const policyOverrides = typeof policyOverridesOrFinal === 'function' ? {} : policyOverridesOrFinal
|
|
33
|
+
const finalFetch = typeof policyOverridesOrFinal === 'function'
|
|
34
|
+
? policyOverridesOrFinal
|
|
35
|
+
: final ?? (async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }))
|
|
36
|
+
return async (url, init) => {
|
|
37
|
+
const text = String(url)
|
|
38
|
+
if (/\/api\/apps\/[^/?]+$/.test(text) && init?.method === 'GET') {
|
|
39
|
+
return new Response(JSON.stringify({ data: { ingress: { groups: [{ items: [{ id: 'policy_hash_1' }] }] } } }), { status: 200 })
|
|
40
|
+
}
|
|
41
|
+
if (/\/api\/ingress\/[^/]+\/policy_hash_1$/.test(text) && init?.method === 'GET') {
|
|
42
|
+
return new Response(JSON.stringify({ data: { id: tableId, policy: { actions, ...policyOverrides } } }), { status: 200 })
|
|
43
|
+
}
|
|
44
|
+
return finalFetch(url, init)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boolean {
|
|
26
49
|
assert.ok(error instanceof CLIError)
|
|
27
50
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
@@ -32,7 +55,7 @@ function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boo
|
|
|
32
55
|
assert.deepEqual(error.details?.shapeHint, {
|
|
33
56
|
update: ['user_scope'],
|
|
34
57
|
options: {
|
|
35
|
-
user_scope: [{ type: '
|
|
58
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
36
59
|
},
|
|
37
60
|
})
|
|
38
61
|
assert.deepEqual(error.details?.commonMistakes, [
|
|
@@ -41,9 +64,9 @@ function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boo
|
|
|
41
64
|
'do not use hash tenant user id as id',
|
|
42
65
|
'id must be numeric TenantUser.ID',
|
|
43
66
|
])
|
|
44
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/
|
|
45
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/
|
|
46
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/
|
|
67
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'))
|
|
68
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'))
|
|
69
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/runtime/arcubase-sdk/types/common.ts'))
|
|
47
70
|
return true
|
|
48
71
|
}
|
|
49
72
|
|
|
@@ -109,6 +132,15 @@ test('table update-schema rejects invalid body file', async () => {
|
|
|
109
132
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
110
133
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO')
|
|
111
134
|
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
|
|
135
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntitySaveReqVO' && item.file === '/runtime/arcubase-sdk/types/entity.ts'))
|
|
136
|
+
assert.ok(error.details?.docHints?.some((item) => item.file === '/runtime/arcubase-sdk/docs/runtime-reference/entity-schema.md'))
|
|
137
|
+
assert.equal((error.details?.shapeHint as any)?.field_id_seq, 1004)
|
|
138
|
+
assert.equal((error.details?.shapeHint as any)?.fields?.[0]?.type, 'text')
|
|
139
|
+
assert.equal((error.details?.shapeHint as any)?.fields?.[1]?.type, 'textarea')
|
|
140
|
+
assert.equal((error.details?.shapeHint as any)?.fields?.[2]?.type, 'select')
|
|
141
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('fields must be an array')))
|
|
142
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('get table shell first')))
|
|
143
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('not "string"')))
|
|
112
144
|
return true
|
|
113
145
|
})
|
|
114
146
|
})
|
|
@@ -125,40 +157,68 @@ test('app create rejects name longer than 20 chars', async () => {
|
|
|
125
157
|
})
|
|
126
158
|
})
|
|
127
159
|
|
|
128
|
-
test('table create
|
|
129
|
-
|
|
160
|
+
test('table create rejects empty schema body before request', async () => {
|
|
161
|
+
await assert.rejects(async () => {
|
|
162
|
+
await executeCLI('admin', ['table', 'create', '--app-id', '2188889844', '--body-json', '{"name":"订单","parent":0}'], env as any, async () => new Response('{}'))
|
|
163
|
+
}, (error: unknown) => {
|
|
164
|
+
assert.ok(error instanceof CLIError)
|
|
165
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
166
|
+
assert.match(error.message, /fields/)
|
|
167
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase-admin table create --app-id <app_id> --body-json')))
|
|
168
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"field_id_seq":1003')))
|
|
169
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"投票描述"')))
|
|
170
|
+
return true
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('table create executes create and schema save with valid body file', async () => {
|
|
175
|
+
const bodyFile = tempJSONFile({ ...validEntitySavePayload({ id: undefined, app_id: undefined }), name: '订单', parent: 0 })
|
|
176
|
+
const calls: Array<{ url: string; method?: string; body: any }> = []
|
|
130
177
|
const out = await executeCLI(
|
|
131
178
|
'admin',
|
|
132
|
-
['table', 'create', '--app-id', '
|
|
179
|
+
['table', 'create', '--app-id', '2188889844', '--body-file', bodyFile],
|
|
133
180
|
env as any,
|
|
134
|
-
async (url, init) =>
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
181
|
+
async (url, init) => {
|
|
182
|
+
calls.push({ url: String(url), method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null })
|
|
183
|
+
if (init?.method === 'POST') {
|
|
184
|
+
return new Response(JSON.stringify({ data: { id: 2188889845 } }), { status: 200 })
|
|
185
|
+
}
|
|
186
|
+
return new Response(JSON.stringify({ data: { id: 2188889845, fields: calls[1]?.body?.fields ?? [] } }), { status: 200 })
|
|
187
|
+
},
|
|
141
188
|
)
|
|
142
189
|
assert.equal(out.kind, 'result')
|
|
143
|
-
assert.equal(out.data.
|
|
144
|
-
assert.equal(
|
|
145
|
-
assert.equal(
|
|
190
|
+
assert.equal(out.data.id, 2188889845)
|
|
191
|
+
assert.equal(calls.length, 2)
|
|
192
|
+
assert.equal(calls[0].url, 'https://arcubase.example.com/api/apps/2188889844/entities')
|
|
193
|
+
assert.equal(calls[0].method, 'POST')
|
|
194
|
+
assert.deepEqual(calls[0].body, { name: '订单', parent: 0 })
|
|
195
|
+
assert.equal(calls[1].url, 'https://arcubase.example.com/api/apps/2188889844/entity/2188889845')
|
|
196
|
+
assert.equal(calls[1].method, 'PUT')
|
|
197
|
+
assert.equal(calls[1].body.id, 2188889845)
|
|
198
|
+
assert.equal(calls[1].body.app_id, 2188889844)
|
|
199
|
+
assert.equal(calls[1].body.fields.length, 1)
|
|
146
200
|
})
|
|
147
201
|
|
|
148
202
|
test('table create executes with valid body json', async () => {
|
|
203
|
+
const calls: Array<{ url: string; method?: string; body: any }> = []
|
|
204
|
+
const payload = JSON.stringify({ ...validEntitySavePayload({ id: undefined, app_id: undefined }), name: '订单', parent: 0 })
|
|
149
205
|
const out = await executeCLI(
|
|
150
206
|
'admin',
|
|
151
|
-
['table', 'create', '--app-id', '
|
|
207
|
+
['table', 'create', '--app-id', '2188889844', '--body-json', payload],
|
|
152
208
|
env as any,
|
|
153
|
-
async (url, init) =>
|
|
154
|
-
url,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
209
|
+
async (url, init) => {
|
|
210
|
+
calls.push({ url: String(url), method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null })
|
|
211
|
+
if (init?.method === 'POST') {
|
|
212
|
+
return new Response(JSON.stringify({ data: { id: 2188889845 } }), { status: 200 })
|
|
213
|
+
}
|
|
214
|
+
return new Response(JSON.stringify({ data: { id: 2188889845, fields: calls[1]?.body?.fields ?? [] } }), { status: 200 })
|
|
215
|
+
},
|
|
158
216
|
)
|
|
159
217
|
assert.equal(out.kind, 'result')
|
|
160
|
-
assert.equal(out.data.
|
|
161
|
-
assert.
|
|
218
|
+
assert.equal(out.data.id, 2188889845)
|
|
219
|
+
assert.equal(calls.length, 2)
|
|
220
|
+
assert.deepEqual(calls[0].body, { name: '订单', parent: 0 })
|
|
221
|
+
assert.equal(calls[1].body.id, 2188889845)
|
|
162
222
|
})
|
|
163
223
|
|
|
164
224
|
test('body json conflicts with body file', async () => {
|
|
@@ -182,6 +242,22 @@ test('body json is rejected for commands without request type', async () => {
|
|
|
182
242
|
})
|
|
183
243
|
})
|
|
184
244
|
|
|
245
|
+
test('entry rejects retired include-app-tables flag before fetch', async () => {
|
|
246
|
+
let called = false
|
|
247
|
+
await assert.rejects(async () => {
|
|
248
|
+
await executeCLI('user', ['entry', '--include-app-tables'], env as any, async () => {
|
|
249
|
+
called = true
|
|
250
|
+
return new Response('{}', { status: 200 })
|
|
251
|
+
})
|
|
252
|
+
}, (error: unknown) => {
|
|
253
|
+
assert.ok(error instanceof CLIError)
|
|
254
|
+
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
255
|
+
assert.match(error.message, /--include-app-tables/)
|
|
256
|
+
return true
|
|
257
|
+
})
|
|
258
|
+
assert.equal(called, false)
|
|
259
|
+
})
|
|
260
|
+
|
|
185
261
|
test('body json parse errors fail fast', async () => {
|
|
186
262
|
await assert.rejects(async () => {
|
|
187
263
|
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env as any, async () => new Response('{}', { status: 200 }))
|
|
@@ -192,6 +268,19 @@ test('body json parse errors fail fast', async () => {
|
|
|
192
268
|
})
|
|
193
269
|
})
|
|
194
270
|
|
|
271
|
+
test('body json parse errors include command docs and type hints', async () => {
|
|
272
|
+
await assert.rejects(async () => {
|
|
273
|
+
await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["options"]}]}'], env as any, async () => new Response('{}', { status: 200 }))
|
|
274
|
+
}, (error: unknown) => {
|
|
275
|
+
assert.ok(error instanceof CLIError)
|
|
276
|
+
assert.equal(error.code, 'INVALID_BODY_JSON')
|
|
277
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
278
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/docs/runtime-reference/access-rule.md'))
|
|
279
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('access-rule update policy changes')))
|
|
280
|
+
return true
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
195
284
|
test('access-rule list executes against external root path', async () => {
|
|
196
285
|
const out = await executeCLI(
|
|
197
286
|
'admin',
|
|
@@ -256,6 +345,36 @@ test('table update-schema normalizes shell null placeholders before validation',
|
|
|
256
345
|
assert.deepEqual(requestBody.options.misc, {})
|
|
257
346
|
})
|
|
258
347
|
|
|
348
|
+
test('table update-schema rejects per-user permission patches', async () => {
|
|
349
|
+
const body = JSON.stringify({
|
|
350
|
+
id: 2188893443,
|
|
351
|
+
app_id: 2188893436,
|
|
352
|
+
name: '投票项目',
|
|
353
|
+
schema_version: 1,
|
|
354
|
+
field_id_seq: 1003,
|
|
355
|
+
layout: [[1001], [1002]],
|
|
356
|
+
fields: [
|
|
357
|
+
{ id: 1001, label: 'Title', type: 'text', required: true, unique: false, editable: true, visible: true, show_label: true, scannable: false, default_value_mode: 0, description: '', value: null, number_decimal: 0, depends: [], key: 'title', code_index: false, options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 }, transfers: null, children: null },
|
|
358
|
+
{ id: 1002, label: 'Description', type: 'textarea', required: false, unique: false, editable: true, visible: true, show_label: true, scannable: false, default_value_mode: 0, description: '', value: null, number_decimal: 0, depends: [], key: 'description', code_index: false, options: { placeholder: '', html: false, size: 'default', lengthLimit: false, lengthMin: 0, lengthMax: 2000 }, transfers: null, children: null, permission: { user: { read: false } } },
|
|
359
|
+
],
|
|
360
|
+
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
361
|
+
workflow_enabled: false,
|
|
362
|
+
})
|
|
363
|
+
await assert.rejects(async () => {
|
|
364
|
+
await executeCLI(
|
|
365
|
+
'admin',
|
|
366
|
+
['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body],
|
|
367
|
+
env as any,
|
|
368
|
+
async () => new Response('{}', { status: 200 }),
|
|
369
|
+
)
|
|
370
|
+
}, (error: unknown) => {
|
|
371
|
+
assert.ok(error instanceof CLIError)
|
|
372
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
373
|
+
assert.match(error.message, /access-rule/)
|
|
374
|
+
return true
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
259
378
|
test('row delete rejects selection type outside ids', async () => {
|
|
260
379
|
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } })
|
|
261
380
|
await assert.rejects(async () => {
|
|
@@ -277,7 +396,7 @@ test('row bulk-update accepts ids selection with number_add action', async () =>
|
|
|
277
396
|
'user',
|
|
278
397
|
['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
279
398
|
env as any,
|
|
280
|
-
|
|
399
|
+
withRowPolicyResolver(),
|
|
281
400
|
)
|
|
282
401
|
assert.equal(out.kind, 'result')
|
|
283
402
|
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 })
|
|
@@ -297,7 +416,7 @@ test('row selection-action accepts condition selection with selectAll', async ()
|
|
|
297
416
|
'user',
|
|
298
417
|
['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile],
|
|
299
418
|
env as any,
|
|
300
|
-
|
|
419
|
+
withRowPolicyResolver(),
|
|
301
420
|
)
|
|
302
421
|
assert.equal(out.kind, 'result')
|
|
303
422
|
assert.equal(out.data.selection.type, 'condition')
|
|
@@ -316,7 +435,139 @@ test('row selection-action rejects condition selection for query action', async
|
|
|
316
435
|
})
|
|
317
436
|
})
|
|
318
437
|
|
|
319
|
-
test('row create
|
|
438
|
+
test('row create resolves internal policy_id from accessible ingress', async () => {
|
|
439
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } })
|
|
440
|
+
const calls: Array<{ url: string; method?: string; body: any }> = []
|
|
441
|
+
const out = await executeCLI(
|
|
442
|
+
'user',
|
|
443
|
+
['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile],
|
|
444
|
+
env as any,
|
|
445
|
+
withRowPolicyResolver('2188900694', ['view', 'add'], async (url, init) => {
|
|
446
|
+
calls.push({
|
|
447
|
+
url: String(url),
|
|
448
|
+
method: init?.method,
|
|
449
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
450
|
+
})
|
|
451
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
452
|
+
}),
|
|
453
|
+
)
|
|
454
|
+
assert.equal(out.kind, 'result')
|
|
455
|
+
assert.deepEqual(calls, [
|
|
456
|
+
{ url: 'https://arcubase.example.com/api/entity/2188900691/2188900694', method: 'GET', body: null },
|
|
457
|
+
{
|
|
458
|
+
url: 'https://arcubase.example.com/api/entity/2188900691/2188900694/insert',
|
|
459
|
+
method: 'POST',
|
|
460
|
+
body: { fields: { 1001: '今天吃什么' }, policy_id: 'policy_hash_1' },
|
|
461
|
+
},
|
|
462
|
+
])
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('row get resolves internal policy_id as query parameter', async () => {
|
|
466
|
+
const calls: string[] = []
|
|
467
|
+
const out = await executeCLI(
|
|
468
|
+
'user',
|
|
469
|
+
['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'],
|
|
470
|
+
env as any,
|
|
471
|
+
withRowPolicyResolver('2188900694', ['view'], async (url) => {
|
|
472
|
+
calls.push(String(url))
|
|
473
|
+
return new Response(JSON.stringify({ data: { id: 4000000046 } }), { status: 200 })
|
|
474
|
+
}),
|
|
475
|
+
)
|
|
476
|
+
assert.equal(out.kind, 'result')
|
|
477
|
+
assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('row command rejects user-provided policy_id', async () => {
|
|
481
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' })
|
|
482
|
+
await assert.rejects(async () => {
|
|
483
|
+
await executeCLI(
|
|
484
|
+
'user',
|
|
485
|
+
['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile],
|
|
486
|
+
env as any,
|
|
487
|
+
withRowPolicyResolver('2188900694', ['add']),
|
|
488
|
+
)
|
|
489
|
+
}, (error: unknown) => {
|
|
490
|
+
assert.ok(error instanceof CLIError)
|
|
491
|
+
assert.equal(error.code, 'INTERNAL_POLICY_ID_NOT_ALLOWED')
|
|
492
|
+
assert.match(error.message, /internal Arcubase ingress parameter/)
|
|
493
|
+
return true
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
test('row command fastfails when no accessible ingress matches action', async () => {
|
|
498
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } })
|
|
499
|
+
await assert.rejects(async () => {
|
|
500
|
+
await executeCLI(
|
|
501
|
+
'user',
|
|
502
|
+
['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile],
|
|
503
|
+
env as any,
|
|
504
|
+
withRowPolicyResolver('2188900694', ['view']),
|
|
505
|
+
)
|
|
506
|
+
}, (error: unknown) => {
|
|
507
|
+
assert.ok(error instanceof CLIError)
|
|
508
|
+
assert.equal(error.code, 'POLICY_ID_RESOLUTION_FAILED')
|
|
509
|
+
assert.match(error.message, /action add/)
|
|
510
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('access-rule')))
|
|
511
|
+
return true
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
test('row create fastfails before request when payload writes non-writable field', async () => {
|
|
516
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '标题', 1002: '隐藏描述' } })
|
|
517
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
518
|
+
await assert.rejects(async () => {
|
|
519
|
+
await executeCLI(
|
|
520
|
+
'user',
|
|
521
|
+
['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile],
|
|
522
|
+
env as any,
|
|
523
|
+
withRowPolicyResolver(
|
|
524
|
+
'2188900694',
|
|
525
|
+
['view', 'add'],
|
|
526
|
+
{ all_fields_write: true, fields: [{ key: ':1002', read: false, write: false, report: false }] },
|
|
527
|
+
async (url, init) => {
|
|
528
|
+
calls.push({ url: String(url), method: init?.method })
|
|
529
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
530
|
+
},
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
}, (error: unknown) => {
|
|
534
|
+
assert.ok(error instanceof CLIError)
|
|
535
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED')
|
|
536
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1002'))
|
|
537
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('remove non-writable field ids')))
|
|
538
|
+
return true
|
|
539
|
+
})
|
|
540
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false)
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
test('row update fastfails before request when payload writes non-writable field', async () => {
|
|
544
|
+
const bodyFile = tempJSONFile({ data: { 1002: '隐藏描述' }, changed_fields: [1002] })
|
|
545
|
+
const calls: Array<{ url: string; method?: string }> = []
|
|
546
|
+
await assert.rejects(async () => {
|
|
547
|
+
await executeCLI(
|
|
548
|
+
'user',
|
|
549
|
+
['row', 'update', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '2188901452', '--body-file', bodyFile],
|
|
550
|
+
env as any,
|
|
551
|
+
withRowPolicyResolver(
|
|
552
|
+
'2188900694',
|
|
553
|
+
['view', 'edit'],
|
|
554
|
+
{ all_fields_write: false, fields: [{ key: ':1001', read: true, write: true, report: false }, { key: ':1002', read: false, write: false, report: false }] },
|
|
555
|
+
async (url, init) => {
|
|
556
|
+
calls.push({ url: String(url), method: init?.method })
|
|
557
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
558
|
+
},
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
}, (error: unknown) => {
|
|
562
|
+
assert.ok(error instanceof CLIError)
|
|
563
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED')
|
|
564
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1002'))
|
|
565
|
+
return true
|
|
566
|
+
})
|
|
567
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
test('row create rejects local file path for file field and points to upload token flow', async () => {
|
|
320
571
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } })
|
|
321
572
|
const calls: Array<{ url: string; method?: string }> = []
|
|
322
573
|
await assert.rejects(async () => {
|
|
@@ -336,12 +587,31 @@ test('row create rejects local file path for file field and points to arcubase u
|
|
|
336
587
|
assert.ok(error instanceof CLIError)
|
|
337
588
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
338
589
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
|
|
339
|
-
assert.match(error.message, /
|
|
590
|
+
assert.match(error.message, /upload token flow/)
|
|
340
591
|
return true
|
|
341
592
|
})
|
|
342
593
|
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }])
|
|
343
594
|
})
|
|
344
595
|
|
|
596
|
+
test('row create non-numeric field key gives table get retry path', async () => {
|
|
597
|
+
const bodyFile = tempJSONFile({ fields: { 标题: '今天吃什么' } })
|
|
598
|
+
await assert.rejects(async () => {
|
|
599
|
+
await executeCLI(
|
|
600
|
+
'user',
|
|
601
|
+
['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile],
|
|
602
|
+
env as any,
|
|
603
|
+
async () => new Response('{}', { status: 200 }),
|
|
604
|
+
)
|
|
605
|
+
}, (error: unknown) => {
|
|
606
|
+
assert.ok(error instanceof CLIError)
|
|
607
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
608
|
+
assert.ok(error.details?.commonMistakes?.some((item) => item.includes('do not use field labels')))
|
|
609
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase table get')))
|
|
610
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase row create')))
|
|
611
|
+
return true
|
|
612
|
+
})
|
|
613
|
+
})
|
|
614
|
+
|
|
345
615
|
test('row update rejects object value for image field and requires upload array', async () => {
|
|
346
616
|
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } })
|
|
347
617
|
await assert.rejects(async () => {
|
|
@@ -360,7 +630,7 @@ test('row update rejects object value for image field and requires upload array'
|
|
|
360
630
|
assert.ok(error instanceof CLIError)
|
|
361
631
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
362
632
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
|
|
363
|
-
assert.match(error.message, /
|
|
633
|
+
assert.match(error.message, /upload value array/)
|
|
364
634
|
return true
|
|
365
635
|
})
|
|
366
636
|
})
|
|
@@ -372,13 +642,13 @@ test('row create accepts upload array for file field after schema preflight', as
|
|
|
372
642
|
'user',
|
|
373
643
|
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
374
644
|
env as any,
|
|
375
|
-
async (url, init) => {
|
|
645
|
+
withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
376
646
|
calls.push({ url: String(url), method: init?.method })
|
|
377
647
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
378
648
|
return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1001, type: 'text', label: 'Lead Name' }, { id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
379
649
|
}
|
|
380
650
|
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
381
|
-
},
|
|
651
|
+
}),
|
|
382
652
|
)
|
|
383
653
|
assert.equal(out.kind, 'result')
|
|
384
654
|
assert.deepEqual(calls, [
|
|
@@ -395,7 +665,7 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
395
665
|
'user',
|
|
396
666
|
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
397
667
|
env as any,
|
|
398
|
-
async (url, init) => {
|
|
668
|
+
withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
399
669
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
400
670
|
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
401
671
|
}
|
|
@@ -407,14 +677,14 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
407
677
|
},
|
|
408
678
|
trace_id: 'req_upload_bind',
|
|
409
679
|
}), { status: 200 })
|
|
410
|
-
},
|
|
680
|
+
}),
|
|
411
681
|
)
|
|
412
682
|
}, (error: unknown) => {
|
|
413
683
|
assert.ok(error instanceof CLIError)
|
|
414
684
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
|
|
415
685
|
assert.equal(error.details?.traceId, 'req_upload_bind')
|
|
416
686
|
assert.equal(error.details?.reason, 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record')
|
|
417
|
-
assert.match(error.details?.hint ?? '', /
|
|
687
|
+
assert.match(error.details?.hint ?? '', /upload token flow/)
|
|
418
688
|
return true
|
|
419
689
|
})
|
|
420
690
|
})
|
|
@@ -431,6 +701,30 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
431
701
|
})
|
|
432
702
|
})
|
|
433
703
|
|
|
704
|
+
test('row query unknown limit flag gives body-json retry path', async () => {
|
|
705
|
+
await assert.rejects(async () => {
|
|
706
|
+
await executeCLI('user', ['row', 'query', '--app-id', '2188900691', '--table-id', '2188900694', '--limit', '1', '--offset', '0'], env as any, async () => new Response('{}', { status: 200 }))
|
|
707
|
+
}, (error: unknown) => {
|
|
708
|
+
assert.ok(error instanceof CLIError)
|
|
709
|
+
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
710
|
+
assert.equal(error.details?.requestType, 'EntityQueryReqVO')
|
|
711
|
+
assert.deepEqual(error.details?.shapeHint, { limit: 20, offset: 0, search: { text_search: 'SO-1001' } })
|
|
712
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('--body-json')))
|
|
713
|
+
return true
|
|
714
|
+
})
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
test('table dataset unknown app flag points to table get', async () => {
|
|
718
|
+
await assert.rejects(async () => {
|
|
719
|
+
await executeCLI('user', ['table', 'dataset', '--app-id', '2188900691', '--table-id', '2188900694'], env as any, async () => new Response('{}', { status: 200 }))
|
|
720
|
+
}, (error: unknown) => {
|
|
721
|
+
assert.ok(error instanceof CLIError)
|
|
722
|
+
assert.equal(error.code, 'UNKNOWN_FLAG')
|
|
723
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('table get --app-id')))
|
|
724
|
+
return true
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
|
|
434
728
|
test('row update rejects fields key', async () => {
|
|
435
729
|
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] })
|
|
436
730
|
await assert.rejects(async () => {
|
|
@@ -456,7 +750,7 @@ test('row update rejects changed_fields that are missing from data', async () =>
|
|
|
456
750
|
})
|
|
457
751
|
|
|
458
752
|
test('workflow approve maps to fixed action path', async () => {
|
|
459
|
-
const bodyFile = tempJSONFile({ task_id:
|
|
753
|
+
const bodyFile = tempJSONFile({ task_id: 2188890521, fields: {}, changed: [] })
|
|
460
754
|
const out = await executeCLI(
|
|
461
755
|
'user',
|
|
462
756
|
['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
@@ -465,7 +759,7 @@ test('workflow approve maps to fixed action path', async () => {
|
|
|
465
759
|
)
|
|
466
760
|
assert.equal(out.kind, 'result')
|
|
467
761
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2')
|
|
468
|
-
assert.equal(out.data.body.task_id,
|
|
762
|
+
assert.equal(out.data.body.task_id, 2188890521)
|
|
469
763
|
})
|
|
470
764
|
|
|
471
765
|
test('workflow query-approve-users requires request body', async () => {
|
|
@@ -478,10 +772,10 @@ test('workflow query-approve-users requires request body', async () => {
|
|
|
478
772
|
})
|
|
479
773
|
})
|
|
480
774
|
|
|
481
|
-
test('entry
|
|
775
|
+
test('entry always includes app tables without exposing a query flag', async () => {
|
|
482
776
|
const out = await executeCLI(
|
|
483
777
|
'user',
|
|
484
|
-
['entry'
|
|
778
|
+
['entry'],
|
|
485
779
|
env as any,
|
|
486
780
|
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
487
781
|
)
|
|
@@ -489,6 +783,45 @@ test('entry passes include_app_tables query flag', async () => {
|
|
|
489
783
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
490
784
|
})
|
|
491
785
|
|
|
786
|
+
test('entry fixed app tables query cannot be disabled by query file', async () => {
|
|
787
|
+
const queryFile = tempJSONFile({ include_app_tables: false })
|
|
788
|
+
const out = await executeCLI(
|
|
789
|
+
'user',
|
|
790
|
+
['entry', '--query-file', queryFile],
|
|
791
|
+
env as any,
|
|
792
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
793
|
+
)
|
|
794
|
+
assert.equal(out.kind, 'result')
|
|
795
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
test('upstream http failures include endpoint status and body details', async () => {
|
|
799
|
+
await assert.rejects(async () => {
|
|
800
|
+
await executeCLI('user', ['entry'], env as any, async () => new Response('', { status: 500 }))
|
|
801
|
+
}, (error: unknown) => {
|
|
802
|
+
assert.ok(error instanceof CLIError)
|
|
803
|
+
assert.equal(error.code, 'UPSTREAM_REQUEST_FAILED')
|
|
804
|
+
assert.equal(error.details?.endpoint, '/api/entry')
|
|
805
|
+
assert.equal(error.details?.method, 'GET')
|
|
806
|
+
assert.equal(error.details?.status, 500)
|
|
807
|
+
return true
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
test('upstream network failures are separated from http failures', async () => {
|
|
812
|
+
await assert.rejects(async () => {
|
|
813
|
+
await executeCLI('user', ['entry'], env as any, async () => {
|
|
814
|
+
throw new Error('dns failed')
|
|
815
|
+
})
|
|
816
|
+
}, (error: unknown) => {
|
|
817
|
+
assert.ok(error instanceof CLIError)
|
|
818
|
+
assert.equal(error.code, 'UPSTREAM_NETWORK_ERROR')
|
|
819
|
+
assert.equal(error.details?.endpoint, '/api/entry')
|
|
820
|
+
assert.match(error.message, /dns failed/)
|
|
821
|
+
return true
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
|
|
492
825
|
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
493
826
|
await assert.rejects(async () => {
|
|
494
827
|
await executeCLI(
|
|
@@ -502,7 +835,7 @@ test('access-rule update rejects enabled without update whitelist', async () =>
|
|
|
502
835
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
503
836
|
assert.match(error.message, /update.*enabled/)
|
|
504
837
|
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
505
|
-
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/
|
|
838
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/types/ingress.ts'))
|
|
506
839
|
return true
|
|
507
840
|
})
|
|
508
841
|
})
|
|
@@ -518,6 +851,134 @@ test('access-rule update accepts enabled with update whitelist', async () => {
|
|
|
518
851
|
assert.deepEqual(out.data, { update: ['enabled'], enabled: true })
|
|
519
852
|
})
|
|
520
853
|
|
|
854
|
+
test('access-rule update rejects nested options patch with canonical policy example', async () => {
|
|
855
|
+
await assert.rejects(async () => {
|
|
856
|
+
await executeCLI(
|
|
857
|
+
'admin',
|
|
858
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["options.policy"],"options":{"policy":{"key":"custom","actions":["view","add"],"fields":[{"key":"1002","read":false}]}}}'],
|
|
859
|
+
env as any,
|
|
860
|
+
async () => new Response('{}', { status: 200 }),
|
|
861
|
+
)
|
|
862
|
+
}, (error: unknown) => {
|
|
863
|
+
assert.ok(error instanceof CLIError)
|
|
864
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
865
|
+
assert.match(error.message, /update.*options/)
|
|
866
|
+
assert.match(error.message, /:1002/)
|
|
867
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('options.policy')))
|
|
868
|
+
return true
|
|
869
|
+
})
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
test('access-rule update accepts canonical policy body with hidden field', async () => {
|
|
873
|
+
const body = '{"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
|
|
874
|
+
const out = await executeCLI(
|
|
875
|
+
'admin',
|
|
876
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', body],
|
|
877
|
+
env as any,
|
|
878
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
879
|
+
)
|
|
880
|
+
assert.equal(out.kind, 'result')
|
|
881
|
+
assert.deepEqual(out.data.update, ['options'])
|
|
882
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }])
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
test('access-rule update rejects partial options user scope overwrite', async () => {
|
|
886
|
+
await assert.rejects(async () => {
|
|
887
|
+
await executeCLI(
|
|
888
|
+
'admin',
|
|
889
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["options"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}'],
|
|
890
|
+
env as any,
|
|
891
|
+
async () => new Response('{}', { status: 200 }),
|
|
892
|
+
)
|
|
893
|
+
}, (error: unknown) => {
|
|
894
|
+
assert.ok(error instanceof CLIError)
|
|
895
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
896
|
+
assert.match(error.message, /options.*policy/)
|
|
897
|
+
assert.match(error.message, /assign-users/)
|
|
898
|
+
return true
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
test('access-rule update rejects FieldVO key names in policy fields', async () => {
|
|
903
|
+
await assert.rejects(async () => {
|
|
904
|
+
await executeCLI(
|
|
905
|
+
'admin',
|
|
906
|
+
['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":false,"all_fields_write":false,"fields":[{"key":"vote_description","read":false,"write":false,"report":false}]},"list_options":{}}}'],
|
|
907
|
+
env as any,
|
|
908
|
+
async () => new Response('{}', { status: 200 }),
|
|
909
|
+
)
|
|
910
|
+
}, (error: unknown) => {
|
|
911
|
+
assert.ok(error instanceof CLIError)
|
|
912
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
913
|
+
assert.match(error.message, /:1002/)
|
|
914
|
+
assert.match(error.message, /vote_name|FieldVO\.key/)
|
|
915
|
+
return true
|
|
916
|
+
})
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
test('access-rule create rejects guessed permission aliases', async () => {
|
|
920
|
+
await assert.rejects(async () => {
|
|
921
|
+
await executeCLI(
|
|
922
|
+
'admin',
|
|
923
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"rule","enabled":true,"permissions":["read","write"],"field_visibility":[{"field_id":1002,"visible":false}]}'],
|
|
924
|
+
env as any,
|
|
925
|
+
async () => new Response('{}', { status: 200 }),
|
|
926
|
+
)
|
|
927
|
+
}, (error: unknown) => {
|
|
928
|
+
assert.ok(error instanceof CLIError)
|
|
929
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
930
|
+
assert.match(error.message, /options\.policy/)
|
|
931
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO')
|
|
932
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('field_visibility')))
|
|
933
|
+
return true
|
|
934
|
+
})
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
test('access-rule create rejects read write action aliases', async () => {
|
|
938
|
+
await assert.rejects(async () => {
|
|
939
|
+
await executeCLI(
|
|
940
|
+
'admin',
|
|
941
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"rule","enabled":true,"type":"form","options":{"policy":{"key":"custom","actions":["read","write"]},"list_options":{}}}'],
|
|
942
|
+
env as any,
|
|
943
|
+
async () => new Response('{}', { status: 200 }),
|
|
944
|
+
)
|
|
945
|
+
}, (error: unknown) => {
|
|
946
|
+
assert.ok(error instanceof CLIError)
|
|
947
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
948
|
+
assert.match(error.message, /view.*add.*edit/)
|
|
949
|
+
return true
|
|
950
|
+
})
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
test('access-rule create rejects FieldVO member type in user scope', async () => {
|
|
954
|
+
await assert.rejects(async () => {
|
|
955
|
+
await executeCLI(
|
|
956
|
+
'admin',
|
|
957
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"rule","enabled":true,"type":"form","options":{"user_scope":[{"type":"member","id":2188889901}],"policy":{"key":"custom","actions":["view"],"all_fields_read":true,"all_fields_write":false},"list_options":{}}}'],
|
|
958
|
+
env as any,
|
|
959
|
+
async () => new Response('{}', { status: 200 }),
|
|
960
|
+
)
|
|
961
|
+
}, (error: unknown) => {
|
|
962
|
+
assert.ok(error instanceof CLIError)
|
|
963
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
964
|
+
assert.match(error.message, /user.*department.*actor|FieldVO type "member"/)
|
|
965
|
+
return true
|
|
966
|
+
})
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
test('access-rule create accepts custom policy with hidden field', async () => {
|
|
970
|
+
const body = '{"name":"rule","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889901}],"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}'
|
|
971
|
+
const out = await executeCLI(
|
|
972
|
+
'admin',
|
|
973
|
+
['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body],
|
|
974
|
+
env as any,
|
|
975
|
+
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
976
|
+
)
|
|
977
|
+
assert.equal(out.kind, 'result')
|
|
978
|
+
assert.deepEqual(out.data.options.policy.actions, ['view', 'add', 'edit'])
|
|
979
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }])
|
|
980
|
+
})
|
|
981
|
+
|
|
521
982
|
test('access-rule assign-users rejects users shortcut', async () => {
|
|
522
983
|
await assert.rejects(async () => {
|
|
523
984
|
await executeCLI(
|
|
@@ -555,29 +1016,29 @@ test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
|
555
1016
|
await assert.rejects(async () => {
|
|
556
1017
|
await executeCLI(
|
|
557
1018
|
'admin',
|
|
558
|
-
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"
|
|
1019
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"user","id":2188889901}]}'],
|
|
559
1020
|
env as any,
|
|
560
1021
|
async () => new Response('{}', { status: 200 }),
|
|
561
1022
|
)
|
|
562
1023
|
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
563
1024
|
})
|
|
564
1025
|
|
|
565
|
-
test('access-rule assign-users rejects non-member scope type', async () => {
|
|
1026
|
+
test('access-rule assign-users rejects non-user member scope type', async () => {
|
|
566
1027
|
await assert.rejects(async () => {
|
|
567
1028
|
await executeCLI(
|
|
568
1029
|
'admin',
|
|
569
|
-
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
1030
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":2188889901}]}}'],
|
|
570
1031
|
env as any,
|
|
571
1032
|
async () => new Response('{}', { status: 200 }),
|
|
572
1033
|
)
|
|
573
|
-
}, (error: unknown) => assertAssignUsersGuidance(error, /type.*
|
|
1034
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /type.*user/))
|
|
574
1035
|
})
|
|
575
1036
|
|
|
576
1037
|
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
577
1038
|
await assert.rejects(async () => {
|
|
578
1039
|
await executeCLI(
|
|
579
1040
|
'admin',
|
|
580
|
-
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
1041
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":"zW96OnE0kyJa3B2"}]}}'],
|
|
581
1042
|
env as any,
|
|
582
1043
|
async () => new Response('{}', { status: 200 }),
|
|
583
1044
|
)
|
|
@@ -587,11 +1048,11 @@ test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
|
587
1048
|
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
588
1049
|
const out = await executeCLI(
|
|
589
1050
|
'admin',
|
|
590
|
-
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"
|
|
1051
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}'],
|
|
591
1052
|
env as any,
|
|
592
1053
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
593
1054
|
)
|
|
594
1055
|
assert.equal(out.kind, 'result')
|
|
595
1056
|
assert.deepEqual(out.data.update, ['user_scope'])
|
|
596
|
-
assert.deepEqual(out.data.options.user_scope, [{ type: '
|
|
1057
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 2188889901 }])
|
|
597
1058
|
})
|