@carthooks/arcubase-cli 0.1.7 → 0.1.9
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 +1042 -5056
- package/bundle/arcubase.mjs +1042 -5056
- package/dist/generated/command_registry.generated.d.ts +693 -2812
- package/dist/generated/command_registry.generated.d.ts.map +1 -1
- package/dist/generated/command_registry.generated.js +812 -4100
- package/dist/generated/type_index.generated.d.ts +8 -397
- package/dist/generated/type_index.generated.d.ts.map +1 -1
- package/dist/generated/type_index.generated.js +8 -403
- package/dist/generated/zod_registry.generated.d.ts +33 -261
- package/dist/generated/zod_registry.generated.d.ts.map +1 -1
- package/dist/generated/zod_registry.generated.js +36 -365
- package/dist/runtime/body_loader.d.ts.map +1 -1
- package/dist/runtime/body_loader.js +12 -0
- package/dist/runtime/command_registry.d.ts +3 -21
- package/dist/runtime/command_registry.d.ts.map +1 -1
- package/dist/runtime/command_registry.js +17 -23
- package/dist/runtime/execute.d.ts +3 -3
- package/dist/runtime/execute.d.ts.map +1 -1
- package/dist/runtime/execute.js +53 -157
- package/dist/runtime/http.js +1 -1
- package/dist/runtime/upload.js +3 -3
- package/dist/runtime/zod_registry.d.ts.map +1 -1
- package/dist/runtime/zod_registry.js +7 -24
- package/dist/tests/command_registry.test.js +81 -28
- package/dist/tests/execute_validation.test.js +175 -510
- package/dist/tests/help.test.js +39 -62
- package/dist/tests/upload.test.js +4 -4
- package/dist/tests/zod_registry.test.js +0 -10
- package/package.json +1 -1
- package/sdk-dist/docs/runtime-reference/README.md +20 -30
- package/sdk-dist/docs/runtime-reference/entity-schema/README.md +4 -5
- package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
- 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/subform.md +2 -2
- package/sdk-dist/docs/runtime-reference/entity-schema.md +4 -4
- package/sdk-dist/docs/runtime-reference/examples/README.md +2 -2
- package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +12 -12
- package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
- package/sdk-dist/docs/runtime-reference/row-crud.md +11 -32
- package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +16 -47
- package/sdk-dist/docs/runtime-reference/table-lifecycle.md +11 -27
- package/sdk-dist/docs/runtime-reference/uploads.md +1 -1
- package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
- package/sdk-dist/generated/command_registry.generated.ts +814 -4102
- package/sdk-dist/generated/type_index.generated.ts +8 -403
- package/sdk-dist/generated/zod_registry.generated.ts +44 -500
- package/sdk-dist/types/app.ts +3 -10
- package/sdk-dist/types/common.ts +7 -0
- package/sdk-dist/types/global-action.ts +2 -1
- package/src/generated/command_registry.generated.ts +814 -4102
- package/src/generated/type_index.generated.ts +8 -403
- package/src/generated/zod_registry.generated.ts +44 -500
- package/src/runtime/body_loader.ts +11 -0
- package/src/runtime/command_registry.ts +22 -44
- package/src/runtime/execute.ts +157 -165
- package/src/runtime/http.ts +1 -1
- package/src/runtime/upload.ts +3 -3
- package/src/runtime/zod_registry.ts +7 -25
- package/src/tests/command_registry.test.ts +81 -31
- package/src/tests/execute_validation.test.ts +236 -603
- package/src/tests/help.test.ts +47 -66
- package/src/tests/upload.test.ts +4 -4
- package/src/tests/zod_registry.test.ts +0 -13
- package/sdk-dist/docs/runtime-reference/app-discovery.md +0 -68
|
@@ -19,104 +19,8 @@ function tempJSONFile(payload) {
|
|
|
19
19
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2));
|
|
20
20
|
return filePath;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
await assert.rejects(async () => {
|
|
25
|
-
await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', invalidFile], env, async () => new Response('{}'));
|
|
26
|
-
}, (error) => {
|
|
27
|
-
assert.ok(error instanceof CLIError);
|
|
28
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
29
|
-
assert.equal(error.details?.requestType, 'EntitySaveReqVO');
|
|
30
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')));
|
|
31
|
-
assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')));
|
|
32
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/entity-schema.md')));
|
|
33
|
-
return true;
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
test('admin create app by tenants rejects name longer than 20 chars', async () => {
|
|
37
|
-
const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' });
|
|
38
|
-
await assert.rejects(async () => {
|
|
39
|
-
await executeCLI('admin', ['app', 'create-app-by-tenants', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
40
|
-
}, (error) => {
|
|
41
|
-
assert.ok(error instanceof CLIError);
|
|
42
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
43
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'));
|
|
44
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/table-lifecycle.md')));
|
|
45
|
-
return true;
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
test('admin create entity executes with valid body file', async () => {
|
|
49
|
-
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
50
|
-
const out = await executeCLI('admin', ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method, headers: init?.headers }), { status: 200 }));
|
|
51
|
-
assert.equal(out.kind, 'result');
|
|
52
|
-
assert.equal(out.status, 200);
|
|
53
|
-
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entities');
|
|
54
|
-
assert.equal(out.data.headers['Authorization'], 'Bearer tok');
|
|
55
|
-
assert.equal(out.data.headers['X-Host'], undefined);
|
|
56
|
-
assert.equal(out.data.headers['X-Tenant-Id'], undefined);
|
|
57
|
-
});
|
|
58
|
-
test('admin api-prefixed command executes against external root path', async () => {
|
|
59
|
-
const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--app-id', 'app_1'], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 }));
|
|
60
|
-
assert.equal(out.kind, 'result');
|
|
61
|
-
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares');
|
|
62
|
-
});
|
|
63
|
-
test('app inventory returns apps with entity lists', async () => {
|
|
64
|
-
const calls = [];
|
|
65
|
-
const out = await executeCLI('admin', ['app', 'inventory'], env, async (url, init) => {
|
|
66
|
-
calls.push(`${init?.method ?? 'GET'} ${String(url)}`);
|
|
67
|
-
if (String(url) === 'https://arcubase.example.com/apps') {
|
|
68
|
-
return new Response(JSON.stringify({
|
|
69
|
-
data: [
|
|
70
|
-
{ id: 101, name: 'Workspace' },
|
|
71
|
-
{ id: 102, name: 'CRM' },
|
|
72
|
-
],
|
|
73
|
-
}), { status: 200 });
|
|
74
|
-
}
|
|
75
|
-
if (String(url) === 'https://arcubase.example.com/apps/101/entities') {
|
|
76
|
-
return new Response(JSON.stringify({
|
|
77
|
-
data: [
|
|
78
|
-
{ id: 201, name: 'Orders' },
|
|
79
|
-
{ id: 202, name: 'Customers' },
|
|
80
|
-
],
|
|
81
|
-
}), { status: 200 });
|
|
82
|
-
}
|
|
83
|
-
if (String(url) === 'https://arcubase.example.com/apps/102/entities') {
|
|
84
|
-
return new Response(JSON.stringify({
|
|
85
|
-
data: [
|
|
86
|
-
{ id: 203, name: 'Leads' },
|
|
87
|
-
],
|
|
88
|
-
}), { status: 200 });
|
|
89
|
-
}
|
|
90
|
-
return new Response('{}', { status: 404 });
|
|
91
|
-
});
|
|
92
|
-
assert.equal(out.kind, 'result');
|
|
93
|
-
assert.deepEqual(out.data, {
|
|
94
|
-
apps: [
|
|
95
|
-
{
|
|
96
|
-
id: '101',
|
|
97
|
-
name: 'Workspace',
|
|
98
|
-
entities: [
|
|
99
|
-
{ id: '201', name: 'Orders' },
|
|
100
|
-
{ id: '202', name: 'Customers' },
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: '102',
|
|
105
|
-
name: 'CRM',
|
|
106
|
-
entities: [
|
|
107
|
-
{ id: '203', name: 'Leads' },
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
assert.deepEqual(calls, [
|
|
113
|
-
'GET https://arcubase.example.com/apps',
|
|
114
|
-
'POST https://arcubase.example.com/apps/101/entities',
|
|
115
|
-
'POST https://arcubase.example.com/apps/102/entities',
|
|
116
|
-
]);
|
|
117
|
-
});
|
|
118
|
-
test('admin save entity treats top-level error body as failure even on http 200', async () => {
|
|
119
|
-
const bodyFile = tempJSONFile({
|
|
22
|
+
function validEntitySavePayload(overrides = {}) {
|
|
23
|
+
return {
|
|
120
24
|
id: 2188889845,
|
|
121
25
|
app_id: 2188889844,
|
|
122
26
|
name: '订单',
|
|
@@ -141,83 +45,141 @@ test('admin save entity treats top-level error body as failure even on http 200'
|
|
|
141
45
|
depends: [],
|
|
142
46
|
key: 'order_id',
|
|
143
47
|
code_index: false,
|
|
144
|
-
options: {
|
|
48
|
+
options: {
|
|
49
|
+
placeholder: '',
|
|
50
|
+
type: 'text',
|
|
51
|
+
lengthLimit: false,
|
|
52
|
+
lengthMin: 0,
|
|
53
|
+
lengthMax: 255,
|
|
54
|
+
},
|
|
145
55
|
transfers: null,
|
|
146
56
|
children: null,
|
|
147
57
|
},
|
|
148
58
|
],
|
|
59
|
+
options: {
|
|
60
|
+
Buttons: [],
|
|
61
|
+
CustomActions: [],
|
|
62
|
+
FrontendEvents: [],
|
|
63
|
+
TitleFormat: {
|
|
64
|
+
Fields: [],
|
|
65
|
+
Parts: [],
|
|
66
|
+
Body: '',
|
|
67
|
+
},
|
|
68
|
+
misc: {},
|
|
69
|
+
},
|
|
149
70
|
workflow_enabled: false,
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
test('table update-schema rejects invalid body file', async () => {
|
|
75
|
+
const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' });
|
|
76
|
+
await assert.rejects(async () => {
|
|
77
|
+
await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', invalidFile], env, async () => new Response('{}'));
|
|
78
|
+
}, (error) => {
|
|
79
|
+
assert.ok(error instanceof CLIError);
|
|
80
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
81
|
+
assert.equal(error.details?.requestType, 'EntitySaveReqVO');
|
|
82
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')));
|
|
83
|
+
return true;
|
|
150
84
|
});
|
|
85
|
+
});
|
|
86
|
+
test('app create rejects name longer than 20 chars', async () => {
|
|
87
|
+
const bodyFile = tempJSONFile({ name: 'CLI Verify Happy Path' });
|
|
151
88
|
await assert.rejects(async () => {
|
|
152
|
-
await executeCLI('admin', ['
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
89
|
+
await executeCLI('admin', ['app', 'create', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
90
|
+
}, (error) => {
|
|
91
|
+
assert.ok(error instanceof CLIError);
|
|
92
|
+
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
93
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.name'));
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
test('table create executes with valid body file', async () => {
|
|
98
|
+
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
99
|
+
const out = await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({
|
|
100
|
+
url,
|
|
101
|
+
method: init?.method,
|
|
102
|
+
hasTenantHeader: Object.prototype.hasOwnProperty.call(init?.headers ?? {}, 'X-Tenant-Id'),
|
|
103
|
+
hasHostHeader: Object.prototype.hasOwnProperty.call(init?.headers ?? {}, 'X-Host'),
|
|
104
|
+
}), { status: 200 }));
|
|
105
|
+
assert.equal(out.kind, 'result');
|
|
106
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities');
|
|
107
|
+
assert.equal(out.data.hasTenantHeader, false);
|
|
108
|
+
assert.equal(out.data.hasHostHeader, false);
|
|
109
|
+
});
|
|
110
|
+
test('table create executes with valid body json', async () => {
|
|
111
|
+
const out = await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":"订单","parent":0}'], env, async (url, init) => new Response(JSON.stringify({
|
|
112
|
+
url,
|
|
113
|
+
method: init?.method,
|
|
114
|
+
body: init?.body ? JSON.parse(String(init.body)) : null,
|
|
115
|
+
}), { status: 200 }));
|
|
116
|
+
assert.equal(out.kind, 'result');
|
|
117
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entities');
|
|
118
|
+
assert.deepEqual(out.data.body, { name: '订单', parent: 0 });
|
|
119
|
+
});
|
|
120
|
+
test('body json conflicts with body file', async () => {
|
|
121
|
+
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
122
|
+
await assert.rejects(async () => {
|
|
123
|
+
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-file', bodyFile, '--body-json', '{"name":"订单"}'], env, async () => new Response('{}', { status: 200 }));
|
|
124
|
+
}, (error) => {
|
|
125
|
+
assert.ok(error instanceof CLIError);
|
|
126
|
+
assert.equal(error.code, 'BODY_JSON_CONFLICT');
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
test('body json is rejected for commands without request type', async () => {
|
|
131
|
+
await assert.rejects(async () => {
|
|
132
|
+
await executeCLI('admin', ['app', 'list', '--body-json', '{"name":"ignored"}'], env, async () => new Response('{}', { status: 200 }));
|
|
133
|
+
}, (error) => {
|
|
134
|
+
assert.ok(error instanceof CLIError);
|
|
135
|
+
assert.equal(error.code, 'BODY_JSON_NOT_SUPPORTED');
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
test('body json parse errors fail fast', async () => {
|
|
140
|
+
await assert.rejects(async () => {
|
|
141
|
+
await executeCLI('admin', ['table', 'create', '--app-id', 'app_1', '--body-json', '{"name":'], env, async () => new Response('{}', { status: 200 }));
|
|
142
|
+
}, (error) => {
|
|
143
|
+
assert.ok(error instanceof CLIError);
|
|
144
|
+
assert.equal(error.code, 'INVALID_BODY_JSON');
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
test('access-rule list executes against external root path', async () => {
|
|
149
|
+
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
|
+
assert.equal(out.kind, 'result');
|
|
151
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/apps/app_1/entity/table_1/ingress');
|
|
152
|
+
});
|
|
153
|
+
test('table update-schema treats top-level error body as failure even on http 200', async () => {
|
|
154
|
+
const bodyFile = tempJSONFile(validEntitySavePayload());
|
|
155
|
+
await assert.rejects(async () => {
|
|
156
|
+
await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response(JSON.stringify({
|
|
157
|
+
error: { message: 'json: cannot unmarshal object into Go struct field .EntityVO.fields of type []models.Field', type: 'system', key: 'error.system' },
|
|
158
158
|
trace_id: 'req_xxx',
|
|
159
159
|
}), { status: 200 }));
|
|
160
160
|
}, (error) => {
|
|
161
161
|
assert.ok(error instanceof CLIError);
|
|
162
162
|
assert.equal(error.code, 'UPSTREAM_BODY_ERROR');
|
|
163
|
-
assert.equal(error.details?.endpoint, '/apps/app_1/entity/
|
|
163
|
+
assert.equal(error.details?.endpoint, '/api/apps/app_1/entity/table_1');
|
|
164
164
|
assert.equal(error.details?.method, 'PUT');
|
|
165
165
|
assert.equal(error.details?.traceId, 'req_xxx');
|
|
166
166
|
return true;
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
|
-
test('
|
|
170
|
-
const bodyFile = tempJSONFile({
|
|
171
|
-
id: 2188889845,
|
|
172
|
-
app_id: 2188889844,
|
|
173
|
-
name: '订单',
|
|
169
|
+
test('table update-schema normalizes shell null placeholders before validation', async () => {
|
|
170
|
+
const bodyFile = tempJSONFile(validEntitySavePayload({
|
|
174
171
|
schema_version: 0,
|
|
175
|
-
field_id_seq: 1002,
|
|
176
172
|
layout: null,
|
|
177
173
|
current_serial: null,
|
|
178
|
-
fields: [
|
|
179
|
-
{
|
|
180
|
-
id: 1001,
|
|
181
|
-
label: '订单号',
|
|
182
|
-
type: 'text',
|
|
183
|
-
required: true,
|
|
184
|
-
unique: false,
|
|
185
|
-
editable: true,
|
|
186
|
-
visible: true,
|
|
187
|
-
show_label: true,
|
|
188
|
-
scannable: false,
|
|
189
|
-
default_value_mode: 0,
|
|
190
|
-
description: '',
|
|
191
|
-
value: null,
|
|
192
|
-
number_decimal: 0,
|
|
193
|
-
depends: [],
|
|
194
|
-
key: 'order_id',
|
|
195
|
-
code_index: false,
|
|
196
|
-
options: {
|
|
197
|
-
placeholder: '',
|
|
198
|
-
type: 'text',
|
|
199
|
-
lengthLimit: false,
|
|
200
|
-
lengthMin: 0,
|
|
201
|
-
lengthMax: 255,
|
|
202
|
-
},
|
|
203
|
-
transfers: null,
|
|
204
|
-
children: null,
|
|
205
|
-
},
|
|
206
|
-
],
|
|
207
174
|
options: {
|
|
208
175
|
Buttons: null,
|
|
209
176
|
CustomActions: null,
|
|
210
177
|
FrontendEvents: null,
|
|
211
|
-
TitleFormat: {
|
|
212
|
-
Fields: null,
|
|
213
|
-
Parts: null,
|
|
214
|
-
Body: '',
|
|
215
|
-
},
|
|
178
|
+
TitleFormat: { Fields: null, Parts: null, Body: '' },
|
|
216
179
|
misc: null,
|
|
217
180
|
},
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
const out = await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
181
|
+
}));
|
|
182
|
+
const out = await executeCLI('admin', ['table', 'update-schema', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
221
183
|
assert.equal(out.kind, 'result');
|
|
222
184
|
const requestBody = out.data;
|
|
223
185
|
assert.deepEqual(requestBody.layout, []);
|
|
@@ -229,119 +191,45 @@ test('admin save entity normalizes shell null placeholders before validation', a
|
|
|
229
191
|
assert.deepEqual(requestBody.options.TitleFormat.Parts, []);
|
|
230
192
|
assert.deepEqual(requestBody.options.misc, {});
|
|
231
193
|
});
|
|
232
|
-
test('delete
|
|
233
|
-
const bodyFile = tempJSONFile({
|
|
234
|
-
selection: {
|
|
235
|
-
type: 'selection',
|
|
236
|
-
ids: [4000000003],
|
|
237
|
-
length: 1,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
194
|
+
test('row delete rejects selection type outside ids', async () => {
|
|
195
|
+
const bodyFile = tempJSONFile({ selection: { type: 'selection', ids: [4000000003], length: 1 } });
|
|
240
196
|
await assert.rejects(async () => {
|
|
241
|
-
await executeCLI('user', ['
|
|
197
|
+
await executeCLI('user', ['row', 'delete', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
242
198
|
}, (error) => {
|
|
243
199
|
assert.ok(error instanceof CLIError);
|
|
244
200
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
245
201
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection'));
|
|
246
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')));
|
|
247
202
|
return true;
|
|
248
203
|
});
|
|
249
204
|
});
|
|
250
|
-
test('bulk
|
|
205
|
+
test('row bulk-update accepts ids selection with number_add action', async () => {
|
|
251
206
|
const bodyFile = tempJSONFile({
|
|
252
|
-
selection: {
|
|
253
|
-
|
|
254
|
-
ids: [4000000003],
|
|
255
|
-
length: 1,
|
|
256
|
-
},
|
|
257
|
-
fields: [
|
|
258
|
-
{
|
|
259
|
-
id: 1002,
|
|
260
|
-
action: {
|
|
261
|
-
type: 'number_add',
|
|
262
|
-
number_add: 10,
|
|
263
|
-
},
|
|
264
|
-
},
|
|
265
|
-
],
|
|
207
|
+
selection: { type: 'ids', ids: [4000000003], length: 1 },
|
|
208
|
+
fields: [{ id: 1002, action: { type: 'number_add', number_add: 10 } }],
|
|
266
209
|
});
|
|
267
|
-
const out = await executeCLI('user', ['
|
|
210
|
+
const out = await executeCLI('user', ['row', 'bulk-update', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
268
211
|
assert.equal(out.kind, 'result');
|
|
269
212
|
assert.deepEqual(out.data.selection, { type: 'ids', ids: [4000000003], length: 1 });
|
|
270
213
|
assert.equal(out.data.fields[0].action.type, 'number_add');
|
|
271
214
|
});
|
|
272
|
-
test('selection
|
|
215
|
+
test('row selection-action accepts condition selection with selectAll', async () => {
|
|
273
216
|
const bodyFile = tempJSONFile({
|
|
274
217
|
selection: {
|
|
275
218
|
type: 'condition',
|
|
276
219
|
length: 1,
|
|
277
|
-
condition: {
|
|
278
|
-
|
|
279
|
-
selectAll: true,
|
|
280
|
-
},
|
|
281
|
-
sorts: [
|
|
282
|
-
{ column: ':1002', order: 'desc' },
|
|
283
|
-
],
|
|
220
|
+
condition: { q: 'A-100', selectAll: true },
|
|
221
|
+
sorts: [{ column: ':1002', order: 'desc' }],
|
|
284
222
|
},
|
|
285
223
|
});
|
|
286
|
-
const out = await executeCLI('user', ['
|
|
224
|
+
const out = await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
287
225
|
assert.equal(out.kind, 'result');
|
|
288
226
|
assert.equal(out.data.selection.type, 'condition');
|
|
289
227
|
assert.equal(out.data.selection.condition.selectAll, true);
|
|
290
228
|
});
|
|
291
|
-
test('selection
|
|
292
|
-
const bodyFile = tempJSONFile({
|
|
293
|
-
selection: {
|
|
294
|
-
type: 'condition',
|
|
295
|
-
length: 1,
|
|
296
|
-
condition: {
|
|
297
|
-
'filter_:1001': '%7B%22is%22%3A%22A-100%22%7D',
|
|
298
|
-
selectAll: true,
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
303
|
-
assert.equal(out.kind, 'result');
|
|
304
|
-
assert.equal(out.data.selection.type, 'condition');
|
|
305
|
-
assert.equal(out.data.selection.condition.selectAll, true);
|
|
306
|
-
});
|
|
307
|
-
test('selection action accepts condition selection with selectAll only for batch_print', async () => {
|
|
308
|
-
const bodyFile = tempJSONFile({
|
|
309
|
-
selection: {
|
|
310
|
-
type: 'condition',
|
|
311
|
-
length: 1,
|
|
312
|
-
condition: {
|
|
313
|
-
selectAll: true,
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'batch_print', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
318
|
-
assert.equal(out.kind, 'result');
|
|
319
|
-
assert.equal(out.data.selection.type, 'condition');
|
|
320
|
-
assert.equal(out.data.selection.condition.selectAll, true);
|
|
321
|
-
});
|
|
322
|
-
test('selection action accepts all selection for query-style actions', async () => {
|
|
323
|
-
const bodyFile = tempJSONFile({
|
|
324
|
-
selection: {
|
|
325
|
-
type: 'all',
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
const out = await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'query', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
329
|
-
assert.equal(out.kind, 'result');
|
|
330
|
-
assert.equal(out.data.selection.type, 'all');
|
|
331
|
-
});
|
|
332
|
-
test('selection action rejects condition selection for query action', async () => {
|
|
333
|
-
const bodyFile = tempJSONFile({
|
|
334
|
-
selection: {
|
|
335
|
-
type: 'condition',
|
|
336
|
-
length: 1,
|
|
337
|
-
condition: {
|
|
338
|
-
q: 'A-100',
|
|
339
|
-
selectAll: true,
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
});
|
|
229
|
+
test('row selection-action rejects condition selection for query action', async () => {
|
|
230
|
+
const bodyFile = tempJSONFile({ selection: { type: 'condition', length: 1, condition: { q: 'A-100', selectAll: true } } });
|
|
343
231
|
await assert.rejects(async () => {
|
|
344
|
-
await executeCLI('user', ['
|
|
232
|
+
await executeCLI('user', ['row', 'selection-action', '--app-id', 'app_1', '--table-id', 'table_1', '--action', 'query', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
345
233
|
}, (error) => {
|
|
346
234
|
assert.ok(error instanceof CLIError);
|
|
347
235
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
@@ -349,62 +237,14 @@ test('selection action rejects condition selection for query action', async () =
|
|
|
349
237
|
return true;
|
|
350
238
|
});
|
|
351
239
|
});
|
|
352
|
-
test('
|
|
353
|
-
const bodyFile = tempJSONFile({
|
|
354
|
-
selection: {
|
|
355
|
-
type: 'all',
|
|
356
|
-
},
|
|
357
|
-
});
|
|
358
|
-
await assert.rejects(async () => {
|
|
359
|
-
await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
360
|
-
}, (error) => {
|
|
361
|
-
assert.ok(error instanceof CLIError);
|
|
362
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
363
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.type'));
|
|
364
|
-
return true;
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
test('selection action rejects selectAll-only condition for archive action', async () => {
|
|
368
|
-
const bodyFile = tempJSONFile({
|
|
369
|
-
selection: {
|
|
370
|
-
type: 'condition',
|
|
371
|
-
length: 1,
|
|
372
|
-
condition: {
|
|
373
|
-
selectAll: true,
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
await assert.rejects(async () => {
|
|
378
|
-
await executeCLI('user', ['rows', 'query-entity-selection', '--app-id', 'app_1', '--entity-id', 'entity_1', '--action', 'archive', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
379
|
-
}, (error) => {
|
|
380
|
-
assert.ok(error instanceof CLIError);
|
|
381
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
382
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.selection.condition'));
|
|
383
|
-
return true;
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
test('insert entity rejects local file path for file field and points to arcubase upload', async () => {
|
|
387
|
-
const bodyFile = tempJSONFile({
|
|
388
|
-
fields: {
|
|
389
|
-
1001: 'Lead A',
|
|
390
|
-
1007: './contract.pdf',
|
|
391
|
-
},
|
|
392
|
-
});
|
|
240
|
+
test('row create rejects local file path for file field and points to arcubase upload', async () => {
|
|
241
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: './contract.pdf' } });
|
|
393
242
|
const calls = [];
|
|
394
243
|
await assert.rejects(async () => {
|
|
395
|
-
await executeCLI('user', ['
|
|
244
|
+
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
396
245
|
calls.push({ url: String(url), method: init?.method });
|
|
397
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
398
|
-
return new Response(JSON.stringify({
|
|
399
|
-
data: {
|
|
400
|
-
id: 1,
|
|
401
|
-
app_id: 1,
|
|
402
|
-
fields: [
|
|
403
|
-
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
404
|
-
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
405
|
-
],
|
|
406
|
-
},
|
|
407
|
-
}), { status: 200 });
|
|
246
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
247
|
+
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 });
|
|
408
248
|
}
|
|
409
249
|
return new Response('{}', { status: 200 });
|
|
410
250
|
});
|
|
@@ -413,37 +253,16 @@ test('insert entity rejects local file path for file field and points to arcubas
|
|
|
413
253
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
414
254
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields.1007'));
|
|
415
255
|
assert.match(error.message, /arcubase upload \.\/contract\.pdf/);
|
|
416
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
|
|
417
256
|
return true;
|
|
418
257
|
});
|
|
419
|
-
assert.deepEqual(calls, [
|
|
420
|
-
{ url: 'https://arcubase.example.com/entity/app_1/entity_1', method: 'GET' },
|
|
421
|
-
]);
|
|
258
|
+
assert.deepEqual(calls, [{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' }]);
|
|
422
259
|
});
|
|
423
|
-
test('update
|
|
424
|
-
const bodyFile = tempJSONFile({
|
|
425
|
-
changed_fields: [1008],
|
|
426
|
-
data: {
|
|
427
|
-
1008: {
|
|
428
|
-
upload_id: 123456,
|
|
429
|
-
file: 'lead-photo.jpg',
|
|
430
|
-
file_name: 'lead-photo.jpg',
|
|
431
|
-
meta: {},
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
});
|
|
260
|
+
test('row update rejects object value for image field and requires upload array', async () => {
|
|
261
|
+
const bodyFile = tempJSONFile({ changed_fields: [1008], data: { 1008: { upload_id: 123456, file: 'lead-photo.jpg', file_name: 'lead-photo.jpg', meta: {} } } });
|
|
435
262
|
await assert.rejects(async () => {
|
|
436
|
-
await executeCLI('user', ['
|
|
437
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
438
|
-
return new Response(JSON.stringify({
|
|
439
|
-
data: {
|
|
440
|
-
id: 1,
|
|
441
|
-
app_id: 1,
|
|
442
|
-
fields: [
|
|
443
|
-
{ id: 1008, type: 'image', label: 'Lead Photo' },
|
|
444
|
-
],
|
|
445
|
-
},
|
|
446
|
-
}), { status: 200 });
|
|
263
|
+
await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
264
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
265
|
+
return new Response(JSON.stringify({ data: { id: 1, app_id: 1, fields: [{ id: 1008, type: 'image', label: 'Lead Photo' }] } }), { status: 200 });
|
|
447
266
|
}
|
|
448
267
|
return new Response('{}', { status: 200 });
|
|
449
268
|
});
|
|
@@ -452,74 +271,36 @@ test('update row rejects object value for image field and requires upload array'
|
|
|
452
271
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
453
272
|
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.data.1008'));
|
|
454
273
|
assert.match(error.message, /must be an array/);
|
|
455
|
-
assert.match(error.message, /arcubase upload <local-file>/);
|
|
456
274
|
return true;
|
|
457
275
|
});
|
|
458
276
|
});
|
|
459
|
-
test('
|
|
460
|
-
const bodyFile = tempJSONFile({
|
|
461
|
-
fields: {
|
|
462
|
-
1001: 'Lead A',
|
|
463
|
-
1007: [
|
|
464
|
-
{
|
|
465
|
-
upload_id: 123456,
|
|
466
|
-
file: 'contract.pdf',
|
|
467
|
-
file_name: 'contract.pdf',
|
|
468
|
-
meta: {},
|
|
469
|
-
},
|
|
470
|
-
],
|
|
471
|
-
},
|
|
472
|
-
});
|
|
277
|
+
test('row create accepts upload array for file field after schema preflight', async () => {
|
|
278
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
473
279
|
const calls = [];
|
|
474
|
-
const out = await executeCLI('user', ['
|
|
280
|
+
const out = await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
475
281
|
calls.push({ url: String(url), method: init?.method });
|
|
476
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
477
|
-
return new Response(JSON.stringify({
|
|
478
|
-
data: {
|
|
479
|
-
id: 1,
|
|
480
|
-
app_id: 1,
|
|
481
|
-
fields: [
|
|
482
|
-
{ id: 1001, type: 'text', label: 'Lead Name' },
|
|
483
|
-
{ id: 1007, type: 'file', label: 'Contract File' },
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
}), { status: 200 });
|
|
282
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
283
|
+
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 });
|
|
487
284
|
}
|
|
488
285
|
return new Response(String(init?.body ?? '{}'), { status: 200 });
|
|
489
286
|
});
|
|
490
287
|
assert.equal(out.kind, 'result');
|
|
491
288
|
assert.deepEqual(calls, [
|
|
492
|
-
{ url: 'https://arcubase.example.com/entity/app_1/
|
|
493
|
-
{ url: 'https://arcubase.example.com/entity/app_1/
|
|
289
|
+
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1', method: 'GET' },
|
|
290
|
+
{ url: 'https://arcubase.example.com/api/entity/app_1/table_1/insert', method: 'POST' },
|
|
494
291
|
]);
|
|
495
292
|
assert.equal(out.data.fields['1007'][0].upload_id, 123456);
|
|
496
293
|
});
|
|
497
|
-
test('
|
|
498
|
-
const bodyFile = tempJSONFile({
|
|
499
|
-
fields: {
|
|
500
|
-
1001: 'Lead A',
|
|
501
|
-
1007: [
|
|
502
|
-
{
|
|
503
|
-
upload_id: 123456,
|
|
504
|
-
file: 'contract.pdf',
|
|
505
|
-
file_name: 'contract.pdf',
|
|
506
|
-
meta: {},
|
|
507
|
-
},
|
|
508
|
-
],
|
|
509
|
-
},
|
|
510
|
-
});
|
|
294
|
+
test('row create decorates upload bind failures with actionable guidance', async () => {
|
|
295
|
+
const bodyFile = tempJSONFile({ fields: { 1001: 'Lead A', 1007: [{ upload_id: 123456, file: 'contract.pdf', file_name: 'contract.pdf', meta: {} }] } });
|
|
511
296
|
await assert.rejects(async () => {
|
|
512
|
-
await executeCLI('user', ['
|
|
513
|
-
if (String(url) === 'https://arcubase.example.com/entity/app_1/
|
|
514
|
-
return new Response(JSON.stringify({
|
|
515
|
-
data: {
|
|
516
|
-
fields: [{ id: 1007, type: 'file', label: 'Contract File' }],
|
|
517
|
-
},
|
|
518
|
-
}), { status: 200 });
|
|
297
|
+
await executeCLI('user', ['row', 'create', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async (url, init) => {
|
|
298
|
+
if (String(url) === 'https://arcubase.example.com/api/entity/app_1/table_1' && init?.method === 'GET') {
|
|
299
|
+
return new Response(JSON.stringify({ data: { fields: [{ id: 1007, type: 'file', label: 'Contract File' }] } }), { status: 200 });
|
|
519
300
|
}
|
|
520
301
|
return new Response(JSON.stringify({
|
|
521
302
|
error: {
|
|
522
|
-
message: 'failed to bind upload to assets: Head
|
|
303
|
+
message: 'failed to bind upload to assets: Head "http://test-bucket.test.aliyuncs.com/upload/123456/contract.pdf": dial tcp: lookup test-bucket.test.aliyuncs.com: no such host',
|
|
523
304
|
type: 'system',
|
|
524
305
|
key: 'error.system',
|
|
525
306
|
},
|
|
@@ -532,176 +313,60 @@ test('insert entity decorates upload bind failures with actionable guidance', as
|
|
|
532
313
|
assert.equal(error.details?.traceId, 'req_upload_bind');
|
|
533
314
|
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');
|
|
534
315
|
assert.match(error.details?.hint ?? '', /rerun arcubase upload/);
|
|
535
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/uploads.md')));
|
|
536
316
|
return true;
|
|
537
317
|
});
|
|
538
318
|
});
|
|
539
|
-
test('
|
|
540
|
-
const bodyFile = tempJSONFile({
|
|
541
|
-
selection: {
|
|
542
|
-
type: 'condition',
|
|
543
|
-
length: 1,
|
|
544
|
-
condition: {
|
|
545
|
-
q: 'A-100',
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
});
|
|
319
|
+
test('row query rejects camelCase viewMode and points to row crud doc', async () => {
|
|
320
|
+
const bodyFile = tempJSONFile({ limit: 20, offset: 0, viewMode: 'grid' });
|
|
549
321
|
await assert.rejects(async () => {
|
|
550
|
-
await executeCLI('user', ['
|
|
322
|
+
await executeCLI('user', ['row', 'query', '--app-id', 'app_1', '--table-id', 'table_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
551
323
|
}, (error) => {
|
|
552
324
|
assert.ok(error instanceof CLIError);
|
|
553
325
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
554
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.
|
|
326
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.viewMode'));
|
|
555
327
|
return true;
|
|
556
328
|
});
|
|
557
329
|
});
|
|
558
|
-
test('
|
|
559
|
-
const bodyFile = tempJSONFile({
|
|
560
|
-
selection: {
|
|
561
|
-
type: 'condition',
|
|
562
|
-
length: 1,
|
|
563
|
-
condition: {
|
|
564
|
-
text_search: 'A-100',
|
|
565
|
-
selectAll: true,
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
});
|
|
330
|
+
test('row update rejects fields key', async () => {
|
|
331
|
+
const bodyFile = tempJSONFile({ fields: { 1002: 99 }, data: { '1002': 99 }, changed_fields: [1002] });
|
|
569
332
|
await assert.rejects(async () => {
|
|
570
|
-
await executeCLI('user', ['
|
|
333
|
+
await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
571
334
|
}, (error) => {
|
|
572
335
|
assert.ok(error instanceof CLIError);
|
|
573
336
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
574
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.
|
|
575
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/search-and-bulk-actions.md')));
|
|
337
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'));
|
|
576
338
|
return true;
|
|
577
339
|
});
|
|
578
340
|
});
|
|
579
|
-
test('
|
|
580
|
-
const bodyFile = tempJSONFile({
|
|
581
|
-
selection: {
|
|
582
|
-
type: 'condition',
|
|
583
|
-
length: 1,
|
|
584
|
-
condition: {
|
|
585
|
-
q: 'A-100',
|
|
586
|
-
selectAll: true,
|
|
587
|
-
},
|
|
588
|
-
sorts: [
|
|
589
|
-
{ column: ':1002', order: 'desc' },
|
|
590
|
-
],
|
|
591
|
-
},
|
|
592
|
-
fields: [
|
|
593
|
-
{
|
|
594
|
-
id: 1002,
|
|
595
|
-
action: {
|
|
596
|
-
type: 'number_add',
|
|
597
|
-
number_add: 2,
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
],
|
|
601
|
-
});
|
|
602
|
-
const out = await executeCLI('user', ['entity', 'update-entity-bulk', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
603
|
-
assert.equal(out.kind, 'result');
|
|
604
|
-
assert.equal(out.data.selection.type, 'condition');
|
|
605
|
-
assert.equal(out.data.fields[0].action.type, 'number_add');
|
|
606
|
-
});
|
|
607
|
-
test('entity tags get-selection-tags accepts ids selection', async () => {
|
|
608
|
-
const bodyFile = tempJSONFile({
|
|
609
|
-
policy_id: '',
|
|
610
|
-
selection: {
|
|
611
|
-
type: 'ids',
|
|
612
|
-
ids: [4000000003],
|
|
613
|
-
length: 1,
|
|
614
|
-
},
|
|
615
|
-
});
|
|
616
|
-
const out = await executeCLI('admin', ['entity-tags', 'get-selection-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
617
|
-
assert.equal(out.kind, 'result');
|
|
618
|
-
assert.equal(out.data.selection.type, 'ids');
|
|
619
|
-
});
|
|
620
|
-
test('entity tags batch-add accepts tag_names and condition selection', async () => {
|
|
621
|
-
const bodyFile = tempJSONFile({
|
|
622
|
-
policy_id: '',
|
|
623
|
-
selection: {
|
|
624
|
-
type: 'condition',
|
|
625
|
-
length: 1,
|
|
626
|
-
condition: {
|
|
627
|
-
q: 'A-100',
|
|
628
|
-
selectAll: true,
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
tag_names: ['tag-a'],
|
|
632
|
-
});
|
|
633
|
-
const out = await executeCLI('admin', ['entity-tags', 'batch-add-entity-tags', '--appId', 'app_1', '--entityId', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
634
|
-
assert.equal(out.kind, 'result');
|
|
635
|
-
assert.equal(out.data.tag_names[0], 'tag-a');
|
|
636
|
-
});
|
|
637
|
-
test('invoke batch operator accepts backend request shape', async () => {
|
|
638
|
-
const bodyFile = tempJSONFile({
|
|
639
|
-
id: 'event_1',
|
|
640
|
-
data: {
|
|
641
|
-
action_id: 'action_1',
|
|
642
|
-
policy_id: '',
|
|
643
|
-
selection: {
|
|
644
|
-
type: 'all',
|
|
645
|
-
},
|
|
646
|
-
},
|
|
647
|
-
params: {},
|
|
648
|
-
});
|
|
649
|
-
const out = await executeCLI('user', ['entity', 'invoke-entity-batch-operator', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', bodyFile], env, async (_url, init) => new Response(String(init?.body ?? '{}'), { status: 200 }));
|
|
650
|
-
assert.equal(out.kind, 'result');
|
|
651
|
-
assert.equal(out.data.id, 'event_1');
|
|
652
|
-
assert.equal(out.data.data.action_id, 'action_1');
|
|
653
|
-
assert.equal(out.data.data.selection.type, 'all');
|
|
654
|
-
});
|
|
655
|
-
test('query entity rejects camelCase viewMode and points to row crud doc', async () => {
|
|
656
|
-
const bodyFile = tempJSONFile({
|
|
657
|
-
limit: 20,
|
|
658
|
-
offset: 0,
|
|
659
|
-
viewMode: 'grid',
|
|
660
|
-
});
|
|
341
|
+
test('row update rejects changed_fields that are missing from data', async () => {
|
|
342
|
+
const bodyFile = tempJSONFile({ data: { '1002': 99 }, changed_fields: [1003] });
|
|
661
343
|
await assert.rejects(async () => {
|
|
662
|
-
await executeCLI('user', ['
|
|
344
|
+
await executeCLI('user', ['row', 'update', '--app-id', 'app_1', '--table-id', 'table_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
663
345
|
}, (error) => {
|
|
664
346
|
assert.ok(error instanceof CLIError);
|
|
665
347
|
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
666
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.
|
|
667
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
|
|
348
|
+
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'));
|
|
668
349
|
return true;
|
|
669
350
|
});
|
|
670
351
|
});
|
|
671
|
-
test('
|
|
672
|
-
const bodyFile = tempJSONFile({
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
'1002': 99,
|
|
678
|
-
},
|
|
679
|
-
changed_fields: [1002],
|
|
680
|
-
});
|
|
681
|
-
await assert.rejects(async () => {
|
|
682
|
-
await executeCLI('user', ['rows', 'update-entity-row', '--app-id', 'app_1', '--entity-id', 'entity_1', '--row-id', 'row_1', '--body-file', bodyFile], env, async () => new Response('{}', { status: 200 }));
|
|
683
|
-
}, (error) => {
|
|
684
|
-
assert.ok(error instanceof CLIError);
|
|
685
|
-
assert.equal(error.code, 'BODY_VALIDATION_FAILED');
|
|
686
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.fields'));
|
|
687
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
|
|
688
|
-
return true;
|
|
689
|
-
});
|
|
352
|
+
test('workflow approve maps to fixed action path', async () => {
|
|
353
|
+
const bodyFile = tempJSONFile({ task_id: 1001, fields: {}, changed: [] });
|
|
354
|
+
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
|
+
assert.equal(out.kind, 'result');
|
|
356
|
+
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, 1001);
|
|
690
358
|
});
|
|
691
|
-
test('
|
|
692
|
-
const bodyFile = tempJSONFile({
|
|
693
|
-
data: {
|
|
694
|
-
'1002': 99,
|
|
695
|
-
},
|
|
696
|
-
changed_fields: [1003],
|
|
697
|
-
});
|
|
359
|
+
test('workflow query-approve-users requires request body', async () => {
|
|
698
360
|
await assert.rejects(async () => {
|
|
699
|
-
await executeCLI('user', ['
|
|
361
|
+
await executeCLI('user', ['workflow', 'query-approve-users', '--app-id', 'app_1', '--table-id', 'table_1'], env, async () => new Response('{}', { status: 200 }));
|
|
700
362
|
}, (error) => {
|
|
701
363
|
assert.ok(error instanceof CLIError);
|
|
702
|
-
assert.equal(error.code, '
|
|
703
|
-
assert.ok((error.details?.issues ?? []).some((item) => item.path === 'body.changed_fields.0'));
|
|
704
|
-
assert.ok((error.details?.docHints ?? []).some((item) => item.file.endsWith('/docs/runtime-reference/row-crud.md')));
|
|
364
|
+
assert.equal(error.code, 'MISSING_BODY_FILE');
|
|
705
365
|
return true;
|
|
706
366
|
});
|
|
707
367
|
});
|
|
368
|
+
test('entry passes include_app_tables query flag', async () => {
|
|
369
|
+
const out = await executeCLI('user', ['entry', '--include-app-tables', 'true'], env, async (url, init) => new Response(JSON.stringify({ url, method: init?.method }), { status: 200 }));
|
|
370
|
+
assert.equal(out.kind, 'result');
|
|
371
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/api/entry?include_app_tables=true');
|
|
372
|
+
});
|