@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.
@@ -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
- const actionPermissions = Object.freeze({
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
- require: "all",
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
- require: "all",
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
- require: "all",
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
- require: "all",
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
- require: "all",
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: routeSurfaceRequiresWorkspace === true,
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
- paramsValidator: routeParamsValidator,
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
- ...buildWorkspaceInputFromRouteParams(request.input.params),
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
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
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
- ...buildWorkspaceInputFromRouteParams(request.input.params),
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
- paramsValidator: routeParamsValidator,
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
- ...buildWorkspaceInputFromRouteParams(request.input.params),
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
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
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
- ...buildWorkspaceInputFromRouteParams(request.input.params),
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
- paramsValidator: [routeParamsValidator, recordIdParamsValidator],
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
- ...buildWorkspaceInputFromRouteParams(request.input.params),
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 { buildTemplateContext, __testables } from "../src/server/buildTemplateContext.js";
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("buildTemplateContext requires table-name", async () => {
222
- await assert.rejects(
223
- buildTemplateContext({
224
- appRoot: process.cwd(),
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
- /requires option "table-name"/
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("crud generator appends member role grants for generated CRUD permissions", () => {
74
- assert.deepEqual(
75
- descriptor.mutations.text,
76
- [
77
- {
78
- op: "append-text",
79
- file: "config/roles.js",
80
- position: "bottom",
81
- skipIfContains: "\"crud.${option:namespace|snake}.list\"",
82
- value:
83
- "\nroleCatalog.roles.member.permissions.push(\n \"crud.${option:namespace|snake}.list\",\n \"crud.${option:namespace|snake}.view\",\n \"crud.${option:namespace|snake}.create\",\n \"crud.${option:namespace|snake}.update\",\n \"crud.${option:namespace|snake}.delete\"\n);\n",
84
- reason: "Grant generated CRUD action permissions to the default member role in the app-owned role catalog.",
85
- category: "crud",
86
- id: "crud-role-catalog-permissions-${option:namespace|snake}"
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
+ });