@carthooks/arcubase-cli 0.1.10 → 0.1.13
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 +1365 -375
- package/bundle/arcubase.mjs +1365 -375
- package/dist/generated/command_registry.generated.d.ts +36 -35
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +37 -36
- package/dist/generated/type_index.generated.d.ts +23 -23
- package/dist/generated/type_index.generated.js +23 -23
- package/dist/runtime/entity_save_schema.d.ts.map +1 -1
- package/dist/runtime/entity_save_schema.js +30 -0
- package/dist/runtime/env.d.ts +2 -1
- package/dist/runtime/env.d.ts.map +1 -1
- package/dist/runtime/env.js +10 -1
- package/dist/runtime/errors.d.ts +3 -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 +1046 -134
- package/dist/runtime/paths.d.ts +1 -1
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +9 -2
- package/dist/runtime/upload.d.ts +1 -11
- package/dist/runtime/upload.d.ts.map +1 -1
- package/dist/runtime/upload.js +19 -133
- package/dist/runtime/zod_registry.d.ts +5 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +108 -48
- package/dist/tests/bootstrap.test.js +12 -0
- package/dist/tests/command_registry.test.js +8 -0
- package/dist/tests/docs_readability.test.d.ts +2 -0
- package/dist/tests/docs_readability.test.d.ts.map +1 -0
- package/dist/tests/docs_readability.test.js +87 -0
- package/dist/tests/entity_save_schema.test.js +22 -0
- package/dist/tests/execute_validation.test.js +443 -44
- package/dist/tests/help.test.js +47 -8
- package/dist/tests/upload.test.js +30 -77
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +43 -29
- package/sdk-dist/docs/runtime-reference/access-rule.md +128 -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 +48 -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 +3 -0
- package/src/runtime/execute.ts +1272 -134
- package/src/runtime/paths.ts +11 -2
- package/src/runtime/upload.ts +19 -178
- package/src/runtime/zod_registry.ts +113 -48
- package/src/tests/bootstrap.test.ts +16 -0
- package/src/tests/command_registry.test.ts +9 -0
- package/src/tests/docs_readability.test.ts +96 -0
- package/src/tests/entity_save_schema.test.ts +22 -0
- package/src/tests/execute_validation.test.ts +577 -47
- package/src/tests/help.test.ts +51 -8
- 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,22 @@ 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
|
+
}
|
|
22
39
|
function assertAssignUsersGuidance(error, messagePattern) {
|
|
23
40
|
assert.ok(error instanceof CLIError);
|
|
24
41
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
@@ -29,7 +46,7 @@ function assertAssignUsersGuidance(error, messagePattern) {
|
|
|
29
46
|
assert.deepEqual(error.details?.shapeHint, {
|
|
30
47
|
update: ['user_scope'],
|
|
31
48
|
options: {
|
|
32
|
-
user_scope: [{ type: '
|
|
49
|
+
user_scope: [{ type: 'user', id: 2188889901 }],
|
|
33
50
|
},
|
|
34
51
|
});
|
|
35
52
|
assert.deepEqual(error.details?.commonMistakes, [
|
|
@@ -38,9 +55,9 @@ function assertAssignUsersGuidance(error, messagePattern) {
|
|
|
38
55
|
'do not use hash tenant user id as id',
|
|
39
56
|
'id must be numeric TenantUser.ID',
|
|
40
57
|
]);
|
|
41
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'AppIngressUpdateReqVO' && item.file === '/
|
|
42
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'EntityIngressOptions' && item.file === '/
|
|
43
|
-
assert.ok(error.details?.tsHints?.some((item) => item.symbol === 'PermitUserScope' && item.file === '/
|
|
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'));
|
|
44
61
|
return true;
|
|
45
62
|
}
|
|
46
63
|
function validEntitySavePayload(overrides = {}) {
|
|
@@ -104,6 +121,15 @@ test('table update-schema rejects invalid body file', async () => {
|
|
|
104
121
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
105
122
|
assert.equal(error.details?.requestType, 'EntitySaveReqVO');
|
|
106
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"')));
|
|
107
133
|
return true;
|
|
108
134
|
});
|
|
109
135
|
});
|
|
@@ -118,28 +144,56 @@ test('app create rejects name longer than 20 chars', async () => {
|
|
|
118
144
|
return true;
|
|
119
145
|
});
|
|
120
146
|
});
|
|
121
|
-
test('table create
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
});
|
|
129
170
|
assert.equal(out.kind, 'result');
|
|
130
|
-
assert.equal(out.data.
|
|
131
|
-
assert.equal(
|
|
132
|
-
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);
|
|
133
181
|
});
|
|
134
182
|
test('table create executes with valid body json', async () => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
body: init?.body ? JSON.parse(String(init.body)) : null
|
|
139
|
-
|
|
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
|
+
});
|
|
140
192
|
assert.equal(out.kind, 'result');
|
|
141
|
-
assert.equal(out.data.
|
|
142
|
-
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);
|
|
143
197
|
});
|
|
144
198
|
test('body json conflicts with body file', async () => {
|
|
145
199
|
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
@@ -160,6 +214,21 @@ test('body json is rejected for commands without request type', async () => {
|
|
|
160
214
|
return true;
|
|
161
215
|
});
|
|
162
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
|
+
});
|
|
163
232
|
test('body json parse errors fail fast', async () => {
|
|
164
233
|
await assert.rejects(async () => {
|
|
165
234
|
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env, async () => new Response('{}', { status: 200 }));
|
|
@@ -169,6 +238,49 @@ test('body json parse errors fail fast', async () => {
|
|
|
169
238
|
return true;
|
|
170
239
|
});
|
|
171
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
|
+
});
|
|
253
|
+
test('access-rule invalid json returns copyable create retry examples', async () => {
|
|
254
|
+
await assert.rejects(async () => {
|
|
255
|
+
await executeCLI('admin', ['access-rule', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', '{"name":"Admin full access","enabled":true,"type":"form","options":{"user_scope":[{"type":"user","id":2188889977}],"policy":{"key":"custom","actions":["view","add","edit","delete"],"all_fields_read":true,"all_fields_write":true},"list_options":{}}'], env, async () => new Response('{}', { status: 200 }));
|
|
256
|
+
}, (error) => {
|
|
257
|
+
assert.ok(error instanceof CLIError);
|
|
258
|
+
assert.equal(error.code, 'INVALID_BODY_JSON');
|
|
259
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO');
|
|
260
|
+
assert.match(error.message, /retryToolCalls/);
|
|
261
|
+
const toolCall = (error.details?.retryToolCalls ?? [])[0];
|
|
262
|
+
assert.ok(toolCall);
|
|
263
|
+
assert.equal(toolCall.command, 'access-rule create');
|
|
264
|
+
assert.deepEqual(toolCall.args.slice(0, 5), ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json']);
|
|
265
|
+
const body = JSON.parse(toolCall.args[5]);
|
|
266
|
+
assert.deepEqual(body, {
|
|
267
|
+
name: 'Admin full access',
|
|
268
|
+
enabled: true,
|
|
269
|
+
type: 'form',
|
|
270
|
+
options: {
|
|
271
|
+
user_scope: [{ type: 'user', id: 2188889977 }],
|
|
272
|
+
policy: {
|
|
273
|
+
key: 'custom',
|
|
274
|
+
actions: ['view', 'add', 'edit', 'delete'],
|
|
275
|
+
all_fields_read: true,
|
|
276
|
+
all_fields_write: true,
|
|
277
|
+
},
|
|
278
|
+
list_options: {},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
});
|
|
172
284
|
test('access-rule list executes against external root path', async () => {
|
|
173
285
|
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 }));
|
|
174
286
|
assert.equal(out.kind, 'result');
|
|
@@ -215,6 +327,30 @@ test('table update-schema normalizes shell null placeholders before validation',
|
|
|
215
327
|
assert.deepEqual(requestBody.options.TitleFormat.Parts, []);
|
|
216
328
|
assert.deepEqual(requestBody.options.misc, {});
|
|
217
329
|
});
|
|
330
|
+
test('table update-schema rejects per-user permission patches', async () => {
|
|
331
|
+
const body = JSON.stringify({
|
|
332
|
+
id: 2188893443,
|
|
333
|
+
app_id: 2188893436,
|
|
334
|
+
name: '投票项目',
|
|
335
|
+
schema_version: 1,
|
|
336
|
+
field_id_seq: 1003,
|
|
337
|
+
layout: [[1001], [1002]],
|
|
338
|
+
fields: [
|
|
339
|
+
{ 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 },
|
|
340
|
+
{ 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 } } },
|
|
341
|
+
],
|
|
342
|
+
options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
|
|
343
|
+
workflow_enabled: false,
|
|
344
|
+
});
|
|
345
|
+
await assert.rejects(async () => {
|
|
346
|
+
await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-json', body], env, async () => new Response('{}', { status: 200 }));
|
|
347
|
+
}, (error) => {
|
|
348
|
+
assert.ok(error instanceof CLIError);
|
|
349
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
350
|
+
assert.match(error.message, /access-rule/);
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
});
|
|
218
354
|
test('row delete rejects selection type outside ids', async () => {
|
|
219
355
|
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } });
|
|
220
356
|
await assert.rejects(async () => {
|
|
@@ -231,7 +367,7 @@ test('row bulk-update accepts ids selection with number_add action', async () =>
|
|
|
231
367
|
selection: { type: 'ids', ids: [4000000003], length: 1 },
|
|
232
368
|
fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
|
|
233
369
|
});
|
|
234
|
-
const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env,
|
|
370
|
+
const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, withRowPolicyResolver());
|
|
235
371
|
assert.equal(out.kind, 'result');
|
|
236
372
|
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 });
|
|
237
373
|
assert.equal(out.data.fields[0].action.type, 'number_add');
|
|
@@ -245,7 +381,7 @@ test('row selection-action accepts condition selection with selectAll', async ()
|
|
|
245
381
|
sorts: [{ column: ':1002', order: 'desc' }],
|
|
246
382
|
},
|
|
247
383
|
});
|
|
248
|
-
const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env,
|
|
384
|
+
const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env, withRowPolicyResolver());
|
|
249
385
|
assert.equal(out.kind, 'result');
|
|
250
386
|
assert.equal(out.data.selection.type, 'condition');
|
|
251
387
|
assert.equal(out.data.selection.condition.selectAll, true);
|
|
@@ -261,7 +397,124 @@ test('row selection-action rejects condition selection for query action', async
|
|
|
261
397
|
return true;
|
|
262
398
|
});
|
|
263
399
|
});
|
|
264
|
-
test('row create
|
|
400
|
+
test('row create resolves internal policy_id from accessible ingress', async () => {
|
|
401
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } });
|
|
402
|
+
const calls = [];
|
|
403
|
+
const out = await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view', 'add'], async (url, init) => {
|
|
404
|
+
calls.push({
|
|
405
|
+
url: String(url),
|
|
406
|
+
method: init?.method,
|
|
407
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
408
|
+
});
|
|
409
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
410
|
+
}));
|
|
411
|
+
assert.equal(out.kind, 'result');
|
|
412
|
+
assert.deepEqual(calls, [
|
|
413
|
+
{ url: 'https://arcubase.example.com/api/entity/2188900691/2188900694', method: 'GET', body: null },
|
|
414
|
+
{
|
|
415
|
+
url: 'https://arcubase.example.com/api/entity/2188900691/2188900694/insert',
|
|
416
|
+
method: 'POST',
|
|
417
|
+
body: { fields: { 1001: '今天吃什么' }, policy_id: 'policy_hash_1' },
|
|
418
|
+
},
|
|
419
|
+
]);
|
|
420
|
+
});
|
|
421
|
+
test('row get resolves internal policy_id as query parameter', async () => {
|
|
422
|
+
const calls = [];
|
|
423
|
+
const out = await executeCLI('user', ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'], env, withRowPolicyResolver('2188900694', ['view'], async (url) => {
|
|
424
|
+
calls.push(String(url));
|
|
425
|
+
return new Response(JSON.stringify({ data: { id: 4000000046 } }), { status: 200 });
|
|
426
|
+
}));
|
|
427
|
+
assert.equal(out.kind, 'result');
|
|
428
|
+
assert.equal(calls[0], 'https://arcubase.example.com/api/entity/2188900691/2188900694/row/4000000046?policy_id=policy_hash_1');
|
|
429
|
+
});
|
|
430
|
+
test('row get returns machine-readable row value evidence', async () => {
|
|
431
|
+
const out = await executeCLI('user', ['row', 'get', '--app-id', '2188900691', '--table-id', '2188900694', '--row-id', '4000000046'], env, withRowPolicyResolver('2188900694', ['view'], async () => new Response(JSON.stringify({
|
|
432
|
+
data: {
|
|
433
|
+
record: {
|
|
434
|
+
id: 4000000046,
|
|
435
|
+
fields: { 1001: '复测投票' },
|
|
436
|
+
},
|
|
437
|
+
fieldPermits: {
|
|
438
|
+
all_fields_read: false,
|
|
439
|
+
fields: [
|
|
440
|
+
{ key: ':1001', read: true, write: true, report: false },
|
|
441
|
+
{ key: ':1002', read: false, write: false, report: false },
|
|
442
|
+
{ key: ':1003', read: true, write: true, report: false },
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
}), { status: 200 })));
|
|
447
|
+
assert.deepEqual(out.rowValueEvidence, {
|
|
448
|
+
source: 'row get record.fields',
|
|
449
|
+
returnedFieldIds: ['1001'],
|
|
450
|
+
notReturnedFieldIds: ['1002', '1003'],
|
|
451
|
+
deniedReadFieldIds: ['1002'],
|
|
452
|
+
absentFieldMeaning: 'not returned to current user; no row-value evidence',
|
|
453
|
+
rules: [
|
|
454
|
+
'current row values come only from row query fields or row get record.fields',
|
|
455
|
+
'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
|
|
456
|
+
'a field id absent from returned row fields has no row-value evidence',
|
|
457
|
+
'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
test('row command rejects user-provided policy_id', async () => {
|
|
462
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' }, policy_id: 'manual_policy' });
|
|
463
|
+
await assert.rejects(async () => {
|
|
464
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['add']));
|
|
465
|
+
}, (error) => {
|
|
466
|
+
assert.ok(error instanceof CLIError);
|
|
467
|
+
assert.equal(error.code, 'INTERNAL_POLICY_ID_NOT_ALLOWED');
|
|
468
|
+
assert.match(error.message, /internal Arcubase ingress parameter/);
|
|
469
|
+
return true;
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
test('row command fastfails when no accessible ingress matches action', async () => {
|
|
473
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '今天吃什么' } });
|
|
474
|
+
await assert.rejects(async () => {
|
|
475
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, withRowPolicyResolver('2188900694', ['view']));
|
|
476
|
+
}, (error) => {
|
|
477
|
+
assert.ok(error instanceof CLIError);
|
|
478
|
+
assert.equal(error.code, 'POLICY_ID_RESOLUTION_FAILED');
|
|
479
|
+
assert.match(error.message, /action add/);
|
|
480
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('access-rule')));
|
|
481
|
+
return true;
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
test('row create fastfails before request when payload writes non-writable field', async () => {
|
|
485
|
+
const bodyFile = tempJSONFile({ fields: { 1001: '标题', 1002: '隐藏描述' } });
|
|
486
|
+
const calls = [];
|
|
487
|
+
await assert.rejects(async () => {
|
|
488
|
+
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) => {
|
|
489
|
+
calls.push({ url: String(url), method: init?.method });
|
|
490
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
491
|
+
}));
|
|
492
|
+
}, (error) => {
|
|
493
|
+
assert.ok(error instanceof CLIError);
|
|
494
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED');
|
|
495
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1002'));
|
|
496
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('remove non-writable field ids')));
|
|
497
|
+
return true;
|
|
498
|
+
});
|
|
499
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false);
|
|
500
|
+
});
|
|
501
|
+
test('row update fastfails before request when payload writes non-writable field', async () => {
|
|
502
|
+
const bodyFile = tempJSONFile({ data: { 1002: '隐藏描述' }, changed_fields: [1002] });
|
|
503
|
+
const calls = [];
|
|
504
|
+
await assert.rejects(async () => {
|
|
505
|
+
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) => {
|
|
506
|
+
calls.push({ url: String(url), method: init?.method });
|
|
507
|
+
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
508
|
+
}));
|
|
509
|
+
}, (error) => {
|
|
510
|
+
assert.ok(error instanceof CLIError);
|
|
511
|
+
assert.equal(error.code, 'FIELD_PERMISSION_REQUIRED');
|
|
512
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1002'));
|
|
513
|
+
return true;
|
|
514
|
+
});
|
|
515
|
+
assert.equal(calls.some((call) => call.method !== 'GET'), false);
|
|
516
|
+
});
|
|
517
|
+
test('row create rejects local file path for file field and points to upload token flow', async () => {
|
|
265
518
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } });
|
|
266
519
|
const calls = [];
|
|
267
520
|
await assert.rejects(async () => {
|
|
@@ -276,11 +529,24 @@ test('row create rejects local file path for file field and points to arcubase u
|
|
|
276
529
|
assert.ok(error instanceof CLIError);
|
|
277
530
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
278
531
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
|
|
279
|
-
assert.match(error.message, /
|
|
532
|
+
assert.match(error.message, /upload token flow/);
|
|
280
533
|
return true;
|
|
281
534
|
});
|
|
282
535
|
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }]);
|
|
283
536
|
});
|
|
537
|
+
test('row create non-numeric field key gives table get retry path', async () => {
|
|
538
|
+
const bodyFile = tempJSONFile({ fields: { 标题: '今天吃什么' } });
|
|
539
|
+
await assert.rejects(async () => {
|
|
540
|
+
await executeCLI('user', ['row', 'create', '--app-id', '2188900691', '--table-id', '2188900694', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
541
|
+
}, (error) => {
|
|
542
|
+
assert.ok(error instanceof CLIError);
|
|
543
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
544
|
+
assert.ok(error.details?.commonMistakes?.some((item) => item.includes('do not use field labels')));
|
|
545
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase table get')));
|
|
546
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('arcubase row create')));
|
|
547
|
+
return true;
|
|
548
|
+
});
|
|
549
|
+
});
|
|
284
550
|
test('row update rejects object value for image field and requires upload array', async () => {
|
|
285
551
|
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } });
|
|
286
552
|
await assert.rejects(async () => {
|
|
@@ -294,20 +560,20 @@ test('row update rejects object value for image field and requires upload array'
|
|
|
294
560
|
assert.ok(error instanceof CLIError);
|
|
295
561
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
296
562
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'));
|
|
297
|
-
assert.match(error.message, /
|
|
563
|
+
assert.match(error.message, /upload value array/);
|
|
298
564
|
return true;
|
|
299
565
|
});
|
|
300
566
|
});
|
|
301
567
|
test('row create accepts upload array for file field after schema preflight', async () => {
|
|
302
568
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
303
569
|
const calls = [];
|
|
304
|
-
const out = await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
570
|
+
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) => {
|
|
305
571
|
calls.push({ url: String(url), method: init?.method });
|
|
306
572
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
307
573
|
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 });
|
|
308
574
|
}
|
|
309
575
|
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
310
|
-
});
|
|
576
|
+
}));
|
|
311
577
|
assert.equal(out.kind, 'result');
|
|
312
578
|
assert.deepEqual(calls, [
|
|
313
579
|
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
|
|
@@ -318,7 +584,7 @@ test('row create accepts upload array for file field after schema preflight', as
|
|
|
318
584
|
test('row create decorates upload bind failures with actionable guidance', async () => {
|
|
319
585
|
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
320
586
|
await assert.rejects(async () => {
|
|
321
|
-
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
587
|
+
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, withRowPolicyResolver('table_1', ['add'], async (url, init) => {
|
|
322
588
|
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
323
589
|
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
|
|
324
590
|
}
|
|
@@ -330,13 +596,13 @@ test('row create decorates upload bind failures with actionable guidance', async
|
|
|
330
596
|
},
|
|
331
597
|
trace_id: 'req_upload_bind',
|
|
332
598
|
}), { status: 200 });
|
|
333
|
-
});
|
|
599
|
+
}));
|
|
334
600
|
}, (error) => {
|
|
335
601
|
assert.ok(error instanceof CLIError);
|
|
336
602
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
|
|
337
603
|
assert.equal(error.details?.traceId, 'req_upload_bind');
|
|
338
604
|
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');
|
|
339
|
-
assert.match(error.details?.hint ?? '', /
|
|
605
|
+
assert.match(error.details?.hint ?? '', /upload token flow/);
|
|
340
606
|
return true;
|
|
341
607
|
});
|
|
342
608
|
});
|
|
@@ -351,6 +617,28 @@ test('row query rejects camelCase viewMode and points to row crud doc', async ()
|
|
|
351
617
|
return true;
|
|
352
618
|
});
|
|
353
619
|
});
|
|
620
|
+
test('row query unknown limit flag gives body-json retry path', async () => {
|
|
621
|
+
await assert.rejects(async () => {
|
|
622
|
+
await executeCLI('user', ['row', 'query', '--app-id', '2188900691', '--table-id', '2188900694', '--limit', '1', '--offset', '0'], env, async () => new Response('{}', { status: 200 }));
|
|
623
|
+
}, (error) => {
|
|
624
|
+
assert.ok(error instanceof CLIError);
|
|
625
|
+
assert.equal(error.code, 'UNKNOWN_FLAG');
|
|
626
|
+
assert.equal(error.details?.requestType, 'EntityQueryReqVO');
|
|
627
|
+
assert.deepEqual(error.details?.shapeHint, { limit: 20, offset: 0, search: { text_search: 'SO-1001' } });
|
|
628
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('--body-json')));
|
|
629
|
+
return true;
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
test('table dataset unknown app flag points to table get', async () => {
|
|
633
|
+
await assert.rejects(async () => {
|
|
634
|
+
await executeCLI('user', ['table', 'dataset', '--app-id', '2188900691', '--table-id', '2188900694'], env, async () => new Response('{}', { status: 200 }));
|
|
635
|
+
}, (error) => {
|
|
636
|
+
assert.ok(error instanceof CLIError);
|
|
637
|
+
assert.equal(error.code, 'UNKNOWN_FLAG');
|
|
638
|
+
assert.ok(error.details?.suggestions?.some((item) => item.includes('table get --app-id')));
|
|
639
|
+
return true;
|
|
640
|
+
});
|
|
641
|
+
});
|
|
354
642
|
test('row update rejects fields key', async () => {
|
|
355
643
|
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] });
|
|
356
644
|
await assert.rejects(async () => {
|
|
@@ -374,11 +662,11 @@ test('row update rejects changed_fields that are missing from data', async () =>
|
|
|
374
662
|
});
|
|
375
663
|
});
|
|
376
664
|
test('workflow approve maps to fixed action path', async () => {
|
|
377
|
-
const bodyFile = tempJSONFile({ task_id:
|
|
665
|
+
const bodyFile = tempJSONFile({ task_id: 2188890521, fields: {}, changed: [] });
|
|
378
666
|
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 }));
|
|
379
667
|
assert.equal(out.kind, 'result');
|
|
380
668
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entity/app_1/table_1/row/row_1/action/2');
|
|
381
|
-
assert.equal(out.data.body.task_id,
|
|
669
|
+
assert.equal(out.data.body.task_id, 2188890521);
|
|
382
670
|
});
|
|
383
671
|
test('workflow query-approve-users requires request body', async () => {
|
|
384
672
|
await assert.rejects(async () => {
|
|
@@ -389,11 +677,42 @@ test('workflow query-approve-users requires request body', async () => {
|
|
|
389
677
|
return true;
|
|
390
678
|
});
|
|
391
679
|
});
|
|
392
|
-
test('entry
|
|
393
|
-
const out = await executeCLI('user', ['entry'
|
|
680
|
+
test('entry always includes app tables without exposing a query flag', async () => {
|
|
681
|
+
const out = await executeCLI('user', ['entry'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
682
|
+
assert.equal(out.kind, 'result');
|
|
683
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
684
|
+
});
|
|
685
|
+
test('entry fixed app tables query cannot be disabled by query file', async () => {
|
|
686
|
+
const queryFile = tempJSONFile({ include_app_tables: false });
|
|
687
|
+
const out = await executeCLI('user', ['entry', '--query-file', queryFile], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
394
688
|
assert.equal(out.kind, 'result');
|
|
395
689
|
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
396
690
|
});
|
|
691
|
+
test('upstream http failures include endpoint status and body details', async () => {
|
|
692
|
+
await assert.rejects(async () => {
|
|
693
|
+
await executeCLI('user', ['entry'], env, async () => new Response('', { status: 500 }));
|
|
694
|
+
}, (error) => {
|
|
695
|
+
assert.ok(error instanceof CLIError);
|
|
696
|
+
assert.equal(error.code, 'UPSTREAM_REQUEST_FAILED');
|
|
697
|
+
assert.equal(error.details?.endpoint, '/api/entry');
|
|
698
|
+
assert.equal(error.details?.method, 'GET');
|
|
699
|
+
assert.equal(error.details?.status, 500);
|
|
700
|
+
return true;
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
test('upstream network failures are separated from http failures', async () => {
|
|
704
|
+
await assert.rejects(async () => {
|
|
705
|
+
await executeCLI('user', ['entry'], env, async () => {
|
|
706
|
+
throw new Error('dns failed');
|
|
707
|
+
});
|
|
708
|
+
}, (error) => {
|
|
709
|
+
assert.ok(error instanceof CLIError);
|
|
710
|
+
assert.equal(error.code, 'UPSTREAM_NETWORK_ERROR');
|
|
711
|
+
assert.equal(error.details?.endpoint, '/api/entry');
|
|
712
|
+
assert.match(error.message, /dns failed/);
|
|
713
|
+
return true;
|
|
714
|
+
});
|
|
715
|
+
});
|
|
397
716
|
test('access-rule update rejects enabled without update whitelist', async () => {
|
|
398
717
|
await assert.rejects(async () => {
|
|
399
718
|
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 }));
|
|
@@ -402,7 +721,7 @@ test('access-rule update rejects enabled without update whitelist', async () =>
|
|
|
402
721
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
403
722
|
assert.match(error.message, /update.*enabled/);
|
|
404
723
|
assert.equal(error.details?.requestType, 'AppIngressUpdateReqVO');
|
|
405
|
-
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/
|
|
724
|
+
assert.ok((error.details?.tsHints ?? []).some((item) => item.file === '/runtime/arcubase-sdk/types/ingress.ts'));
|
|
406
725
|
return true;
|
|
407
726
|
});
|
|
408
727
|
});
|
|
@@ -411,6 +730,86 @@ test('access-rule update accepts enabled with update whitelist', async () => {
|
|
|
411
730
|
assert.equal(out.kind, 'result');
|
|
412
731
|
assert.deepEqual(out.data, { update: ['enabled'], enabled: true });
|
|
413
732
|
});
|
|
733
|
+
test('access-rule update rejects nested options patch with canonical policy example', async () => {
|
|
734
|
+
await assert.rejects(async () => {
|
|
735
|
+
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 }));
|
|
736
|
+
}, (error) => {
|
|
737
|
+
assert.ok(error instanceof CLIError);
|
|
738
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
739
|
+
assert.match(error.message, /update.*options/);
|
|
740
|
+
assert.match(error.message, /:1002/);
|
|
741
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('options.policy')));
|
|
742
|
+
return true;
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
test('access-rule update accepts canonical policy body with hidden field', async () => {
|
|
746
|
+
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":{}}}';
|
|
747
|
+
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 }));
|
|
748
|
+
assert.equal(out.kind, 'result');
|
|
749
|
+
assert.deepEqual(out.data.update, ['options']);
|
|
750
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }]);
|
|
751
|
+
});
|
|
752
|
+
test('access-rule update rejects partial options user scope overwrite', async () => {
|
|
753
|
+
await assert.rejects(async () => {
|
|
754
|
+
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 }));
|
|
755
|
+
}, (error) => {
|
|
756
|
+
assert.ok(error instanceof CLIError);
|
|
757
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
758
|
+
assert.match(error.message, /options.*policy/);
|
|
759
|
+
assert.match(error.message, /assign-users/);
|
|
760
|
+
return true;
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
test('access-rule update rejects FieldVO key names in policy fields', async () => {
|
|
764
|
+
await assert.rejects(async () => {
|
|
765
|
+
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 }));
|
|
766
|
+
}, (error) => {
|
|
767
|
+
assert.ok(error instanceof CLIError);
|
|
768
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
769
|
+
assert.match(error.message, /:1002/);
|
|
770
|
+
assert.match(error.message, /vote_name|FieldVO\.key/);
|
|
771
|
+
return true;
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
test('access-rule create rejects guessed permission aliases', async () => {
|
|
775
|
+
await assert.rejects(async () => {
|
|
776
|
+
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 }));
|
|
777
|
+
}, (error) => {
|
|
778
|
+
assert.ok(error instanceof CLIError);
|
|
779
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
780
|
+
assert.match(error.message, /options\.policy/);
|
|
781
|
+
assert.equal(error.details?.requestType, 'AppIngressCreateReqVO');
|
|
782
|
+
assert.ok((error.details?.commonMistakes ?? []).some((item) => item.includes('field_visibility')));
|
|
783
|
+
return true;
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
test('access-rule create rejects read write action aliases', async () => {
|
|
787
|
+
await assert.rejects(async () => {
|
|
788
|
+
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 }));
|
|
789
|
+
}, (error) => {
|
|
790
|
+
assert.ok(error instanceof CLIError);
|
|
791
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
792
|
+
assert.match(error.message, /view.*add.*edit/);
|
|
793
|
+
return true;
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
test('access-rule create rejects FieldVO member type in user scope', async () => {
|
|
797
|
+
await assert.rejects(async () => {
|
|
798
|
+
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 }));
|
|
799
|
+
}, (error) => {
|
|
800
|
+
assert.ok(error instanceof CLIError);
|
|
801
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
802
|
+
assert.match(error.message, /user.*department.*actor|FieldVO type "member"/);
|
|
803
|
+
return true;
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
test('access-rule create accepts custom policy with hidden field', async () => {
|
|
807
|
+
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":{}}}';
|
|
808
|
+
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 }));
|
|
809
|
+
assert.equal(out.kind, 'result');
|
|
810
|
+
assert.deepEqual(out.data.options.policy.actions, ['view', 'add', 'edit']);
|
|
811
|
+
assert.deepEqual(out.data.options.policy.fields, [{ key: ':1002', read: false, write: false, report: false }]);
|
|
812
|
+
});
|
|
414
813
|
test('access-rule assign-users rejects users shortcut', async () => {
|
|
415
814
|
await assert.rejects(async () => {
|
|
416
815
|
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 }));
|
|
@@ -428,22 +827,22 @@ test('access-rule assign-users rejects allowedUsers shortcut', async () => {
|
|
|
428
827
|
});
|
|
429
828
|
test('access-rule assign-users rejects top-level user_scope', async () => {
|
|
430
829
|
await assert.rejects(async () => {
|
|
431
|
-
await executeCLI('admin', ['access-rule', 'assign-users', '--app-id', 'app_1', '--table-id', 'table_1', '--rule-id', 'rule_1', '--body-json', '{"user_scope":[{"type":"
|
|
830
|
+
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 }));
|
|
432
831
|
}, (error) => assertAssignUsersGuidance(error, /options.user_scope/));
|
|
433
832
|
});
|
|
434
|
-
test('access-rule assign-users rejects non-member scope type', async () => {
|
|
833
|
+
test('access-rule assign-users rejects non-user member scope type', async () => {
|
|
435
834
|
await assert.rejects(async () => {
|
|
436
|
-
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":"
|
|
437
|
-
}, (error) => assertAssignUsersGuidance(error, /type.*
|
|
835
|
+
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 }));
|
|
836
|
+
}, (error) => assertAssignUsersGuidance(error, /type.*user/));
|
|
438
837
|
});
|
|
439
838
|
test('access-rule assign-users rejects hash tenant user id', async () => {
|
|
440
839
|
await assert.rejects(async () => {
|
|
441
|
-
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":"
|
|
840
|
+
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 }));
|
|
442
841
|
}, (error) => assertAssignUsersGuidance(error, /numeric TenantUser.ID/));
|
|
443
842
|
});
|
|
444
843
|
test('access-rule assign-users accepts canonical user_scope body', async () => {
|
|
445
|
-
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":"
|
|
844
|
+
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 }));
|
|
446
845
|
assert.equal(out.kind, 'result');
|
|
447
846
|
assert.deepEqual(out.data.update, ['user_scope']);
|
|
448
|
-
assert.deepEqual(out.data.options.user_scope, [{ type: '
|
|
847
|
+
assert.deepEqual(out.data.options.user_scope, [{ type: 'user', id: 2188889901 }]);
|
|
449
848
|
});
|