@jskit-ai/crud 0.1.4
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/package.descriptor.mjs +322 -0
- package/package.json +22 -0
- package/src/client/index.js +3 -0
- package/src/server/CrudServiceProvider.js +11 -0
- package/src/server/actionIds.js +22 -0
- package/src/server/actions.js +152 -0
- package/src/server/registerRoutes.js +235 -0
- package/src/server/repository.js +162 -0
- package/src/server/service.js +96 -0
- package/src/shared/crud/crudModuleConfig.js +310 -0
- package/src/shared/crud/crudResource.js +191 -0
- package/src/shared/index.js +12 -0
- package/templates/migrations/crud_initial.cjs +42 -0
- package/templates/src/elements/CreateElement.vue +115 -0
- package/templates/src/elements/EditElement.vue +140 -0
- package/templates/src/elements/ListElement.vue +88 -0
- package/templates/src/elements/ViewElement.vue +126 -0
- package/templates/src/elements/clientSupport.js +41 -0
- package/templates/src/local-package/client/index.js +4 -0
- package/templates/src/local-package/package.descriptor.mjs +83 -0
- package/templates/src/local-package/package.json +14 -0
- package/templates/src/local-package/server/CrudServiceProvider.js +87 -0
- package/templates/src/local-package/server/actionIds.js +9 -0
- package/templates/src/local-package/server/actions.js +151 -0
- package/templates/src/local-package/server/diTokens.js +4 -0
- package/templates/src/local-package/server/registerRoutes.js +196 -0
- package/templates/src/local-package/server/repository.js +1 -0
- package/templates/src/local-package/server/service.js +96 -0
- package/templates/src/local-package/shared/crudResource.js +1 -0
- package/templates/src/local-package/shared/index.js +8 -0
- package/templates/src/local-package/shared/moduleConfig.js +169 -0
- package/templates/src/pages/admin/crud/[recordId]/edit.vue +7 -0
- package/templates/src/pages/admin/crud/[recordId]/index.vue +7 -0
- package/templates/src/pages/admin/crud/index.vue +7 -0
- package/templates/src/pages/admin/crud/new.vue +7 -0
- package/test/crudModuleConfig.test.js +225 -0
- package/test/crudResource.test.js +41 -0
- package/test/crudServerGuards.test.js +61 -0
- package/test/crudService.test.js +83 -0
- package/test/routeInputContracts.test.js +211 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
resolveCrudConfig,
|
|
5
|
+
resolveCrudSurfacePolicy,
|
|
6
|
+
resolveCrudConfigFromModules,
|
|
7
|
+
resolveCrudConfigsFromModules
|
|
8
|
+
} from "../src/shared/crud/crudModuleConfig.js";
|
|
9
|
+
|
|
10
|
+
test("resolveCrudConfig throws when namespace is missing", () => {
|
|
11
|
+
assert.throws(
|
|
12
|
+
() => resolveCrudConfig({}),
|
|
13
|
+
/requires a non-empty namespace/
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("resolveCrudConfig normalizes namespaced public settings", () => {
|
|
18
|
+
const config = resolveCrudConfig({
|
|
19
|
+
namespace: "CRM Team",
|
|
20
|
+
ownershipFilter: "public"
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.equal(config.namespace, "crm-team");
|
|
24
|
+
assert.equal(config.ownershipFilter, "public");
|
|
25
|
+
assert.equal(config.workspaceScoped, false);
|
|
26
|
+
assert.equal(config.namespacePath, "/crm-team");
|
|
27
|
+
assert.equal(config.relativePath, "/crm-team");
|
|
28
|
+
assert.equal(config.apiBasePath, "/api/crm-team");
|
|
29
|
+
assert.equal(config.tableName, "crud_crm_team");
|
|
30
|
+
assert.equal(config.actionIdPrefix, "crud.crm_team");
|
|
31
|
+
assert.equal(config.contributorId, "crud.crm_team");
|
|
32
|
+
assert.equal(config.domain, "crud");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("resolveCrudConfigsFromModules returns only crud module entries", () => {
|
|
36
|
+
const configs = resolveCrudConfigsFromModules({
|
|
37
|
+
"crud.customers": {
|
|
38
|
+
module: "crud",
|
|
39
|
+
namespace: "customers",
|
|
40
|
+
ownershipFilter: "workspace"
|
|
41
|
+
},
|
|
42
|
+
"crud.dragons": {
|
|
43
|
+
module: "crud",
|
|
44
|
+
namespace: "dragons",
|
|
45
|
+
ownershipFilter: "public"
|
|
46
|
+
},
|
|
47
|
+
"users.default": {
|
|
48
|
+
module: "users",
|
|
49
|
+
namespace: "ignored"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(configs.map((entry) => entry.namespace), ["customers", "dragons"]);
|
|
54
|
+
assert.deepEqual(configs.map((entry) => entry.ownershipFilter), ["workspace", "public"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("resolveCrudConfigFromModules resolves explicit namespace", () => {
|
|
58
|
+
const config = resolveCrudConfigFromModules(
|
|
59
|
+
{
|
|
60
|
+
"crud.customers": {
|
|
61
|
+
module: "crud",
|
|
62
|
+
namespace: "customers",
|
|
63
|
+
ownershipFilter: "workspace"
|
|
64
|
+
},
|
|
65
|
+
"crud.dragons": {
|
|
66
|
+
module: "crud",
|
|
67
|
+
namespace: "dragons",
|
|
68
|
+
ownershipFilter: "workspace_user"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
namespace: "dragons"
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
assert.ok(config);
|
|
77
|
+
assert.equal(config.namespace, "dragons");
|
|
78
|
+
assert.equal(config.ownershipFilter, "workspace_user");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("resolveCrudConfigFromModules returns null without namespace when multiple crud entries exist", () => {
|
|
82
|
+
const config = resolveCrudConfigFromModules({
|
|
83
|
+
"crud.customers": {
|
|
84
|
+
module: "crud",
|
|
85
|
+
namespace: "customers",
|
|
86
|
+
ownershipFilter: "workspace"
|
|
87
|
+
},
|
|
88
|
+
"crud.dragons": {
|
|
89
|
+
module: "crud",
|
|
90
|
+
namespace: "dragons",
|
|
91
|
+
ownershipFilter: "workspace"
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
assert.equal(config, null);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("resolveCrudConfigsFromModules rejects duplicate normalized namespaces", () => {
|
|
99
|
+
assert.throws(
|
|
100
|
+
() =>
|
|
101
|
+
resolveCrudConfigsFromModules({
|
|
102
|
+
"crud.customers": {
|
|
103
|
+
module: "crud",
|
|
104
|
+
namespace: "customers",
|
|
105
|
+
ownershipFilter: "workspace"
|
|
106
|
+
},
|
|
107
|
+
"crud.customers-copy": {
|
|
108
|
+
module: "crud",
|
|
109
|
+
namespace: "Customers",
|
|
110
|
+
ownershipFilter: "public"
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
/Duplicate CRUD namespace/
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("resolveCrudConfigsFromModules rejects module entries without namespace", () => {
|
|
118
|
+
assert.throws(
|
|
119
|
+
() =>
|
|
120
|
+
resolveCrudConfigsFromModules({
|
|
121
|
+
"crud.invalid": {
|
|
122
|
+
module: "crud",
|
|
123
|
+
namespace: "",
|
|
124
|
+
ownershipFilter: "workspace"
|
|
125
|
+
}
|
|
126
|
+
}),
|
|
127
|
+
/requires a non-empty namespace/
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("resolveCrudSurfacePolicy resolves auto ownership filter from workspace surface metadata", () => {
|
|
132
|
+
const policy = resolveCrudSurfacePolicy(
|
|
133
|
+
{
|
|
134
|
+
surface: "admin",
|
|
135
|
+
ownershipFilter: "auto",
|
|
136
|
+
relativePath: "/crm/customers"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
surfaceDefinitions: {
|
|
140
|
+
admin: { requiresWorkspace: true, requiresAuth: true, enabled: true }
|
|
141
|
+
},
|
|
142
|
+
defaultSurfaceId: "admin"
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
assert.equal(policy.surfaceId, "admin");
|
|
147
|
+
assert.equal(policy.ownershipFilter, "workspace");
|
|
148
|
+
assert.equal(policy.workspaceScoped, true);
|
|
149
|
+
assert.equal(policy.relativePath, "/crm/customers");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("resolveCrudSurfacePolicy resolves auto ownership filter from auth-only surface metadata", () => {
|
|
153
|
+
const policy = resolveCrudSurfacePolicy(
|
|
154
|
+
{
|
|
155
|
+
surface: "console",
|
|
156
|
+
ownershipFilter: "auto",
|
|
157
|
+
relativePath: "/crm/customers"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
surfaceDefinitions: {
|
|
161
|
+
console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
|
|
162
|
+
},
|
|
163
|
+
defaultSurfaceId: "console"
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
assert.equal(policy.surfaceId, "console");
|
|
168
|
+
assert.equal(policy.ownershipFilter, "user");
|
|
169
|
+
assert.equal(policy.workspaceScoped, false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("resolveCrudSurfacePolicy rejects explicit workspace ownership filter on non-workspace surfaces", () => {
|
|
173
|
+
assert.throws(
|
|
174
|
+
() =>
|
|
175
|
+
resolveCrudSurfacePolicy(
|
|
176
|
+
{
|
|
177
|
+
surface: "console",
|
|
178
|
+
ownershipFilter: "workspace",
|
|
179
|
+
relativePath: "/crm/customers"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
surfaceDefinitions: {
|
|
183
|
+
console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/requires a workspace-enabled surface/
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("resolveCrudSurfacePolicy rejects unknown or disabled surfaces", () => {
|
|
192
|
+
assert.throws(
|
|
193
|
+
() =>
|
|
194
|
+
resolveCrudSurfacePolicy(
|
|
195
|
+
{
|
|
196
|
+
surface: "missing",
|
|
197
|
+
ownershipFilter: "auto",
|
|
198
|
+
relativePath: "/crm/customers"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
surfaceDefinitions: {
|
|
202
|
+
console: { requiresWorkspace: false, requiresAuth: true, enabled: true }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
/cannot resolve surface "missing"/
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
assert.throws(
|
|
210
|
+
() =>
|
|
211
|
+
resolveCrudSurfacePolicy(
|
|
212
|
+
{
|
|
213
|
+
surface: "console",
|
|
214
|
+
ownershipFilter: "auto",
|
|
215
|
+
relativePath: "/crm/customers"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
surfaceDefinitions: {
|
|
219
|
+
console: { requiresWorkspace: false, requiresAuth: true, enabled: false }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
),
|
|
223
|
+
/surface "console" is disabled/
|
|
224
|
+
);
|
|
225
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { crudResource } from "../src/shared/crud/crudResource.js";
|
|
4
|
+
|
|
5
|
+
test("crudResource normalizes create payload", () => {
|
|
6
|
+
const normalized = crudResource.operations.create.bodyValidator.normalize({
|
|
7
|
+
textField: " Example text ",
|
|
8
|
+
dateField: "2026-03-11",
|
|
9
|
+
numberField: "42.5"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(normalized, {
|
|
13
|
+
textField: "Example text",
|
|
14
|
+
dateField: "2026-03-11 00:00:00.000",
|
|
15
|
+
numberField: 42.5
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("crudResource normalizes list output", () => {
|
|
20
|
+
const normalized = crudResource.operations.list.outputValidator.normalize({
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
id: "7",
|
|
24
|
+
textField: " Example text ",
|
|
25
|
+
dateField: "2026-03-10",
|
|
26
|
+
numberField: "99",
|
|
27
|
+
createdAt: "2026-03-11 00:00:00.000",
|
|
28
|
+
updatedAt: "2026-03-11 00:00:00.000"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
nextCursor: " 8 "
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(normalized.items[0].id, 7);
|
|
35
|
+
assert.equal(normalized.items[0].textField, "Example text");
|
|
36
|
+
assert.equal(normalized.items[0].dateField, "2026-03-10T00:00:00.000Z");
|
|
37
|
+
assert.equal(normalized.items[0].numberField, 99);
|
|
38
|
+
assert.match(normalized.items[0].createdAt, /T/);
|
|
39
|
+
assert.match(normalized.items[0].updatedAt, /T/);
|
|
40
|
+
assert.equal(normalized.nextCursor, "8");
|
|
41
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createActions } from "../src/server/actions.js";
|
|
4
|
+
import { createActionIds } from "../src/server/actionIds.js";
|
|
5
|
+
import { createRepository } from "../src/server/repository.js";
|
|
6
|
+
import { registerRoutes } from "../src/server/registerRoutes.js";
|
|
7
|
+
|
|
8
|
+
test("createActionIds requires explicit actionIdPrefix", () => {
|
|
9
|
+
assert.throws(
|
|
10
|
+
() => createActionIds(""),
|
|
11
|
+
/requires actionIdPrefix/
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("createRepository requires explicit tableName", () => {
|
|
16
|
+
const knex = () => {
|
|
17
|
+
throw new Error("not expected");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => createRepository(knex, {}),
|
|
22
|
+
/requires tableName/
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("createActions requires explicit surface", () => {
|
|
27
|
+
assert.throws(
|
|
28
|
+
() =>
|
|
29
|
+
createActions({
|
|
30
|
+
actionIdPrefix: "crud.customers"
|
|
31
|
+
}),
|
|
32
|
+
/requires a non-empty surface/
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("registerRoutes requires explicit routeRelativePath and actionIds", () => {
|
|
37
|
+
const app = {
|
|
38
|
+
make() {
|
|
39
|
+
return {
|
|
40
|
+
register() {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
assert.throws(
|
|
46
|
+
() => registerRoutes(app, {}),
|
|
47
|
+
/requires routeRelativePath/
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
assert.throws(
|
|
51
|
+
() =>
|
|
52
|
+
registerRoutes(app, {
|
|
53
|
+
routeRelativePath: "/customers",
|
|
54
|
+
routeSurfaceRequiresWorkspace: true,
|
|
55
|
+
actionIds: {
|
|
56
|
+
list: "crud.customers.list"
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
/requires actionIds.view/
|
|
60
|
+
);
|
|
61
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createService } from "../src/server/service.js";
|
|
4
|
+
|
|
5
|
+
test("crudService delegates CRUD operations to the repository", async () => {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const crudRepository = {
|
|
8
|
+
async list(query) {
|
|
9
|
+
calls.push(["list", query]);
|
|
10
|
+
return { items: [], nextCursor: null };
|
|
11
|
+
},
|
|
12
|
+
async findById(recordId) {
|
|
13
|
+
calls.push(["findById", recordId]);
|
|
14
|
+
return { id: recordId, textField: "Example", dateField: "2026-03-11T00:00:00.000Z", numberField: 3 };
|
|
15
|
+
},
|
|
16
|
+
async create(payload) {
|
|
17
|
+
calls.push(["create", payload]);
|
|
18
|
+
return { id: 1, ...payload };
|
|
19
|
+
},
|
|
20
|
+
async updateById(recordId, payload) {
|
|
21
|
+
calls.push(["updateById", recordId, payload]);
|
|
22
|
+
return { id: recordId, ...payload };
|
|
23
|
+
},
|
|
24
|
+
async deleteById(recordId) {
|
|
25
|
+
calls.push(["deleteById", recordId]);
|
|
26
|
+
return { id: recordId, deleted: true };
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const service = createService({ crudRepository });
|
|
31
|
+
|
|
32
|
+
const options = {};
|
|
33
|
+
await service.listRecords({ limit: 10 }, options);
|
|
34
|
+
await service.getRecord(3, options);
|
|
35
|
+
await service.createRecord({ textField: "Example", dateField: "2026-03-11", numberField: 3 }, options);
|
|
36
|
+
await service.updateRecord(4, { textField: "Changed" }, options);
|
|
37
|
+
await service.deleteRecord(5, options);
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(calls, [
|
|
40
|
+
["list", { limit: 10 }],
|
|
41
|
+
["findById", 3],
|
|
42
|
+
["create", { textField: "Example", dateField: "2026-03-11", numberField: 3 }],
|
|
43
|
+
["updateById", 4, { textField: "Changed" }],
|
|
44
|
+
["deleteById", 5]
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("crudService throws 404 when a record is missing", async () => {
|
|
49
|
+
const service = createService({
|
|
50
|
+
crudRepository: {
|
|
51
|
+
async list() {
|
|
52
|
+
return { items: [], nextCursor: null };
|
|
53
|
+
},
|
|
54
|
+
async findById() {
|
|
55
|
+
return null;
|
|
56
|
+
},
|
|
57
|
+
async create(payload) {
|
|
58
|
+
return { id: 1, ...payload };
|
|
59
|
+
},
|
|
60
|
+
async updateById() {
|
|
61
|
+
return null;
|
|
62
|
+
},
|
|
63
|
+
async deleteById() {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await assert.rejects(
|
|
70
|
+
() => service.getRecord(9, {}),
|
|
71
|
+
(error) => error?.status === 404 && error?.message === "Record not found."
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await assert.rejects(
|
|
75
|
+
() => service.updateRecord(9, { textField: "Changed" }, {}),
|
|
76
|
+
(error) => error?.status === 404 && error?.message === "Record not found."
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
() => service.deleteRecord(9, {}),
|
|
81
|
+
(error) => error?.status === 404 && error?.message === "Record not found."
|
|
82
|
+
);
|
|
83
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import { registerRoutes } from "../src/server/registerRoutes.js";
|
|
5
|
+
|
|
6
|
+
function createReplyDouble() {
|
|
7
|
+
return {
|
|
8
|
+
statusCode: 200,
|
|
9
|
+
payload: null,
|
|
10
|
+
code(statusCode) {
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
return this;
|
|
13
|
+
},
|
|
14
|
+
send(payload) {
|
|
15
|
+
this.payload = payload;
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findRoute(routes, method, path) {
|
|
22
|
+
return routes.find((route) => route.method === method && route.path === path) || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("crud routes build create/update action input with explicit payload and patch keys", async () => {
|
|
26
|
+
const registeredRoutes = [];
|
|
27
|
+
const router = {
|
|
28
|
+
register(method, path, route, handler) {
|
|
29
|
+
registeredRoutes.push({
|
|
30
|
+
method,
|
|
31
|
+
path,
|
|
32
|
+
route,
|
|
33
|
+
handler
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const app = {
|
|
38
|
+
make(token) {
|
|
39
|
+
if (token !== KERNEL_TOKENS.HttpRouter) {
|
|
40
|
+
throw new Error(`Unexpected token: ${String(token)}`);
|
|
41
|
+
}
|
|
42
|
+
return router;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
registerRoutes(app, {
|
|
47
|
+
routeRelativePath: "/customers",
|
|
48
|
+
routeSurfaceRequiresWorkspace: true,
|
|
49
|
+
actionIds: {
|
|
50
|
+
list: "crud.customers.list",
|
|
51
|
+
view: "crud.customers.view",
|
|
52
|
+
create: "crud.customers.create",
|
|
53
|
+
update: "crud.customers.update",
|
|
54
|
+
delete: "crud.customers.delete"
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const createRoute = findRoute(registeredRoutes, "POST", "/api/w/:workspaceSlug/workspace/customers");
|
|
59
|
+
const updateRoute = findRoute(registeredRoutes, "PATCH", "/api/w/:workspaceSlug/workspace/customers/:recordId");
|
|
60
|
+
assert.ok(createRoute);
|
|
61
|
+
assert.ok(updateRoute);
|
|
62
|
+
|
|
63
|
+
const calls = [];
|
|
64
|
+
const executeAction = async (payload) => {
|
|
65
|
+
calls.push(payload);
|
|
66
|
+
return {};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await createRoute.handler(
|
|
70
|
+
{
|
|
71
|
+
input: {
|
|
72
|
+
params: { workspaceSlug: "acme" },
|
|
73
|
+
body: { textField: "A", dateField: "2026-03-11", numberField: 2 }
|
|
74
|
+
},
|
|
75
|
+
executeAction
|
|
76
|
+
},
|
|
77
|
+
createReplyDouble()
|
|
78
|
+
);
|
|
79
|
+
await updateRoute.handler(
|
|
80
|
+
{
|
|
81
|
+
input: {
|
|
82
|
+
params: { workspaceSlug: "acme", recordId: 12 },
|
|
83
|
+
body: { textField: "Renamed" }
|
|
84
|
+
},
|
|
85
|
+
executeAction
|
|
86
|
+
},
|
|
87
|
+
createReplyDouble()
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(calls[0].input, {
|
|
91
|
+
workspaceSlug: "acme",
|
|
92
|
+
payload: { textField: "A", dateField: "2026-03-11", numberField: 2 }
|
|
93
|
+
});
|
|
94
|
+
assert.deepEqual(calls[1].input, {
|
|
95
|
+
workspaceSlug: "acme",
|
|
96
|
+
recordId: 12,
|
|
97
|
+
patch: { textField: "Renamed" }
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("crud routes omit workspaceSlug for non-workspace calls and apply configured route surface", async () => {
|
|
102
|
+
const registeredRoutes = [];
|
|
103
|
+
const router = {
|
|
104
|
+
register(method, path, route, handler) {
|
|
105
|
+
registeredRoutes.push({
|
|
106
|
+
method,
|
|
107
|
+
path,
|
|
108
|
+
route,
|
|
109
|
+
handler
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const app = {
|
|
114
|
+
make(token) {
|
|
115
|
+
if (token !== KERNEL_TOKENS.HttpRouter) {
|
|
116
|
+
throw new Error(`Unexpected token: ${String(token)}`);
|
|
117
|
+
}
|
|
118
|
+
return router;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
registerRoutes(app, {
|
|
123
|
+
routeRelativePath: "/customers",
|
|
124
|
+
routeSurface: "console",
|
|
125
|
+
actionIds: {
|
|
126
|
+
list: "crud.customers.list",
|
|
127
|
+
view: "crud.customers.view",
|
|
128
|
+
create: "crud.customers.create",
|
|
129
|
+
update: "crud.customers.update",
|
|
130
|
+
delete: "crud.customers.delete"
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const createRoute = findRoute(registeredRoutes, "POST", "/api/customers");
|
|
135
|
+
assert.ok(createRoute);
|
|
136
|
+
assert.equal(createRoute.route.surface, "console");
|
|
137
|
+
|
|
138
|
+
const calls = [];
|
|
139
|
+
const executeAction = async (payload) => {
|
|
140
|
+
calls.push(payload);
|
|
141
|
+
return {};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await createRoute.handler(
|
|
145
|
+
{
|
|
146
|
+
input: {
|
|
147
|
+
params: {},
|
|
148
|
+
body: { textField: "A", dateField: "2026-03-11", numberField: 2 }
|
|
149
|
+
},
|
|
150
|
+
executeAction
|
|
151
|
+
},
|
|
152
|
+
createReplyDouble()
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
assert.deepEqual(calls[0].input, {
|
|
156
|
+
payload: { textField: "A", dateField: "2026-03-11", numberField: 2 }
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("crud routes normalize route ownership filter values before registering visibility", () => {
|
|
161
|
+
const registeredRoutes = [];
|
|
162
|
+
const router = {
|
|
163
|
+
register(method, path, route, handler) {
|
|
164
|
+
registeredRoutes.push({
|
|
165
|
+
method,
|
|
166
|
+
path,
|
|
167
|
+
route,
|
|
168
|
+
handler
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const app = {
|
|
173
|
+
make(token) {
|
|
174
|
+
if (token !== KERNEL_TOKENS.HttpRouter) {
|
|
175
|
+
throw new Error(`Unexpected token: ${String(token)}`);
|
|
176
|
+
}
|
|
177
|
+
return router;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
registerRoutes(app, {
|
|
182
|
+
routeRelativePath: "/customers",
|
|
183
|
+
routeOwnershipFilter: " Workspace_User ",
|
|
184
|
+
actionIds: {
|
|
185
|
+
list: "crud.customers.list",
|
|
186
|
+
view: "crud.customers.view",
|
|
187
|
+
create: "crud.customers.create",
|
|
188
|
+
update: "crud.customers.update",
|
|
189
|
+
delete: "crud.customers.delete"
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
registerRoutes(app, {
|
|
193
|
+
routeRelativePath: "/customers-public",
|
|
194
|
+
routeOwnershipFilter: "not-a-real-filter",
|
|
195
|
+
actionIds: {
|
|
196
|
+
list: "crud.customers-public.list",
|
|
197
|
+
view: "crud.customers-public.view",
|
|
198
|
+
create: "crud.customers-public.create",
|
|
199
|
+
update: "crud.customers-public.update",
|
|
200
|
+
delete: "crud.customers-public.delete"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const workspaceUserRoute = findRoute(registeredRoutes, "GET", "/api/customers");
|
|
205
|
+
const fallbackPublicRoute = findRoute(registeredRoutes, "GET", "/api/customers-public");
|
|
206
|
+
|
|
207
|
+
assert.ok(workspaceUserRoute);
|
|
208
|
+
assert.ok(fallbackPublicRoute);
|
|
209
|
+
assert.equal(workspaceUserRoute.route.visibility, "workspace_user");
|
|
210
|
+
assert.equal(fallbackPublicRoute.route.visibility, "public");
|
|
211
|
+
});
|