@jskit-ai/crud-server-generator 0.1.41 → 0.1.43
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 +31 -17
- package/package.json +6 -6
- package/src/server/buildTemplateContext.js +415 -14
- package/templates/src/local-package/server/CrudProvider.js +1 -2
- package/templates/src/local-package/server/actions.js +12 -50
- package/templates/src/local-package/server/registerRoutes.js +12 -21
- package/test/buildTemplateContext.test.js +202 -11
- package/test/crudServerGuards.test.js +21 -18
- package/test/packageDescriptor.test.js +37 -0
- package/test/routeInputContracts.test.js +63 -16
- package/test-support/templateServerFixture.js +134 -23
|
@@ -7,20 +7,14 @@ import {
|
|
|
7
7
|
lookupIncludeQueryValidator,
|
|
8
8
|
createCrudParentFilterQueryValidator
|
|
9
9
|
} from "@jskit-ai/crud-core/server/listQueryValidators";
|
|
10
|
-
import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
11
10
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
12
11
|
import { actionIds } from "./actionIds.js";
|
|
13
12
|
import { LIST_CONFIG } from "./listConfig.js";
|
|
13
|
+
__JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__
|
|
14
14
|
|
|
15
15
|
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
16
16
|
const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
|
|
17
|
-
|
|
18
|
-
list: "crud.${option:namespace|snake}.list",
|
|
19
|
-
view: "crud.${option:namespace|snake}.view",
|
|
20
|
-
create: "crud.${option:namespace|snake}.create",
|
|
21
|
-
update: "crud.${option:namespace|snake}.update",
|
|
22
|
-
delete: "crud.${option:namespace|snake}.delete"
|
|
23
|
-
});
|
|
17
|
+
__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__
|
|
24
18
|
|
|
25
19
|
function requireActionSurface(surface = "") {
|
|
26
20
|
const normalizedSurface = String(surface || "").trim().toLowerCase();
|
|
@@ -41,17 +35,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
41
35
|
kind: "query",
|
|
42
36
|
channels: ["api", "automation", "internal"],
|
|
43
37
|
surfaces: [actionSurface],
|
|
44
|
-
permission:
|
|
45
|
-
|
|
46
|
-
permissions: [actionPermissions.list]
|
|
47
|
-
},
|
|
48
|
-
inputValidator: [
|
|
49
|
-
workspaceSlugParamsValidator,
|
|
50
|
-
listCursorPaginationQueryValidator,
|
|
51
|
-
listSearchQueryValidator,
|
|
52
|
-
listParentFilterQueryValidator,
|
|
53
|
-
lookupIncludeQueryValidator
|
|
54
|
-
],
|
|
38
|
+
permission: __JSKIT_CRUD_LIST_ACTION_PERMISSION__,
|
|
39
|
+
inputValidator: __JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__,
|
|
55
40
|
outputValidator: resource.operations.list.outputValidator,
|
|
56
41
|
idempotency: "none",
|
|
57
42
|
audit: {
|
|
@@ -71,11 +56,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
71
56
|
kind: "query",
|
|
72
57
|
channels: ["api", "automation", "internal"],
|
|
73
58
|
surfaces: [actionSurface],
|
|
74
|
-
permission:
|
|
75
|
-
|
|
76
|
-
permissions: [actionPermissions.view]
|
|
77
|
-
},
|
|
78
|
-
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator, lookupIncludeQueryValidator],
|
|
59
|
+
permission: __JSKIT_CRUD_VIEW_ACTION_PERMISSION__,
|
|
60
|
+
inputValidator: __JSKIT_CRUD_VIEW_ACTION_INPUT_VALIDATOR__,
|
|
79
61
|
outputValidator: resource.operations.view.outputValidator,
|
|
80
62
|
idempotency: "none",
|
|
81
63
|
audit: {
|
|
@@ -96,16 +78,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
96
78
|
kind: "command",
|
|
97
79
|
channels: ["api", "automation", "internal"],
|
|
98
80
|
surfaces: [actionSurface],
|
|
99
|
-
permission:
|
|
100
|
-
|
|
101
|
-
permissions: [actionPermissions.create]
|
|
102
|
-
},
|
|
103
|
-
inputValidator: [
|
|
104
|
-
workspaceSlugParamsValidator,
|
|
105
|
-
{
|
|
106
|
-
payload: resource.operations.create.bodyValidator
|
|
107
|
-
}
|
|
108
|
-
],
|
|
81
|
+
permission: __JSKIT_CRUD_CREATE_ACTION_PERMISSION__,
|
|
82
|
+
inputValidator: __JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__,
|
|
109
83
|
outputValidator: resource.operations.create.outputValidator,
|
|
110
84
|
idempotency: "optional",
|
|
111
85
|
audit: {
|
|
@@ -125,17 +99,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
125
99
|
kind: "command",
|
|
126
100
|
channels: ["api", "automation", "internal"],
|
|
127
101
|
surfaces: [actionSurface],
|
|
128
|
-
permission:
|
|
129
|
-
|
|
130
|
-
permissions: [actionPermissions.update]
|
|
131
|
-
},
|
|
132
|
-
inputValidator: [
|
|
133
|
-
workspaceSlugParamsValidator,
|
|
134
|
-
recordIdParamsValidator,
|
|
135
|
-
{
|
|
136
|
-
patch: resource.operations.patch.bodyValidator
|
|
137
|
-
}
|
|
138
|
-
],
|
|
102
|
+
permission: __JSKIT_CRUD_UPDATE_ACTION_PERMISSION__,
|
|
103
|
+
inputValidator: __JSKIT_CRUD_UPDATE_ACTION_INPUT_VALIDATOR__,
|
|
139
104
|
outputValidator: resource.operations.patch.outputValidator,
|
|
140
105
|
idempotency: "optional",
|
|
141
106
|
audit: {
|
|
@@ -155,11 +120,8 @@ function createActions({ surface = "" } = {}) {
|
|
|
155
120
|
kind: "command",
|
|
156
121
|
channels: ["api", "automation", "internal"],
|
|
157
122
|
surfaces: [actionSurface],
|
|
158
|
-
permission:
|
|
159
|
-
|
|
160
|
-
permissions: [actionPermissions.delete]
|
|
161
|
-
},
|
|
162
|
-
inputValidator: [workspaceSlugParamsValidator, recordIdParamsValidator],
|
|
123
|
+
permission: __JSKIT_CRUD_DELETE_ACTION_PERMISSION__,
|
|
124
|
+
inputValidator: __JSKIT_CRUD_DELETE_ACTION_INPUT_VALIDATOR__,
|
|
163
125
|
outputValidator: resource.operations.delete.outputValidator,
|
|
164
126
|
idempotency: "optional",
|
|
165
127
|
audit: {
|
|
@@ -9,13 +9,12 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
recordIdParamsValidator
|
|
11
11
|
} from "@jskit-ai/kernel/shared/validators";
|
|
12
|
-
import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
|
|
13
12
|
import { checkRouteVisibility } from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
14
|
-
import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/support/workspaceRouteInput";
|
|
15
13
|
import { resolveApiBasePath } from "@jskit-ai/users-core/shared/support/usersApiPaths";
|
|
16
14
|
import { actionIds } from "./actionIds.js";
|
|
17
15
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
18
16
|
import { LIST_CONFIG } from "./listConfig.js";
|
|
17
|
+
__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__
|
|
19
18
|
|
|
20
19
|
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator(LIST_CONFIG);
|
|
21
20
|
const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
|
|
@@ -25,14 +24,13 @@ function registerRoutes(
|
|
|
25
24
|
{
|
|
26
25
|
routeOwnershipFilter = "public",
|
|
27
26
|
routeSurface = "",
|
|
28
|
-
routeSurfaceRequiresWorkspace = false,
|
|
29
27
|
routeRelativePath = ""
|
|
30
28
|
} = {}
|
|
31
29
|
) {
|
|
32
30
|
const router = app.make("jskit.http.router");
|
|
33
31
|
const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
|
|
34
32
|
const routeBase = resolveApiBasePath({
|
|
35
|
-
surfaceRequiresWorkspace:
|
|
33
|
+
surfaceRequiresWorkspace: __JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__,
|
|
36
34
|
relativePath: routeRelativePath
|
|
37
35
|
});
|
|
38
36
|
|
|
@@ -47,7 +45,7 @@ function registerRoutes(
|
|
|
47
45
|
tags: ["crud"],
|
|
48
46
|
summary: "List records."
|
|
49
47
|
},
|
|
50
|
-
|
|
48
|
+
__JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__
|
|
51
49
|
queryValidator: [
|
|
52
50
|
listCursorPaginationQueryValidator,
|
|
53
51
|
listSearchQueryValidator,
|
|
@@ -60,8 +58,7 @@ function registerRoutes(
|
|
|
60
58
|
},
|
|
61
59
|
async function (request, reply) {
|
|
62
60
|
const listInput = {
|
|
63
|
-
|
|
64
|
-
...(request.input.query || {})
|
|
61
|
+
__JSKIT_CRUD_LIST_ROUTE_INPUT_LINES__
|
|
65
62
|
};
|
|
66
63
|
const response = await request.executeAction({
|
|
67
64
|
actionId: actionIds.list,
|
|
@@ -82,7 +79,7 @@ function registerRoutes(
|
|
|
82
79
|
tags: ["crud"],
|
|
83
80
|
summary: "View a record."
|
|
84
81
|
},
|
|
85
|
-
|
|
82
|
+
__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__
|
|
86
83
|
queryValidator: [lookupIncludeQueryValidator],
|
|
87
84
|
responseValidators: withStandardErrorResponses({
|
|
88
85
|
200: resource.operations.view.outputValidator
|
|
@@ -92,9 +89,7 @@ function registerRoutes(
|
|
|
92
89
|
const response = await request.executeAction({
|
|
93
90
|
actionId: actionIds.view,
|
|
94
91
|
input: {
|
|
95
|
-
|
|
96
|
-
recordId: request.input.params.recordId,
|
|
97
|
-
...(request.input.query || {})
|
|
92
|
+
__JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__
|
|
98
93
|
}
|
|
99
94
|
});
|
|
100
95
|
reply.code(200).send(response);
|
|
@@ -112,7 +107,7 @@ function registerRoutes(
|
|
|
112
107
|
tags: ["crud"],
|
|
113
108
|
summary: "Create a record."
|
|
114
109
|
},
|
|
115
|
-
|
|
110
|
+
__JSKIT_CRUD_CREATE_ROUTE_PARAMS_VALIDATOR_LINE__
|
|
116
111
|
bodyValidator: resource.operations.create.bodyValidator,
|
|
117
112
|
responseValidators: withStandardErrorResponses(
|
|
118
113
|
{
|
|
@@ -125,8 +120,7 @@ function registerRoutes(
|
|
|
125
120
|
const response = await request.executeAction({
|
|
126
121
|
actionId: actionIds.create,
|
|
127
122
|
input: {
|
|
128
|
-
|
|
129
|
-
payload: request.input.body
|
|
123
|
+
__JSKIT_CRUD_CREATE_ROUTE_INPUT_LINES__
|
|
130
124
|
}
|
|
131
125
|
});
|
|
132
126
|
reply.code(201).send(response);
|
|
@@ -144,7 +138,7 @@ function registerRoutes(
|
|
|
144
138
|
tags: ["crud"],
|
|
145
139
|
summary: "Update a record."
|
|
146
140
|
},
|
|
147
|
-
|
|
141
|
+
__JSKIT_CRUD_UPDATE_ROUTE_PARAMS_VALIDATOR_LINE__
|
|
148
142
|
bodyValidator: resource.operations.patch.bodyValidator,
|
|
149
143
|
responseValidators: withStandardErrorResponses(
|
|
150
144
|
{
|
|
@@ -157,9 +151,7 @@ function registerRoutes(
|
|
|
157
151
|
const response = await request.executeAction({
|
|
158
152
|
actionId: actionIds.update,
|
|
159
153
|
input: {
|
|
160
|
-
|
|
161
|
-
recordId: request.input.params.recordId,
|
|
162
|
-
patch: request.input.body
|
|
154
|
+
__JSKIT_CRUD_UPDATE_ROUTE_INPUT_LINES__
|
|
163
155
|
}
|
|
164
156
|
});
|
|
165
157
|
reply.code(200).send(response);
|
|
@@ -177,7 +169,7 @@ function registerRoutes(
|
|
|
177
169
|
tags: ["crud"],
|
|
178
170
|
summary: "Delete a record."
|
|
179
171
|
},
|
|
180
|
-
|
|
172
|
+
__JSKIT_CRUD_DELETE_ROUTE_PARAMS_VALIDATOR_LINE__
|
|
181
173
|
responseValidators: withStandardErrorResponses({
|
|
182
174
|
200: resource.operations.delete.outputValidator
|
|
183
175
|
})
|
|
@@ -186,8 +178,7 @@ function registerRoutes(
|
|
|
186
178
|
const response = await request.executeAction({
|
|
187
179
|
actionId: actionIds.delete,
|
|
188
180
|
input: {
|
|
189
|
-
|
|
190
|
-
recordId: request.input.params.recordId
|
|
181
|
+
__JSKIT_CRUD_DELETE_ROUTE_INPUT_LINES__
|
|
191
182
|
}
|
|
192
183
|
});
|
|
193
184
|
reply.code(200).send(response);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import test from "node:test";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { __testables } from "../src/server/buildTemplateContext.js";
|
|
8
9
|
|
|
9
10
|
function createSnapshot({
|
|
10
11
|
tableName = "contacts",
|
|
@@ -152,6 +153,22 @@ function createSnapshot({
|
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
async function withTempApp(run, publicConfigSource) {
|
|
157
|
+
const appRoot = await mkdtemp(path.join(tmpdir(), "crud-server-generator-"));
|
|
158
|
+
try {
|
|
159
|
+
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
160
|
+
await writeFile(
|
|
161
|
+
path.join(appRoot, "package.json"),
|
|
162
|
+
`${JSON.stringify({ name: "crud-server-generator-test-app", private: true, type: "module" }, null, 2)}\n`,
|
|
163
|
+
"utf8"
|
|
164
|
+
);
|
|
165
|
+
await writeFile(path.join(appRoot, "config", "public.js"), publicConfigSource, "utf8");
|
|
166
|
+
return await run(appRoot);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(appRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
test("resolveOwnershipFilterForGeneration infers ownership filter for table introspection mode", () => {
|
|
156
173
|
const snapshotBoth = createSnapshot({
|
|
157
174
|
hasWorkspaceIdColumn: true,
|
|
@@ -218,15 +235,19 @@ test("resolveOwnershipFilterForGeneration rejects explicit ownership filters whe
|
|
|
218
235
|
);
|
|
219
236
|
});
|
|
220
237
|
|
|
221
|
-
test("
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
options: {
|
|
226
|
-
namespace: "contacts"
|
|
227
|
-
}
|
|
238
|
+
test("resolveCrudGenerationTableName defaults table-name from namespace", () => {
|
|
239
|
+
assert.equal(
|
|
240
|
+
__testables.resolveCrudGenerationTableName({
|
|
241
|
+
namespace: "contacts"
|
|
228
242
|
}),
|
|
229
|
-
|
|
243
|
+
"contacts"
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
__testables.resolveCrudGenerationTableName({
|
|
247
|
+
namespace: "contacts",
|
|
248
|
+
"table-name": "customer_contacts"
|
|
249
|
+
}),
|
|
250
|
+
"customer_contacts"
|
|
230
251
|
);
|
|
231
252
|
});
|
|
232
253
|
|
|
@@ -235,12 +256,51 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
235
256
|
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
236
257
|
namespace: "contacts",
|
|
237
258
|
snapshot,
|
|
238
|
-
resolvedOwnershipFilter: "workspace_user"
|
|
259
|
+
resolvedOwnershipFilter: "workspace_user",
|
|
260
|
+
surfaceRequiresWorkspace: true
|
|
239
261
|
});
|
|
240
262
|
|
|
241
263
|
assert.equal(replacements.__JSKIT_CRUD_TABLE_NAME__, "\"contacts\"");
|
|
242
264
|
assert.equal(replacements.__JSKIT_CRUD_ID_COLUMN__, "\"id\"");
|
|
265
|
+
assert.equal(replacements.__JSKIT_CRUD_SURFACE_ID__, "\"\"");
|
|
243
266
|
assert.equal(replacements.__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__, "workspace_user");
|
|
267
|
+
assert.match(
|
|
268
|
+
replacements.__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__,
|
|
269
|
+
/const actionPermissions = Object\.freeze\(\{/
|
|
270
|
+
);
|
|
271
|
+
assert.match(
|
|
272
|
+
replacements.__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__,
|
|
273
|
+
/"crud\.contacts\.delete"/
|
|
274
|
+
);
|
|
275
|
+
assert.equal(
|
|
276
|
+
replacements.__JSKIT_CRUD_LIST_ACTION_PERMISSION__,
|
|
277
|
+
'{ require: "all", permissions: [actionPermissions.list] }'
|
|
278
|
+
);
|
|
279
|
+
assert.match(
|
|
280
|
+
replacements.__JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__,
|
|
281
|
+
/workspaceSlugParamsValidator/
|
|
282
|
+
);
|
|
283
|
+
assert.match(
|
|
284
|
+
replacements.__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__,
|
|
285
|
+
/buildWorkspaceInputFromRouteParams/
|
|
286
|
+
);
|
|
287
|
+
assert.equal(replacements.__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__, "true");
|
|
288
|
+
assert.equal(
|
|
289
|
+
replacements.__JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__,
|
|
290
|
+
"[workspaceSlugParamsValidator, listCursorPaginationQueryValidator, listSearchQueryValidator, listParentFilterQueryValidator, lookupIncludeQueryValidator]"
|
|
291
|
+
);
|
|
292
|
+
assert.equal(
|
|
293
|
+
replacements.__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__,
|
|
294
|
+
" paramsValidator: [routeParamsValidator, recordIdParamsValidator],"
|
|
295
|
+
);
|
|
296
|
+
assert.match(
|
|
297
|
+
replacements.__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__,
|
|
298
|
+
/roleCatalog\.roles\.member\.permissions\.push\(/
|
|
299
|
+
);
|
|
300
|
+
assert.match(
|
|
301
|
+
replacements.__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__,
|
|
302
|
+
/"crud\.contacts\.delete"/
|
|
303
|
+
);
|
|
244
304
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.bigIncrements\("id"\)/);
|
|
245
305
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.string\("first_name", 160\)/);
|
|
246
306
|
assert.equal(replacements.__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__, "");
|
|
@@ -282,6 +342,129 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
282
342
|
assert.equal(replacements.__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__, "");
|
|
283
343
|
});
|
|
284
344
|
|
|
345
|
+
test("buildReplacementsFromSnapshot omits named permissions and role grants when disabled", () => {
|
|
346
|
+
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
347
|
+
namespace: "contacts",
|
|
348
|
+
snapshot: createSnapshot({
|
|
349
|
+
hasWorkspaceIdColumn: false,
|
|
350
|
+
hasUserIdColumn: false
|
|
351
|
+
}),
|
|
352
|
+
resolvedOwnershipFilter: "public",
|
|
353
|
+
surfaceRequiresWorkspace: false
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
assert.match(
|
|
357
|
+
replacements.__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__,
|
|
358
|
+
/const authenticatedPermission = Object\.freeze\(\{/
|
|
359
|
+
);
|
|
360
|
+
assert.equal(replacements.__JSKIT_CRUD_SURFACE_ID__, "\"\"");
|
|
361
|
+
assert.equal(replacements.__JSKIT_CRUD_LIST_ACTION_PERMISSION__, "authenticatedPermission");
|
|
362
|
+
assert.equal(replacements.__JSKIT_CRUD_DELETE_ACTION_PERMISSION__, "authenticatedPermission");
|
|
363
|
+
assert.equal(replacements.__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__, "");
|
|
364
|
+
assert.equal(replacements.__JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__, "");
|
|
365
|
+
assert.equal(replacements.__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__, "");
|
|
366
|
+
assert.equal(replacements.__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__, "false");
|
|
367
|
+
assert.equal(
|
|
368
|
+
replacements.__JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__,
|
|
369
|
+
"{ payload: resource.operations.create.bodyValidator }"
|
|
370
|
+
);
|
|
371
|
+
assert.equal(replacements.__JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__, "");
|
|
372
|
+
assert.equal(
|
|
373
|
+
replacements.__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__,
|
|
374
|
+
" paramsValidator: recordIdParamsValidator,"
|
|
375
|
+
);
|
|
376
|
+
assert.equal(
|
|
377
|
+
replacements.__JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__,
|
|
378
|
+
[
|
|
379
|
+
" recordId: request.input.params.recordId,",
|
|
380
|
+
" ...(request.input.query || {})"
|
|
381
|
+
].join("\n")
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("resolveCrudSurfaceRequiresWorkspace follows surface workspace requirements from app config", async () => {
|
|
386
|
+
await withTempApp(
|
|
387
|
+
async (appRoot) => {
|
|
388
|
+
assert.equal(
|
|
389
|
+
await __testables.resolveCrudSurfaceRequiresWorkspace({
|
|
390
|
+
appRoot,
|
|
391
|
+
options: {
|
|
392
|
+
namespace: "contacts",
|
|
393
|
+
surface: "home",
|
|
394
|
+
"ownership-filter": "auto"
|
|
395
|
+
}
|
|
396
|
+
}),
|
|
397
|
+
false
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
assert.equal(
|
|
401
|
+
await __testables.resolveCrudSurfaceRequiresWorkspace({
|
|
402
|
+
appRoot,
|
|
403
|
+
options: {
|
|
404
|
+
namespace: "contacts",
|
|
405
|
+
surface: "admin",
|
|
406
|
+
"ownership-filter": "auto"
|
|
407
|
+
}
|
|
408
|
+
}),
|
|
409
|
+
true
|
|
410
|
+
);
|
|
411
|
+
},
|
|
412
|
+
`export const config = {
|
|
413
|
+
surfaceDefinitions: {
|
|
414
|
+
home: { id: "home", enabled: true, requiresAuth: true, requiresWorkspace: false },
|
|
415
|
+
admin: { id: "admin", enabled: true, requiresAuth: true, requiresWorkspace: true }
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
`
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("resolveCrudGenerationSurfaceId defaults home for non-workspace apps", async () => {
|
|
423
|
+
await withTempApp(
|
|
424
|
+
async (appRoot) => {
|
|
425
|
+
assert.equal(
|
|
426
|
+
await __testables.resolveCrudGenerationSurfaceId({
|
|
427
|
+
appRoot,
|
|
428
|
+
options: {
|
|
429
|
+
namespace: "contacts"
|
|
430
|
+
}
|
|
431
|
+
}),
|
|
432
|
+
"home"
|
|
433
|
+
);
|
|
434
|
+
},
|
|
435
|
+
`export const config = {
|
|
436
|
+
surfaceDefinitions: {
|
|
437
|
+
home: { id: "home", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
438
|
+
console: { id: "console", enabled: true, requiresAuth: true, requiresWorkspace: false }
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
`
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("resolveCrudGenerationSurfaceId requires explicit surface for workspace-capable apps", async () => {
|
|
446
|
+
await withTempApp(
|
|
447
|
+
async (appRoot) => {
|
|
448
|
+
await assert.rejects(
|
|
449
|
+
__testables.resolveCrudGenerationSurfaceId({
|
|
450
|
+
appRoot,
|
|
451
|
+
options: {
|
|
452
|
+
namespace: "contacts"
|
|
453
|
+
}
|
|
454
|
+
}),
|
|
455
|
+
/requires option "surface"/
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
`export const config = {
|
|
459
|
+
surfaceDefinitions: {
|
|
460
|
+
home: { id: "home", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
461
|
+
admin: { id: "admin", enabled: true, requiresAuth: true, requiresWorkspace: true }
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
`
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
285
468
|
test("buildReplacementsFromSnapshot omits default list ordering when created_at is absent", () => {
|
|
286
469
|
const snapshot = createSnapshot({
|
|
287
470
|
hasCreatedAtColumn: false
|
|
@@ -336,6 +519,7 @@ test("buildReplacementsFromSnapshot renders append-only field meta entries from
|
|
|
336
519
|
};
|
|
337
520
|
|
|
338
521
|
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
522
|
+
namespace: "contacts",
|
|
339
523
|
snapshot,
|
|
340
524
|
resolvedOwnershipFilter: "workspace_user"
|
|
341
525
|
});
|
|
@@ -380,6 +564,7 @@ test("buildReplacementsFromSnapshot renders enum field meta options as select co
|
|
|
380
564
|
};
|
|
381
565
|
|
|
382
566
|
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
567
|
+
namespace: "contacts",
|
|
383
568
|
snapshot,
|
|
384
569
|
resolvedOwnershipFilter: "public"
|
|
385
570
|
});
|
|
@@ -493,6 +678,7 @@ test("buildReplacementsFromSnapshot preserves custom collations, hash unique ind
|
|
|
493
678
|
hasUserIdColumn: false
|
|
494
679
|
});
|
|
495
680
|
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
681
|
+
namespace: "services",
|
|
496
682
|
snapshot: {
|
|
497
683
|
...snapshot,
|
|
498
684
|
columns: Object.freeze([
|
|
@@ -668,6 +854,10 @@ test("crud actions and routes templates share LIST_CONFIG for cursor validation"
|
|
|
668
854
|
assert.match(actionsTemplateSource, /createCrudCursorPaginationQueryValidator/);
|
|
669
855
|
assert.match(actionsTemplateSource, /import \{ LIST_CONFIG \} from "\.\/listConfig\.js";/);
|
|
670
856
|
assert.match(actionsTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(LIST_CONFIG\);/);
|
|
857
|
+
assert.match(actionsTemplateSource, /__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__/);
|
|
858
|
+
assert.match(actionsTemplateSource, /__JSKIT_CRUD_LIST_ACTION_PERMISSION__/);
|
|
859
|
+
assert.doesNotMatch(actionsTemplateSource, /ACTIONS_REQUIRE_NAMED_PERMISSIONS/);
|
|
860
|
+
assert.doesNotMatch(actionsTemplateSource, /createActionPermission/);
|
|
671
861
|
assert.match(registerRoutesTemplateSource, /createCrudCursorPaginationQueryValidator/);
|
|
672
862
|
assert.match(registerRoutesTemplateSource, /import \{ LIST_CONFIG \} from "\.\/listConfig\.js";/);
|
|
673
863
|
assert.match(registerRoutesTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(LIST_CONFIG\);/);
|
|
@@ -738,6 +928,7 @@ test("buildReplacementsFromSnapshot uses shared framework time schemas in genera
|
|
|
738
928
|
enumValues: Object.freeze([])
|
|
739
929
|
});
|
|
740
930
|
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
931
|
+
namespace: "opening-hours",
|
|
741
932
|
snapshot: {
|
|
742
933
|
...snapshot,
|
|
743
934
|
columns: Object.freeze([...snapshot.columns, timeColumn])
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import test, { after } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { recordIdParamsValidator } from "@jskit-ai/kernel/shared/validators";
|
|
3
4
|
import { createTemplateServerFixture } from "../test-support/templateServerFixture.js";
|
|
4
|
-
import descriptor from "../package.descriptor.mjs";
|
|
5
5
|
|
|
6
6
|
const fixture = await createTemplateServerFixture();
|
|
7
|
+
const nonWorkspaceFixture = await createTemplateServerFixture({
|
|
8
|
+
surfaceRequiresWorkspace: false,
|
|
9
|
+
requiresNamedPermissions: false
|
|
10
|
+
});
|
|
7
11
|
const { createActions } = await fixture.importServerModule("actions.js");
|
|
8
12
|
const { createRepository } = await fixture.importServerModule("repository.js");
|
|
13
|
+
const { createActions: createNonWorkspaceActions } = await nonWorkspaceFixture.importServerModule("actions.js");
|
|
9
14
|
|
|
10
15
|
after(async () => {
|
|
11
16
|
await fixture.cleanup();
|
|
17
|
+
await nonWorkspaceFixture.cleanup();
|
|
12
18
|
});
|
|
13
19
|
|
|
14
20
|
test("template createRepository defaults tableName from resource metadata", () => {
|
|
@@ -70,21 +76,18 @@ test("template createActions requires namespaced CRUD permissions by default", (
|
|
|
70
76
|
);
|
|
71
77
|
});
|
|
72
78
|
|
|
73
|
-
test("
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
]
|
|
89
|
-
);
|
|
79
|
+
test("template createActions omits workspace validators for non-workspace generation", () => {
|
|
80
|
+
const actions = createNonWorkspaceActions({ surface: "home" });
|
|
81
|
+
|
|
82
|
+
assert.equal(Array.isArray(actions[0].inputValidator), true);
|
|
83
|
+
assert.equal(actions[0].inputValidator.length, 4);
|
|
84
|
+
assert.equal(Array.isArray(actions[1].inputValidator), true);
|
|
85
|
+
assert.equal(actions[1].inputValidator.length, 2);
|
|
86
|
+
assert.equal(actions[1].inputValidator[0], recordIdParamsValidator);
|
|
87
|
+
assert.deepEqual(Object.keys(actions[2].inputValidator), ["payload"]);
|
|
88
|
+
assert.equal(Array.isArray(actions[3].inputValidator), true);
|
|
89
|
+
assert.equal(actions[3].inputValidator.length, 2);
|
|
90
|
+
assert.equal(actions[3].inputValidator[0], recordIdParamsValidator);
|
|
91
|
+
assert.equal(actions[4].inputValidator, recordIdParamsValidator);
|
|
92
|
+
assert.equal(actions[0].permission.require, "authenticated");
|
|
90
93
|
});
|
|
@@ -5,6 +5,17 @@ import descriptor from "../package.descriptor.mjs";
|
|
|
5
5
|
test("crud-server-generator surface option validates against enabled surface ids", () => {
|
|
6
6
|
assert.equal(descriptor.kind, "generator");
|
|
7
7
|
assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
|
|
8
|
+
assert.equal(descriptor.options?.surface?.required, false);
|
|
9
|
+
assert.equal(descriptor.options?.["ownership-filter"]?.validationType, "enum");
|
|
10
|
+
assert.deepEqual(
|
|
11
|
+
descriptor.options?.["ownership-filter"]?.allowedValues,
|
|
12
|
+
["auto", "public", "user", "workspace", "workspace_user"]
|
|
13
|
+
);
|
|
14
|
+
assert.equal(descriptor.options?.["table-name"]?.required, false);
|
|
15
|
+
assert.equal(
|
|
16
|
+
descriptor.options?.["table-name"]?.defaultFromOptionTemplate,
|
|
17
|
+
"${option:namespace}"
|
|
18
|
+
);
|
|
8
19
|
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("surface"), true);
|
|
9
20
|
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("force"), true);
|
|
10
21
|
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.createTarget?.pathTemplate, "packages/${option:namespace|kebab}");
|
|
@@ -24,3 +35,29 @@ test("crud-server-generator installs listConfig alongside server templates", ()
|
|
|
24
35
|
export: "buildTemplateContext"
|
|
25
36
|
});
|
|
26
37
|
});
|
|
38
|
+
|
|
39
|
+
test("crud-server-generator wires action and role mutations through template context", () => {
|
|
40
|
+
const files = descriptor.mutations?.files || [];
|
|
41
|
+
const actionsTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/actions.js");
|
|
42
|
+
const routesTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/registerRoutes.js");
|
|
43
|
+
const roleGrantMutation = (descriptor.mutations?.text || []).find((entry) => entry.file === "config/roles.js");
|
|
44
|
+
|
|
45
|
+
assert.ok(actionsTemplate);
|
|
46
|
+
assert.deepEqual(actionsTemplate.templateContext, {
|
|
47
|
+
entrypoint: "src/server/buildTemplateContext.js",
|
|
48
|
+
export: "buildTemplateContext"
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.ok(routesTemplate);
|
|
52
|
+
assert.deepEqual(routesTemplate.templateContext, {
|
|
53
|
+
entrypoint: "src/server/buildTemplateContext.js",
|
|
54
|
+
export: "buildTemplateContext"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
assert.ok(roleGrantMutation);
|
|
58
|
+
assert.equal(roleGrantMutation.value, "__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__");
|
|
59
|
+
assert.deepEqual(roleGrantMutation.templateContext, {
|
|
60
|
+
entrypoint: "src/server/buildTemplateContext.js",
|
|
61
|
+
export: "buildTemplateContext"
|
|
62
|
+
});
|
|
63
|
+
});
|