@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
|
@@ -8,6 +8,7 @@ import { CLIError } from '../runtime/errors.js';
|
|
|
8
8
|
const env = {
|
|
9
9
|
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
10
10
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
11
|
+
ARCUBASE_SDK_ROOT: '/runtime/arcubase-sdk',
|
|
11
12
|
ARCUBASE_TRACE_ID: '',
|
|
12
13
|
ARCUBASE_SUBJECT_TYPE: '',
|
|
13
14
|
ARCUBASE_SUBJECT_ID: '',
|
|
@@ -19,6 +20,46 @@ function tempJSONFile(payload) {
|
|
|
19
20
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
|
20
21
|
return filePath;
|
|
21
22
|
}
|
|
23
|
+
function withRowPolicyResolver(tableId = 'table_1', actions = ['view', 'add', 'edit', 'delete', 'bulk_update', 'archive'], policyOverridesOrFinal = {}, final) {
|
|
24
|
+
const policyOverrides = typeof policyOverridesOrFinal === 'function' ? {} : policyOverridesOrFinal;
|
|
25
|
+
const finalFetch = typeof policyOverridesOrFinal === 'function'
|
|
26
|
+
? policyOverridesOrFinal
|
|
27
|
+
: final ?? (async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
28
|
+
return async (url, init) => {
|
|
29
|
+
const text = String(url);
|
|
30
|
+
if (/\/api\/apps\/[^/?]+$/.test(text) && init?.method === 'GET') {
|
|
31
|
+
return new Response(JSON.stringify({ data: { ingress: { groups: [{ items: [{ id: 'policy_hash_1' }] }] } } }), { status: 200 });
|
|
32
|
+
}
|
|
33
|
+
if (/\/api\/ingress\/[^/]+\/policy_hash_1$/.test(text) && init?.method === 'GET') {
|
|
34
|
+
return new Response(JSON.stringify({ data: { id: tableId, policy: { actions, ...policyOverrides } } }), { status: 200 });
|
|
35
|
+
}
|
|
36
|
+
return finalFetch(url, init);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function assertAssignUsersGuidance(error, messagePattern) {
|
|
40
|
+
assert.ok(error instanceof CLIError);
|
|
41
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
42
|
+
if (messagePattern) {
|
|
43
|
+
assert.match(error.message, messagePattern);
|
|
44
|
+
}
|
|
45
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
46
|
+
assert.deepEqual(error.details?.shapeHint, {
|
|
47
|
+
update: ['user_scope'],
|
|
48
|
+
options: {
|
|
49
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(error.details?.commonMistakes, [
|
|
53
|
+
'do not use user_scope at top level',
|
|
54
|
+
'do not use users or allowedUsers',
|
|
55
|
+
'do not use hash tenant user id as id',
|
|
56
|
+
'id must be numeric TenantUser.ID',
|
|
57
|
+
]);
|
|
58
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'));
|
|
59
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/runtime/arcubase-sdk/types/ingress.ts'));
|
|
60
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/runtime/arcubase-sdk/types/common.ts'));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
22
63
|
function validEntitySavePayload(overrides = {}) {
|
|
23
64
|
return {
|
|
24
65
|
id: 2188889845,
|
|
@@ -80,6 +121,15 @@ test('table update-schema rejects invalid body file', async () => {
|
|
|
80
121
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
81
122
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO');
|
|
82
123
|
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')));
|
|
124
|
+
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntitySaveReqVO' && item.file === '/runtime/arcubase-sdk/types/entity.ts'));
|
|
125
|
+
assert.ok(error.details?.docHints?.some((item) => item.file === '/runtime/arcubase-sdk/docs/runtime-reference/entity-schema.md'));
|
|
126
|
+
assert.equal(error.details?.shapeHint?.field_id_seq, 1004);
|
|
127
|
+
assert.equal(error.details?.shapeHint?.fields?.[0]?.type, 'text');
|
|
128
|
+
assert.equal(error.details?.shapeHint?.fields?.[1]?.type, 'textarea');
|
|
129
|
+
assert.equal(error.details?.shapeHint?.fields?.[2]?.type, 'select');
|
|
130
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('fields must be an array')));
|
|
131
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('get table shell first')));
|
|
132
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('not "string"')));
|
|
83
133
|
return true;
|
|
84
134
|
});
|
|
85
135
|
});
|
|
@@ -94,28 +144,56 @@ test('app create rejects name longer than 20 chars', async () => {
|
|
|
94
144
|
return true;
|
|
95
145
|
});
|
|
96
146
|
});
|
|
97
|
-
test('table create
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
147
|
+
test('table create rejects empty schema body before request', async () => {
|
|
148
|
+
await assert.rejects(async () => {
|
|
149
|
+
await executeCLI('admin', ['table', 'create', '--app-id', '2188889844', '--body-json', '{"name":"订单","parent":0}'], env, async () => new Response('{}'));
|
|
150
|
+
}, (error) => {
|
|
151
|
+
assert.ok(error instanceof CLIError);
|
|
152
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
153
|
+
assert.match(error.message, /fields/);
|
|
154
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase-admin table create --app-id <app_id> --body-json')));
|
|
155
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"field_id_seq":1003')));
|
|
156
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('"投票描述"')));
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
test('table create executes create and schema save with valid body file', async () => {
|
|
161
|
+
const bodyFile = tempJSONFile({ ...validEntitySavePayload({ id: undefined, app_id: undefined }), name: '订单', parent: 0 });
|
|
162
|
+
const calls = [];
|
|
163
|
+
const out = await executeCLI('admin', ['table', 'create', '--app-id', '2188889844', '--body-file', bodyFile], env, async (url, init) => {
|
|
164
|
+
calls.push({ url: String(url), method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null });
|
|
165
|
+
if (init?.method === 'POST') {
|
|
166
|
+
return new Response(JSON.stringify({ data: { id: 2188889845 } }), { status: 200 });
|
|
167
|
+
}
|
|
168
|
+
return new Response(JSON.stringify({ data: { id: 2188889845, fields: calls[1]?.body?.fields ?? [] } }), { status: 200 });
|
|
169
|
+
});
|
|
105
170
|
assert.equal(out.kind, 'result');
|
|
106
|
-
assert.equal(out.data.
|
|
107
|
-
assert.equal(
|
|
108
|
-
assert.equal(
|
|
171
|
+
assert.equal(out.data.id, 2188889845);
|
|
172
|
+
assert.equal(calls.length, 2);
|
|
173
|
+
assert.equal(calls[0].url, 'https://arcubase.example.com/api/apps/2188889844/entities');
|
|
174
|
+
assert.equal(calls[0].method, 'POST');
|
|
175
|
+
assert.deepEqual(calls[0].body, { name: '订单', parent: 0 });
|
|
176
|
+
assert.equal(calls[1].url, 'https://arcubase.example.com/api/apps/2188889844/entity/2188889845');
|
|
177
|
+
assert.equal(calls[1].method, 'PUT');
|
|
178
|
+
assert.equal(calls[1].body.id, 2188889845);
|
|
179
|
+
assert.equal(calls[1].body.app_id, 2188889844);
|
|
180
|
+
assert.equal(calls[1].body.fields.length, 1);
|
|
109
181
|
});
|
|
110
182
|
test('table create executes with valid body json', async () => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
body: init?.body ? JSON.parse(String(init.body)) : null
|
|
115
|
-
|
|
183
|
+
const calls = [];
|
|
184
|
+
const payload = JSON.stringify({ ...validEntitySavePayload({ id: undefined, app_id: undefined }), name: '订单', parent: 0 });
|
|
185
|
+
const out = await executeCLI('admin', ['table', 'create', '--app-id', '2188889844', '--body-json', payload], env, async (url, init) => {
|
|
186
|
+
calls.push({ url: String(url), method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null });
|
|
187
|
+
if (init?.method === 'POST') {
|
|
188
|
+
return new Response(JSON.stringify({ data: { id: 2188889845 } }), { status: 200 });
|
|
189
|
+
}
|
|
190
|
+
return new Response(JSON.stringify({ data: { id: 2188889845, fields: calls[1]?.body?.fields ?? [] } }), { status: 200 });
|
|
191
|
+
});
|
|
116
192
|
assert.equal(out.kind, 'result');
|
|
117
|
-
assert.equal(out.data.
|
|
118
|
-
assert.
|
|
193
|
+
assert.equal(out.data.id, 2188889845);
|
|
194
|
+
assert.equal(calls.length, 2);
|
|
195
|
+
assert.deepEqual(calls[0].body, { name: '订单', parent: 0 });
|
|
196
|
+
assert.equal(calls[1].body.id, 2188889845);
|
|
119
197
|
});
|
|
120
198
|
test('body json conflicts with body file', async () => {
|
|
121
199
|
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
@@ -136,6 +214,21 @@ test('body json is rejected for commands without request type', async () => {
|
|
|
136
214
|
return true;
|
|
137
215
|
});
|
|
138
216
|
});
|
|
217
|
+
test('entry rejects retired include-app-tables flag before fetch', async () => {
|
|
218
|
+
let called = false;
|
|
219
|
+
await assert.rejects(async () => {
|
|
220
|
+
await executeCLI('user', ['entry', '--include-app-tables'], env, async () => {
|
|
221
|
+
called = true;
|
|
222
|
+
return new Response('{}', { status: 200 });
|
|
223
|
+
});
|
|
224
|
+
}, (error) => {
|
|
225
|
+
assert.ok(error instanceof CLIError);
|
|
226
|
+
assert.equal(error.code, 'UNKNOWN_FLAG');
|
|
227
|
+
assert.match(error.message, /--include-app-tables/);
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
assert.equal(called, false);
|
|
231
|
+
});
|
|
139
232
|
test('body json parse errors fail fast', async () => {
|
|
140
233
|
await assert.rejects(async () => {
|
|
141
234
|
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env, async () => new Response('{}', { status: 200 }));
|
|
@@ -145,6 +238,18 @@ test('body json parse errors fail fast', async () => {
|
|
|
145
238
|
return true;
|
|
146
239
|
});
|
|
147
240
|
});
|
|
241
|
+
test('body json parse errors include command docs and type hints', async () => {
|
|
242
|
+
await assert.rejects(async () => {
|
|
243
|
+
await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["options"]}]}'], env, async () => new Response('{}', { status: 200 }));
|
|
244
|
+
}, (error) => {
|
|
245
|
+
assert.ok(error instanceof CLIError);
|
|
246
|
+
assert.equal(error.code, 'INVALID_BODY_JSON');
|
|
247
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
248
|
+
assert.ok((error.details?.docHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/docs/runtime-reference/access-rule.md'));
|
|
249
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('access-rule update policy changes')));
|
|
250
|
+
return true;
|
|
251
|
+
});
|
|
252
|
+
});
|
|
148
253
|
test('access-rule list executes against external root path', async () => {
|
|
149
254
|
const out = await executeCLI('admin', ['access-rule', 'list', '--app-id', 'app_1', '--table-id', 'table_1'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
150
255
|
assert.equal(out.kind, 'result');
|
|
@@ -191,6 +296,30 @@ test('table update-schema normalizes shell null placeholders before validation',
|
|
|
191
296
|
assert.deepEqual(requestBody.options.TitleFormat.Parts, []);
|
|
192
297
|
assert.deepEqual(requestBody.options.misc, {});
|
|
193
298
|
});
|
|
299
|
+
test('table update-schema rejects per-user permission patches', async () => {
|
|
300
|
+
const body = JSON.stringify({
|
|
301
|
+
id: 2188893443,
|
|
302
|
+
app_id: 2188893436,
|
|
303
|
+
name: '投票项目',
|
|
304
|
+
schema_version: 1,
|
|
305
|
+
field_id_seq: 1003,
|
|
306
|
+
layout: [[1001], [1002]],
|
|
307
|
+
fields: [
|
|
308
|
+
{ 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 },
|
|
309
|
+
{ 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 } } },
|
|
310
|
+
],
|
|
311
|
+
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
312
|
+
workflow_enabled: false,
|
|
313
|
+
});
|
|
314
|
+
await assert.rejects(async () => {
|
|
315
|
+
await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body], env, async () => new Response('{}', { status: 200 }));
|
|
316
|
+
}, (error) => {
|
|
317
|
+
assert.ok(error instanceof CLIError);
|
|
318
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
319
|
+
assert.match(error.message, /access-rule/);
|
|
320
|
+
return true;
|
|
321
|
+
});
|
|
322
|
+
});
|
|
194
323
|
test('row delete rejects selection type outside ids', async () => {
|
|
195
324
|
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } });
|
|
196
325
|
await assert.rejects(async () => {
|
|
@@ -207,7 +336,7 @@ test('row bulk-update accepts ids selection with number_add action', async () =>
|
|
|
207
336
|
selection: { type: 'ids', ids: [4000000003], length: 1 },
|
|
208
337
|
fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
|
|
209
338
|
});
|
|
210
|
-
const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env,
|
|
339
|
+
const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, withRowPolicyResolver());
|
|
211
340
|
assert.equal(out.kind, 'result');
|
|
212
341
|
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 });
|
|
213
342
|
assert.equal(out.data.fields[0].action.type, 'number_add');
|
|
@@ -221,7 +350,7 @@ test('row selection-action accepts condition selection with selectAll', async ()
|
|
|
221
350
|
sorts: [{ column: ':1002', order: 'desc' }],
|
|
222
351
|
},
|
|
223
352
|
});
|
|
224
|
-
const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env,
|
|
353
|
+
const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env, withRowPolicyResolver());
|
|
225
354
|
assert.equal(out.kind, 'result');
|
|
226
355
|
assert.equal(out.data.selection.type, 'condition');
|
|
227
356
|
assert.equal(out.data.selection.condition.selectAll, true);
|
|
@@ -237,7 +366,93 @@ test('row selection-action rejects condition selection for query action', async
|
|
|
237
366
|
return true;
|
|
238
367
|
});
|
|
239
368
|
});
|
|
240
|
-
test('row create
|
|
369
|
+
test('row create resolves internal policy_id from accessible ingress', async () => {
|
|
370
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } });
|
|
371
|
+
const calls = [];
|
|
372
|
+
const out = await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view', 'add'], async (url, init) => {
|
|
373
|
+
calls.push({
|
|
374
|
+
url: String(url),
|
|
375
|
+
method: init?.method,
|
|
376
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
377
|
+
});
|
|
378
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
379
|
+
}));
|
|
380
|
+
assert.equal(out.kind, 'result');
|
|
381
|
+
assert.deepEqual(calls, [
|
|
382
|
+
{ url: 'https://arcubase.example.com/api/entity/2188900691/2188900694', method: 'GET', body: null },
|
|
383
|
+
{
|
|
384
|
+
url: 'https://arcubase.example.com/api/entity/2188900691/2188900694/insert',
|
|
385
|
+
method: 'POST',
|
|
386
|
+
body: { fields: { 1001: '今天吃什么' }, policy_id: 'policy_hash_1' },
|
|
387
|
+
},
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
test('row get resolves internal policy_id as query parameter', async () => {
|
|
391
|
+
const calls = [];
|
|
392
|
+
const out = await executeCLI('user', ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'], env, withRowPolicyResolver('2188900694', ['view'], async (url) => {
|
|
393
|
+
calls.push(String(url));
|
|
394
|
+
return new Response(JSON.stringify({ data: { id: 4000000046 } }), { status: 200 });
|
|
395
|
+
}));
|
|
396
|
+
assert.equal(out.kind, 'result');
|
|
397
|
+
assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1');
|
|
398
|
+
});
|
|
399
|
+
test('row command rejects user-provided policy_id', async () => {
|
|
400
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' });
|
|
401
|
+
await assert.rejects(async () => {
|
|
402
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['add']));
|
|
403
|
+
}, (error) => {
|
|
404
|
+
assert.ok(error instanceof CLIError);
|
|
405
|
+
assert.equal(error.code, 'INTERNAL_POLICY_ID_NOT_ALLOWED');
|
|
406
|
+
assert.match(error.message, /internal Arcubase ingress parameter/);
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
test('row command fastfails when no accessible ingress matches action', async () => {
|
|
411
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } });
|
|
412
|
+
await assert.rejects(async () => {
|
|
413
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view']));
|
|
414
|
+
}, (error) => {
|
|
415
|
+
assert.ok(error instanceof CLIError);
|
|
416
|
+
assert.equal(error.code, 'POLICY_ID_RESOLUTION_FAILED');
|
|
417
|
+
assert.match(error.message, /action add/);
|
|
418
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('access-rule')));
|
|
419
|
+
return true;
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
test('row create fastfails before request when payload writes non-writable field', async () => {
|
|
423
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '标题', 1002: '隐藏描述' } });
|
|
424
|
+
const calls = [];
|
|
425
|
+
await assert.rejects(async () => {
|
|
426
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view', 'add'], { all_fields_write: true, fields: [{ key: ':1002', read: false, write: false, report: false }] }, async (url, init) => {
|
|
427
|
+
calls.push({ url: String(url), method: init?.method });
|
|
428
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
429
|
+
}));
|
|
430
|
+
}, (error) => {
|
|
431
|
+
assert.ok(error instanceof CLIError);
|
|
432
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED');
|
|
433
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1002'));
|
|
434
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('remove non-writable field ids')));
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false);
|
|
438
|
+
});
|
|
439
|
+
test('row update fastfails before request when payload writes non-writable field', async () => {
|
|
440
|
+
const bodyFile = tempJSONFile({ data: { 1002: '隐藏描述' }, changed_fields: [1002] });
|
|
441
|
+
const calls = [];
|
|
442
|
+
await assert.rejects(async () => {
|
|
443
|
+
await executeCLI('user', ['row', 'update', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '2188901452', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view', 'edit'], { all_fields_write: false, fields: [{ key: ':1001', read: true, write: true, report: false }, { key: ':1002', read: false, write: false, report: false }] }, async (url, init) => {
|
|
444
|
+
calls.push({ url: String(url), method: init?.method });
|
|
445
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
446
|
+
}));
|
|
447
|
+
}, (error) => {
|
|
448
|
+
assert.ok(error instanceof CLIError);
|
|
449
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED');
|
|
450
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1002'));
|
|
451
|
+
return true;
|
|
452
|
+
});
|
|
453
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false);
|
|
454
|
+
});
|
|
455
|
+
test('row create rejects local file path for file field and points to upload token flow', async () => {
|
|
241
456
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } });
|
|
242
457
|
const calls = [];
|
|
243
458
|
await assert.rejects(async () => {
|
|
@@ -252,11 +467,24 @@ test('row create rejects local file path for file field and points to arcubase u
|
|
|
252
467
|
assert.ok(error instanceof CLIError);
|
|
253
468
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
254
469
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
|
|
255
|
-
assert.match(error.message, /
|
|
470
|
+
assert.match(error.message, /upload token flow/);
|
|
256
471
|
return true;
|
|
257
472
|
});
|
|
258
473
|
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }]);
|
|
259
474
|
});
|
|
475
|
+
test('row create non-numeric field key gives table get retry path', async () => {
|
|
476
|
+
const bodyFile = tempJSONFile({ fields: { 标题: '今天吃什么' } });
|
|
477
|
+
await assert.rejects(async () => {
|
|
478
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
479
|
+
}, (error) => {
|
|
480
|
+
assert.ok(error instanceof CLIError);
|
|
481
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
482
|
+
assert.ok(error.details?.commonMistakes?.some((item) => item.includes('do not use field labels')));
|
|
483
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase table get')));
|
|
484
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase row create')));
|
|
485
|
+
return true;
|
|
486
|
+
});
|
|
487
|
+
});
|
|
260
488
|
test('row update rejects object value for image field and requires upload array', async () => {
|
|
261
489
|
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } });
|
|
262
490
|
await assert.rejects(async () => {
|
|
@@ -270,20 +498,20 @@ test('row update rejects object value for image field and requires upload array'
|
|
|
270
498
|
assert.ok(error instanceof CLIError);
|
|
271
499
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
272
500
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'));
|
|
273
|
-
assert.match(error.message, /
|
|
501
|
+
assert.match(error.message, /upload value array/);
|
|
274
502
|
return true;
|
|
275
503
|
});
|
|
276
504
|
});
|
|
277
505
|
test('row create accepts upload array for file field after schema preflight', async () => {
|
|
278
506
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
279
507
|
const calls = [];
|
|
280
|
-
const out = await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
508
|
+
const out = await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
281
509
|
calls.push({ url: String(url), method: init?.method });
|
|
282
510
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
283
511
|
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 });
|
|
284
512
|
}
|
|
285
513
|
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
286
|
-
});
|
|
514
|
+
}));
|
|
287
515
|
assert.equal(out.kind, 'result');
|
|
288
516
|
assert.deepEqual(calls, [
|
|
289
517
|
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
|
|
@@ -294,7 +522,7 @@ test('row create accepts upload array for file field after schema preflight', as
|
|
|
294
522
|
test('row create decorates upload bind failures with actionable guidance', async () => {
|
|
295
523
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
296
524
|
await assert.rejects(async () => {
|
|
297
|
-
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
525
|
+
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
298
526
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
299
527
|
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
|
|
300
528
|
}
|
|
@@ -306,13 +534,13 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
306
534
|
},
|
|
307
535
|
trace_id: 'req_upload_bind',
|
|
308
536
|
}), { status: 200 });
|
|
309
|
-
});
|
|
537
|
+
}));
|
|
310
538
|
}, (error) => {
|
|
311
539
|
assert.ok(error instanceof CLIError);
|
|
312
540
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
|
|
313
541
|
assert.equal(error.details?.traceId, 'req_upload_bind');
|
|
314
542
|
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');
|
|
315
|
-
assert.match(error.details?.hint ?? '', /
|
|
543
|
+
assert.match(error.details?.hint ?? '', /upload token flow/);
|
|
316
544
|
return true;
|
|
317
545
|
});
|
|
318
546
|
});
|
|
@@ -327,6 +555,28 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
327
555
|
return true;
|
|
328
556
|
});
|
|
329
557
|
});
|
|
558
|
+
test('row query unknown limit flag gives body-json retry path', async () => {
|
|
559
|
+
await assert.rejects(async () => {
|
|
560
|
+
await executeCLI('user', ['row', 'query', '--app-id', '2188900691', '--table-id', '2188900694', '--limit', '1', '--offset', '0'], env, async () => new Response('{}', { status: 200 }));
|
|
561
|
+
}, (error) => {
|
|
562
|
+
assert.ok(error instanceof CLIError);
|
|
563
|
+
assert.equal(error.code, 'UNKNOWN_FLAG');
|
|
564
|
+
assert.equal(error.details?.requestType, 'EntityQueryReqVO');
|
|
565
|
+
assert.deepEqual(error.details?.shapeHint, { limit: 20, offset: 0, search: { text_search: 'SO-1001' } });
|
|
566
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('--body-json')));
|
|
567
|
+
return true;
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
test('table dataset unknown app flag points to table get', async () => {
|
|
571
|
+
await assert.rejects(async () => {
|
|
572
|
+
await executeCLI('user', ['table', 'dataset', '--app-id', '2188900691', '--table-id', '2188900694'], env, async () => new Response('{}', { status: 200 }));
|
|
573
|
+
}, (error) => {
|
|
574
|
+
assert.ok(error instanceof CLIError);
|
|
575
|
+
assert.equal(error.code, 'UNKNOWN_FLAG');
|
|
576
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('table get --app-id')));
|
|
577
|
+
return true;
|
|
578
|
+
});
|
|
579
|
+
});
|
|
330
580
|
test('row update rejects fields key', async () => {
|
|
331
581
|
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] });
|
|
332
582
|
await assert.rejects(async () => {
|
|
@@ -350,11 +600,11 @@ test('row update rejects changed_fields that are missing from data', async () =>
|
|
|
350
600
|
});
|
|
351
601
|
});
|
|
352
602
|
test('workflow approve maps to fixed action path', async () => {
|
|
353
|
-
const bodyFile = tempJSONFile({ task_id:
|
|
603
|
+
const bodyFile = tempJSONFile({ task_id: 2188890521, fields: {}, changed: [] });
|
|
354
604
|
const out = await executeCLI('user', ['workflow', 'approve', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method, body: init?.body ? JSON.parse(String(init.body)) : null }), { status: 200 }));
|
|
355
605
|
assert.equal(out.kind, 'result');
|
|
356
606
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2');
|
|
357
|
-
assert.equal(out.data.body.task_id,
|
|
607
|
+
assert.equal(out.data.body.task_id, 2188890521);
|
|
358
608
|
});
|
|
359
609
|
test('workflow query-approve-users requires request body', async () => {
|
|
360
610
|
await assert.rejects(async () => {
|
|
@@ -365,8 +615,172 @@ test('workflow query-approve-users requires request body', async () => {
|
|
|
365
615
|
return true;
|
|
366
616
|
});
|
|
367
617
|
});
|
|
368
|
-
test('entry
|
|
369
|
-
const out = await executeCLI('user', ['entry'
|
|
618
|
+
test('entry always includes app tables without exposing a query flag', async () => {
|
|
619
|
+
const out = await executeCLI('user', ['entry'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
620
|
+
assert.equal(out.kind, 'result');
|
|
621
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
622
|
+
});
|
|
623
|
+
test('entry fixed app tables query cannot be disabled by query file', async () => {
|
|
624
|
+
const queryFile = tempJSONFile({ include_app_tables: false });
|
|
625
|
+
const out = await executeCLI('user', ['entry', '--query-file', queryFile], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
370
626
|
assert.equal(out.kind, 'result');
|
|
371
627
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
372
628
|
});
|
|
629
|
+
test('upstream http failures include endpoint status and body details', async () => {
|
|
630
|
+
await assert.rejects(async () => {
|
|
631
|
+
await executeCLI('user', ['entry'], env, async () => new Response('', { status: 500 }));
|
|
632
|
+
}, (error) => {
|
|
633
|
+
assert.ok(error instanceof CLIError);
|
|
634
|
+
assert.equal(error.code, 'UPSTREAM_REQUEST_FAILED');
|
|
635
|
+
assert.equal(error.details?.endpoint, '/api/entry');
|
|
636
|
+
assert.equal(error.details?.method, 'GET');
|
|
637
|
+
assert.equal(error.details?.status, 500);
|
|
638
|
+
return true;
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
test('upstream network failures are separated from http failures', async () => {
|
|
642
|
+
await assert.rejects(async () => {
|
|
643
|
+
await executeCLI('user', ['entry'], env, async () => {
|
|
644
|
+
throw new Error('dns failed');
|
|
645
|
+
});
|
|
646
|
+
}, (error) => {
|
|
647
|
+
assert.ok(error instanceof CLIError);
|
|
648
|
+
assert.equal(error.code, 'UPSTREAM_NETWORK_ERROR');
|
|
649
|
+
assert.equal(error.details?.endpoint, '/api/entry');
|
|
650
|
+
assert.match(error.message, /dns failed/);
|
|
651
|
+
return true;
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
655
|
+
await assert.rejects(async () => {
|
|
656
|
+
await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"enabled":true}'], env, async () => new Response('{}', { status: 200 }));
|
|
657
|
+
}, (error) => {
|
|
658
|
+
assert.ok(error instanceof CLIError);
|
|
659
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
660
|
+
assert.match(error.message, /update.*enabled/);
|
|
661
|
+
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
662
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/types/ingress.ts'));
|
|
663
|
+
return true;
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
test('access-rule update accepts enabled with update whitelist', async () => {
|
|
667
|
+
const out = await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["enabled"],"enabled":true}'], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
668
|
+
assert.equal(out.kind, 'result');
|
|
669
|
+
assert.deepEqual(out.data, { update: ['enabled'], enabled: true });
|
|
670
|
+
});
|
|
671
|
+
test('access-rule update rejects nested options patch with canonical policy example', async () => {
|
|
672
|
+
await assert.rejects(async () => {
|
|
673
|
+
await executeCLI('admin', ['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}]}}}'], env, async () => new Response('{}', { status: 200 }));
|
|
674
|
+
}, (error) => {
|
|
675
|
+
assert.ok(error instanceof CLIError);
|
|
676
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
677
|
+
assert.match(error.message, /update.*options/);
|
|
678
|
+
assert.match(error.message, /:1002/);
|
|
679
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('options.policy')));
|
|
680
|
+
return true;
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
test('access-rule update accepts canonical policy body with hidden field', async () => {
|
|
684
|
+
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":{}}}';
|
|
685
|
+
const out = await executeCLI('admin', ['access-rule', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', body], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
686
|
+
assert.equal(out.kind, 'result');
|
|
687
|
+
assert.deepEqual(out.data.update, ['options']);
|
|
688
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }]);
|
|
689
|
+
});
|
|
690
|
+
test('access-rule update rejects partial options user scope overwrite', async () => {
|
|
691
|
+
await assert.rejects(async () => {
|
|
692
|
+
await executeCLI('admin', ['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}]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
693
|
+
}, (error) => {
|
|
694
|
+
assert.ok(error instanceof CLIError);
|
|
695
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
696
|
+
assert.match(error.message, /options.*policy/);
|
|
697
|
+
assert.match(error.message, /assign-users/);
|
|
698
|
+
return true;
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
test('access-rule update rejects FieldVO key names in policy fields', async () => {
|
|
702
|
+
await assert.rejects(async () => {
|
|
703
|
+
await executeCLI('admin', ['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":{}}}'], env, async () => new Response('{}', { status: 200 }));
|
|
704
|
+
}, (error) => {
|
|
705
|
+
assert.ok(error instanceof CLIError);
|
|
706
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
707
|
+
assert.match(error.message, /:1002/);
|
|
708
|
+
assert.match(error.message, /vote_name|FieldVO\.key/);
|
|
709
|
+
return true;
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
test('access-rule create rejects guessed permission aliases', async () => {
|
|
713
|
+
await assert.rejects(async () => {
|
|
714
|
+
await executeCLI('admin', ['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}]}'], env, async () => new Response('{}', { status: 200 }));
|
|
715
|
+
}, (error) => {
|
|
716
|
+
assert.ok(error instanceof CLIError);
|
|
717
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
718
|
+
assert.match(error.message, /options\.policy/);
|
|
719
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO');
|
|
720
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('field_visibility')));
|
|
721
|
+
return true;
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
test('access-rule create rejects read write action aliases', async () => {
|
|
725
|
+
await assert.rejects(async () => {
|
|
726
|
+
await executeCLI('admin', ['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":{}}}'], env, async () => new Response('{}', { status: 200 }));
|
|
727
|
+
}, (error) => {
|
|
728
|
+
assert.ok(error instanceof CLIError);
|
|
729
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
730
|
+
assert.match(error.message, /view.*add.*edit/);
|
|
731
|
+
return true;
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
test('access-rule create rejects FieldVO member type in user scope', async () => {
|
|
735
|
+
await assert.rejects(async () => {
|
|
736
|
+
await executeCLI('admin', ['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":{}}}'], env, async () => new Response('{}', { status: 200 }));
|
|
737
|
+
}, (error) => {
|
|
738
|
+
assert.ok(error instanceof CLIError);
|
|
739
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
740
|
+
assert.match(error.message, /user.*department.*actor|FieldVO type "member"/);
|
|
741
|
+
return true;
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
test('access-rule create accepts custom policy with hidden field', async () => {
|
|
745
|
+
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":{}}}';
|
|
746
|
+
const out = await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
747
|
+
assert.equal(out.kind, 'result');
|
|
748
|
+
assert.deepEqual(out.data.options.policy.actions, ['view', 'add', 'edit']);
|
|
749
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }]);
|
|
750
|
+
});
|
|
751
|
+
test('access-rule assign-users rejects users shortcut', async () => {
|
|
752
|
+
await assert.rejects(async () => {
|
|
753
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"users":["tu_1"]}'], env, async () => new Response('{}', { status: 200 }));
|
|
754
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
755
|
+
});
|
|
756
|
+
test('access-rule assign-users requires update user_scope and non-empty user_scope', async () => {
|
|
757
|
+
await assert.rejects(async () => {
|
|
758
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"update":["user_scope"],"options":{"user_scope":[]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
759
|
+
}, (error) => assertAssignUsersGuidance(error, /non-empty/));
|
|
760
|
+
});
|
|
761
|
+
test('access-rule assign-users rejects allowedUsers shortcut', async () => {
|
|
762
|
+
await assert.rejects(async () => {
|
|
763
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"allowedUsers":["tu_1"]}'], env, async () => new Response('{}', { status: 200 }));
|
|
764
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
765
|
+
});
|
|
766
|
+
test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
767
|
+
await assert.rejects(async () => {
|
|
768
|
+
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"user","id":2188889901}]}'], env, async () => new Response('{}', { status: 200 }));
|
|
769
|
+
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
770
|
+
});
|
|
771
|
+
test('access-rule assign-users rejects non-user member scope type', async () => {
|
|
772
|
+
await assert.rejects(async () => {
|
|
773
|
+
await executeCLI('admin', ['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}]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
774
|
+
}, (error) => assertAssignUsersGuidance(error, /type.*user/));
|
|
775
|
+
});
|
|
776
|
+
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
777
|
+
await assert.rejects(async () => {
|
|
778
|
+
await executeCLI('admin', ['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"}]}}'], env, async () => new Response('{}', { status: 200 }));
|
|
779
|
+
}, (error) => assertAssignUsersGuidance(error, /numeric TenantUser.ID/));
|
|
780
|
+
});
|
|
781
|
+
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
782
|
+
const out = await executeCLI('admin', ['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}]}}'], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
783
|
+
assert.equal(out.kind, 'result');
|
|
784
|
+
assert.deepEqual(out.data.update, ['user_scope']);
|
|
785
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 2188889901 }]);
|
|
786
|
+
});
|