@carthooks/arcubase-cli 0.1.9 → 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 +1407 -345
- package/bundle/arcubase.mjs +1407 -345
- 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 +4 -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 +1025 -106
- 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 +446 -32
- package/dist/tests/help.test.js +56 -4
- 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 +4 -0
- package/src/runtime/execute.ts +1249 -129
- 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 +564 -45
- package/src/tests/help.test.ts +51 -7
- 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,53 @@ 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
|
+
|
|
48
|
+
function assertAssignUsersGuidance(error: unknown, messagePattern?: RegExp): boolean {
|
|
49
|
+
assert.ok(error instanceof CLIError)
|
|
50
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
51
|
+
if (messagePattern) {
|
|
52
|
+
assert.match(error.message, messagePattern)
|
|
53
|
+
}
|
|
54
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
55
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
56
|
+
update: ['user_scope'],
|
|
57
|
+
options: {
|
|
58
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
assert.deepEqual(error.details?.commonMistakes, [
|
|
62
|
+
'do not use user_scope at top level',
|
|
63
|
+
'do not use users or allowedUsers',
|
|
64
|
+
'do not use hash tenant user id as id',
|
|
65
|
+
'id must be numeric TenantUser.ID',
|
|
66
|
+
])
|
|
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'))
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
function validEntitySavePayload(overrides: Record<string, unknown> = {}) {
|
|
26
74
|
return {
|
|
27
75
|
id: 2188889845,
|
|
@@ -84,6 +132,15 @@ test('table update-schema rejects invalid body file', async () => {
|
|
|
84
132
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
85
133
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO')
|
|
86
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"')))
|
|
87
144
|
return true
|
|
88
145
|
})
|
|
89
146
|
})
|
|
@@ -100,40 +157,68 @@ test('app create rejects name longer than 20 chars', async () => {
|
|
|
100
157
|
})
|
|
101
158
|
})
|
|
102
159
|
|
|
103
|
-
test('table create
|
|
104
|
-
|
|
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 }> = []
|
|
105
177
|
const out = await executeCLI(
|
|
106
178
|
'admin',
|
|
107
|
-
['table', 'create', '--app-id', '
|
|
179
|
+
['table', 'create', '--app-id', '2188889844', '--body-file', bodyFile],
|
|
108
180
|
env as any,
|
|
109
|
-
async (url, init) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
},
|
|
116
188
|
)
|
|
117
189
|
assert.equal(out.kind, 'result')
|
|
118
|
-
assert.equal(out.data.
|
|
119
|
-
assert.equal(
|
|
120
|
-
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)
|
|
121
200
|
})
|
|
122
201
|
|
|
123
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 })
|
|
124
205
|
const out = await executeCLI(
|
|
125
206
|
'admin',
|
|
126
|
-
['table', 'create', '--app-id', '
|
|
207
|
+
['table', 'create', '--app-id', '2188889844', '--body-json', payload],
|
|
127
208
|
env as any,
|
|
128
|
-
async (url, init) =>
|
|
129
|
-
url,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
},
|
|
133
216
|
)
|
|
134
217
|
assert.equal(out.kind, 'result')
|
|
135
|
-
assert.equal(out.data.
|
|
136
|
-
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)
|
|
137
222
|
})
|
|
138
223
|
|
|
139
224
|
test('body json conflicts with body file', async () => {
|
|
@@ -157,6 +242,22 @@ test('body json is rejected for commands without request type', async () => {
|
|
|
157
242
|
})
|
|
158
243
|
})
|
|
159
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
|
+
|
|
160
261
|
test('body json parse errors fail fast', async () => {
|
|
161
262
|
await assert.rejects(async () => {
|
|
162
263
|
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env as any, async () => new Response('{}', { status: 200 }))
|
|
@@ -167,6 +268,19 @@ test('body json parse errors fail fast', async () => {
|
|
|
167
268
|
})
|
|
168
269
|
})
|
|
169
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
|
+
|
|
170
284
|
test('access-rule list executes against external root path', async () => {
|
|
171
285
|
const out = await executeCLI(
|
|
172
286
|
'admin',
|
|
@@ -231,6 +345,36 @@ test('table update-schema normalizes shell null placeholders before validation',
|
|
|
231
345
|
assert.deepEqual(requestBody.options.misc, {})
|
|
232
346
|
})
|
|
233
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
|
+
|
|
234
378
|
test('row delete rejects selection type outside ids', async () => {
|
|
235
379
|
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } })
|
|
236
380
|
await assert.rejects(async () => {
|
|
@@ -252,7 +396,7 @@ test('row bulk-update accepts ids selection with number_add action', async () =>
|
|
|
252
396
|
'user',
|
|
253
397
|
['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
254
398
|
env as any,
|
|
255
|
-
|
|
399
|
+
withRowPolicyResolver(),
|
|
256
400
|
)
|
|
257
401
|
assert.equal(out.kind, 'result')
|
|
258
402
|
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 })
|
|
@@ -272,7 +416,7 @@ test('row selection-action accepts condition selection with selectAll', async ()
|
|
|
272
416
|
'user',
|
|
273
417
|
['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile],
|
|
274
418
|
env as any,
|
|
275
|
-
|
|
419
|
+
withRowPolicyResolver(),
|
|
276
420
|
)
|
|
277
421
|
assert.equal(out.kind, 'result')
|
|
278
422
|
assert.equal(out.data.selection.type, 'condition')
|
|
@@ -291,7 +435,139 @@ test('row selection-action rejects condition selection for query action', async
|
|
|
291
435
|
})
|
|
292
436
|
})
|
|
293
437
|
|
|
294
|
-
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 () => {
|
|
295
571
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } })
|
|
296
572
|
const calls: Array<{ url: string; method?: string }> = []
|
|
297
573
|
await assert.rejects(async () => {
|
|
@@ -311,12 +587,31 @@ test('row create rejects local file path for file field and points to arcubase u
|
|
|
311
587
|
assert.ok(error instanceof CLIError)
|
|
312
588
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
313
589
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'))
|
|
314
|
-
assert.match(error.message, /
|
|
590
|
+
assert.match(error.message, /upload token flow/)
|
|
315
591
|
return true
|
|
316
592
|
})
|
|
317
593
|
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }])
|
|
318
594
|
})
|
|
319
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
|
+
|
|
320
615
|
test('row update rejects object value for image field and requires upload array', async () => {
|
|
321
616
|
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } })
|
|
322
617
|
await assert.rejects(async () => {
|
|
@@ -335,7 +630,7 @@ test('row update rejects object value for image field and requires upload array'
|
|
|
335
630
|
assert.ok(error instanceof CLIError)
|
|
336
631
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
337
632
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'))
|
|
338
|
-
assert.match(error.message, /
|
|
633
|
+
assert.match(error.message, /upload value array/)
|
|
339
634
|
return true
|
|
340
635
|
})
|
|
341
636
|
})
|
|
@@ -347,13 +642,13 @@ test('row create accepts upload array for file field after schema preflight', as
|
|
|
347
642
|
'user',
|
|
348
643
|
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
349
644
|
env as any,
|
|
350
|
-
async (url, init) => {
|
|
645
|
+
withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
351
646
|
calls.push({ url: String(url), method: init?.method })
|
|
352
647
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
353
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 })
|
|
354
649
|
}
|
|
355
650
|
return new Response(String(init?.body ?? '{}'), { status: 200 })
|
|
356
|
-
},
|
|
651
|
+
}),
|
|
357
652
|
)
|
|
358
653
|
assert.equal(out.kind, 'result')
|
|
359
654
|
assert.deepEqual(calls, [
|
|
@@ -370,7 +665,7 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
370
665
|
'user',
|
|
371
666
|
['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile],
|
|
372
667
|
env as any,
|
|
373
|
-
async (url, init) => {
|
|
668
|
+
withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
374
669
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
375
670
|
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 })
|
|
376
671
|
}
|
|
@@ -382,14 +677,14 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
382
677
|
},
|
|
383
678
|
trace_id: 'req_upload_bind',
|
|
384
679
|
}), { status: 200 })
|
|
385
|
-
},
|
|
680
|
+
}),
|
|
386
681
|
)
|
|
387
682
|
}, (error: unknown) => {
|
|
388
683
|
assert.ok(error instanceof CLIError)
|
|
389
684
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR')
|
|
390
685
|
assert.equal(error.details?.traceId, 'req_upload_bind')
|
|
391
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')
|
|
392
|
-
assert.match(error.details?.hint ?? '', /
|
|
687
|
+
assert.match(error.details?.hint ?? '', /upload token flow/)
|
|
393
688
|
return true
|
|
394
689
|
})
|
|
395
690
|
})
|
|
@@ -406,6 +701,30 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
406
701
|
})
|
|
407
702
|
})
|
|
408
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
|
+
|
|
409
728
|
test('row update rejects fields key', async () => {
|
|
410
729
|
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] })
|
|
411
730
|
await assert.rejects(async () => {
|
|
@@ -431,7 +750,7 @@ test('row update rejects changed_fields that are missing from data', async () =>
|
|
|
431
750
|
})
|
|
432
751
|
|
|
433
752
|
test('workflow approve maps to fixed action path', async () => {
|
|
434
|
-
const bodyFile = tempJSONFile({ task_id:
|
|
753
|
+
const bodyFile = tempJSONFile({ task_id: 2188890521, fields: {}, changed: [] })
|
|
435
754
|
const out = await executeCLI(
|
|
436
755
|
'user',
|
|
437
756
|
['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile],
|
|
@@ -440,7 +759,7 @@ test('workflow approve maps to fixed action path', async () => {
|
|
|
440
759
|
)
|
|
441
760
|
assert.equal(out.kind, 'result')
|
|
442
761
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2')
|
|
443
|
-
assert.equal(out.data.body.task_id,
|
|
762
|
+
assert.equal(out.data.body.task_id, 2188890521)
|
|
444
763
|
})
|
|
445
764
|
|
|
446
765
|
test('workflow query-approve-users requires request body', async () => {
|
|
@@ -453,10 +772,22 @@ test('workflow query-approve-users requires request body', async () => {
|
|
|
453
772
|
})
|
|
454
773
|
})
|
|
455
774
|
|
|
456
|
-
test('entry
|
|
775
|
+
test('entry always includes app tables without exposing a query flag', async () => {
|
|
776
|
+
const out = await executeCLI(
|
|
777
|
+
'user',
|
|
778
|
+
['entry'],
|
|
779
|
+
env as any,
|
|
780
|
+
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
781
|
+
)
|
|
782
|
+
assert.equal(out.kind, 'result')
|
|
783
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
test('entry fixed app tables query cannot be disabled by query file', async () => {
|
|
787
|
+
const queryFile = tempJSONFile({ include_app_tables: false })
|
|
457
788
|
const out = await executeCLI(
|
|
458
789
|
'user',
|
|
459
|
-
['entry', '--
|
|
790
|
+
['entry', '--query-file', queryFile],
|
|
460
791
|
env as any,
|
|
461
792
|
async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }),
|
|
462
793
|
)
|
|
@@ -464,6 +795,33 @@ test('entry passes include_app_tables query flag', async () => {
|
|
|
464
795
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true')
|
|
465
796
|
})
|
|
466
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
|
+
|
|
467
825
|
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
468
826
|
await assert.rejects(async () => {
|
|
469
827
|
await executeCLI(
|
|
@@ -477,7 +835,7 @@ test('access-rule update rejects enabled without update whitelist', async () =>
|
|
|
477
835
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
478
836
|
assert.match(error.message, /update.*enabled/)
|
|
479
837
|
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO')
|
|
480
|
-
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'))
|
|
481
839
|
return true
|
|
482
840
|
})
|
|
483
841
|
})
|
|
@@ -493,47 +851,208 @@ test('access-rule update accepts enabled with update whitelist', async () => {
|
|
|
493
851
|
assert.deepEqual(out.data, { update: ['enabled'], enabled: true })
|
|
494
852
|
})
|
|
495
853
|
|
|
496
|
-
test('access-rule
|
|
854
|
+
test('access-rule update rejects nested options patch with canonical policy example', async () => {
|
|
497
855
|
await assert.rejects(async () => {
|
|
498
856
|
await executeCLI(
|
|
499
857
|
'admin',
|
|
500
|
-
['access-rule', '
|
|
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}]}}}'],
|
|
501
859
|
env as any,
|
|
502
860
|
async () => new Response('{}', { status: 200 }),
|
|
503
861
|
)
|
|
504
862
|
}, (error: unknown) => {
|
|
505
863
|
assert.ok(error instanceof CLIError)
|
|
506
864
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
507
|
-
assert.match(error.message, /options
|
|
508
|
-
assert.
|
|
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')))
|
|
509
868
|
return true
|
|
510
869
|
})
|
|
511
870
|
})
|
|
512
871
|
|
|
513
|
-
test('access-rule
|
|
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 () => {
|
|
514
886
|
await assert.rejects(async () => {
|
|
515
887
|
await executeCLI(
|
|
516
888
|
'admin',
|
|
517
|
-
['access-rule', '
|
|
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":{}}}'],
|
|
518
958
|
env as any,
|
|
519
959
|
async () => new Response('{}', { status: 200 }),
|
|
520
960
|
)
|
|
521
961
|
}, (error: unknown) => {
|
|
522
962
|
assert.ok(error instanceof CLIError)
|
|
523
963
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED')
|
|
524
|
-
assert.match(error.message, /
|
|
964
|
+
assert.match(error.message, /user.*department.*actor|FieldVO type "member"/)
|
|
525
965
|
return true
|
|
526
966
|
})
|
|
527
967
|
})
|
|
528
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
|
+
|
|
982
|
+
test('access-rule assign-users rejects users shortcut', async () => {
|
|
983
|
+
await assert.rejects(async () => {
|
|
984
|
+
await executeCLI(
|
|
985
|
+
'admin',
|
|
986
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'],
|
|
987
|
+
env as any,
|
|
988
|
+
async () => new Response('{}', { status: 200 }),
|
|
989
|
+
)
|
|
990
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
994
|
+
await assert.rejects(async () => {
|
|
995
|
+
await executeCLI(
|
|
996
|
+
'admin',
|
|
997
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'],
|
|
998
|
+
env as any,
|
|
999
|
+
async () => new Response('{}', { status: 200 }),
|
|
1000
|
+
)
|
|
1001
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /non-empty/))
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
test('access-rule assign-users rejects allowedUsers shortcut', async () => {
|
|
1005
|
+
await assert.rejects(async () => {
|
|
1006
|
+
await executeCLI(
|
|
1007
|
+
'admin',
|
|
1008
|
+
['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"allowedUsers":["tu_1"]}'],
|
|
1009
|
+
env as any,
|
|
1010
|
+
async () => new Response('{}', { status: 200 }),
|
|
1011
|
+
)
|
|
1012
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
1016
|
+
await assert.rejects(async () => {
|
|
1017
|
+
await executeCLI(
|
|
1018
|
+
'admin',
|
|
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}]}'],
|
|
1020
|
+
env as any,
|
|
1021
|
+
async () => new Response('{}', { status: 200 }),
|
|
1022
|
+
)
|
|
1023
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /options.user_scope/))
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
test('access-rule assign-users rejects non-user member scope type', async () => {
|
|
1027
|
+
await assert.rejects(async () => {
|
|
1028
|
+
await executeCLI(
|
|
1029
|
+
'admin',
|
|
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}]}}'],
|
|
1031
|
+
env as any,
|
|
1032
|
+
async () => new Response('{}', { status: 200 }),
|
|
1033
|
+
)
|
|
1034
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /type.*user/))
|
|
1035
|
+
})
|
|
1036
|
+
|
|
1037
|
+
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
1038
|
+
await assert.rejects(async () => {
|
|
1039
|
+
await executeCLI(
|
|
1040
|
+
'admin',
|
|
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"}]}}'],
|
|
1042
|
+
env as any,
|
|
1043
|
+
async () => new Response('{}', { status: 200 }),
|
|
1044
|
+
)
|
|
1045
|
+
}, (error: unknown) => assertAssignUsersGuidance(error, /numeric TenantUser.ID/))
|
|
1046
|
+
})
|
|
1047
|
+
|
|
529
1048
|
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
530
1049
|
const out = await executeCLI(
|
|
531
1050
|
'admin',
|
|
532
|
-
['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":
|
|
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}]}}'],
|
|
533
1052
|
env as any,
|
|
534
1053
|
async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }),
|
|
535
1054
|
)
|
|
536
1055
|
assert.equal(out.kind, 'result')
|
|
537
1056
|
assert.deepEqual(out.data.update, ['user_scope'])
|
|
538
|
-
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id:
|
|
1057
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 2188889901 }])
|
|
539
1058
|
})
|