@jskit-ai/crud-server-generator 0.1.41 → 0.1.42

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/crud-server-generator",
4
- version: "0.1.41",
4
+ version: "0.1.42",
5
5
  kind: "generator",
6
6
  description: "CRUD server generator with routes, actions, and persistence scaffolding.",
7
7
  options: {
@@ -13,16 +13,18 @@ export default Object.freeze({
13
13
  promptHint: "Required slug (example: customers, appointments, vendors)."
14
14
  },
15
15
  surface: {
16
- required: true,
16
+ required: false,
17
17
  inputType: "text",
18
18
  validationType: "enabled-surface-id",
19
19
  promptLabel: "Target surface",
20
- promptHint: "Must match an enabled surface id."
20
+ promptHint: "Optional for non-workspace apps; otherwise must match an enabled surface id."
21
21
  },
22
22
  "ownership-filter": {
23
23
  required: true,
24
24
  inputType: "text",
25
25
  defaultValue: "auto",
26
+ validationType: "enum",
27
+ allowedValues: ["auto", "public", "user", "workspace", "workspace_user"],
26
28
  promptLabel: "Ownership filter",
27
29
  promptHint: "auto | public | user | workspace | workspace_user"
28
30
  },
@@ -34,11 +36,12 @@ export default Object.freeze({
34
36
  promptHint: "Optional subpath prepended to the CRUD route path (example: crm or ops/team-a)."
35
37
  },
36
38
  "table-name": {
37
- required: true,
39
+ required: false,
38
40
  inputType: "text",
39
41
  defaultValue: "",
42
+ defaultFromOptionTemplate: "${option:namespace}",
40
43
  promptLabel: "Table name",
41
- promptHint: "Required existing MySQL table to introspect for CRUD schema generation."
44
+ promptHint: "Existing MySQL table to introspect for CRUD schema generation (defaults to namespace)."
42
45
  },
43
46
  "id-column": {
44
47
  required: false,
@@ -148,13 +151,13 @@ export default Object.freeze({
148
151
  mutations: {
149
152
  dependencies: {
150
153
  runtime: {
151
- "@jskit-ai/auth-core": "0.1.32",
152
- "@jskit-ai/crud-core": "0.1.41",
153
- "@jskit-ai/database-runtime": "0.1.33",
154
- "@jskit-ai/http-runtime": "0.1.32",
155
- "@jskit-ai/kernel": "0.1.33",
156
- "@jskit-ai/realtime": "0.1.32",
157
- "@jskit-ai/users-core": "0.1.43",
154
+ "@jskit-ai/auth-core": "0.1.33",
155
+ "@jskit-ai/crud-core": "0.1.42",
156
+ "@jskit-ai/database-runtime": "0.1.34",
157
+ "@jskit-ai/http-runtime": "0.1.33",
158
+ "@jskit-ai/kernel": "0.1.34",
159
+ "@jskit-ai/realtime": "0.1.33",
160
+ "@jskit-ai/users-core": "0.1.44",
158
161
  "@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
159
162
  "typebox": "^1.0.81"
160
163
  },
@@ -208,7 +211,11 @@ export default Object.freeze({
208
211
  to: "packages/${option:namespace|kebab}/src/server/actions.js",
209
212
  reason: "Install app-local CRUD action definitions.",
210
213
  category: "crud",
211
- id: "crud-local-package-server-actions-${option:namespace|snake}"
214
+ id: "crud-local-package-server-actions-${option:namespace|snake}",
215
+ templateContext: {
216
+ entrypoint: "src/server/buildTemplateContext.js",
217
+ export: "buildTemplateContext"
218
+ }
212
219
  },
213
220
  {
214
221
  from: "templates/src/local-package/server/actionIds.js",
@@ -233,7 +240,11 @@ export default Object.freeze({
233
240
  to: "packages/${option:namespace|kebab}/src/server/registerRoutes.js",
234
241
  reason: "Install app-local CRUD route registration.",
235
242
  category: "crud",
236
- id: "crud-local-package-server-routes-${option:namespace|snake}"
243
+ id: "crud-local-package-server-routes-${option:namespace|snake}",
244
+ templateContext: {
245
+ entrypoint: "src/server/buildTemplateContext.js",
246
+ export: "buildTemplateContext"
247
+ }
237
248
  },
238
249
  {
239
250
  from: "templates/src/local-package/server/repository.js",
@@ -278,11 +289,14 @@ export default Object.freeze({
278
289
  file: "config/roles.js",
279
290
  position: "bottom",
280
291
  skipIfContains: "\"crud.${option:namespace|snake}.list\"",
281
- value:
282
- "\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",
292
+ value: "__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__",
283
293
  reason: "Grant generated CRUD action permissions to the default member role in the app-owned role catalog.",
284
294
  category: "crud",
285
- id: "crud-role-catalog-permissions-${option:namespace|snake}"
295
+ id: "crud-role-catalog-permissions-${option:namespace|snake}",
296
+ templateContext: {
297
+ entrypoint: "src/server/buildTemplateContext.js",
298
+ export: "buildTemplateContext"
299
+ }
286
300
  }
287
301
  ]
288
302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-server-generator",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -13,11 +13,11 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@babel/parser": "^7.29.2",
16
- "@jskit-ai/crud-core": "0.1.41",
17
- "@jskit-ai/database-runtime": "0.1.33",
18
- "@jskit-ai/http-runtime": "0.1.32",
19
- "@jskit-ai/kernel": "0.1.33",
20
- "@jskit-ai/users-core": "0.1.43",
16
+ "@jskit-ai/crud-core": "0.1.42",
17
+ "@jskit-ai/database-runtime": "0.1.34",
18
+ "@jskit-ai/http-runtime": "0.1.33",
19
+ "@jskit-ai/kernel": "0.1.34",
20
+ "@jskit-ai/users-core": "0.1.44",
21
21
  "recast": "^0.23.11",
22
22
  "typebox": "^1.0.81"
23
23
  }
@@ -8,20 +8,51 @@ import {
8
8
  resolveKnexConnectionFromEnvironment,
9
9
  toKnexClientId
10
10
  } from "@jskit-ai/database-runtime/shared";
11
+ import { resolveCrudSurfacePolicyFromAppConfig } from "@jskit-ai/crud-core/server/crudModuleConfig";
11
12
  import { checkCrudLookupFormControl } from "@jskit-ai/crud-core/shared/crudFieldMetaSupport";
13
+ import {
14
+ importFreshModuleFromAbsolutePath,
15
+ loadAppConfigFromModuleUrl,
16
+ resolveRequiredAppRoot
17
+ } from "@jskit-ai/kernel/server/support";
12
18
  import { normalizeCrudLookupNamespace } from "@jskit-ai/kernel/shared/support/crudLookup";
13
19
  import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
20
+ import descriptor from "../../package.descriptor.mjs";
14
21
 
15
22
  const DEFAULT_ID_COLUMN = "id";
16
- const OWNERSHIP_FILTER_AUTO = "auto";
17
- const OWNERSHIP_FILTER_VALUES = new Set([
18
- OWNERSHIP_FILTER_AUTO,
19
- "public",
20
- "user",
21
- "workspace",
22
- "workspace_user"
23
- ]);
23
+ const DEFAULT_OWNERSHIP_FILTER_VALUES = Object.freeze(["auto", "public", "user", "workspace", "workspace_user"]);
24
24
  const MYSQL_CLIENT_ID = "mysql2";
25
+ const CRUD_PERMISSION_OPERATIONS = Object.freeze(["list", "view", "create", "update", "delete"]);
26
+
27
+ function resolveAllowedValues(schema = {}, fallbackValues = []) {
28
+ const resolvedValues = [];
29
+ const seen = new Set();
30
+ for (const rawValue of Array.isArray(schema?.allowedValues) ? schema.allowedValues : []) {
31
+ const value = normalizeText(typeof rawValue === "string" ? rawValue : rawValue?.value).toLowerCase();
32
+ if (!value || seen.has(value)) {
33
+ continue;
34
+ }
35
+ seen.add(value);
36
+ resolvedValues.push(value);
37
+ }
38
+ if (resolvedValues.length > 0) {
39
+ return Object.freeze(resolvedValues);
40
+ }
41
+ return Object.freeze(
42
+ (Array.isArray(fallbackValues) ? fallbackValues : [])
43
+ .map((value) => normalizeText(value).toLowerCase())
44
+ .filter(Boolean)
45
+ );
46
+ }
47
+
48
+ const OWNERSHIP_FILTER_ALLOWED_VALUES = resolveAllowedValues(
49
+ descriptor?.options?.["ownership-filter"],
50
+ DEFAULT_OWNERSHIP_FILTER_VALUES
51
+ );
52
+ const OWNERSHIP_FILTER_AUTO = normalizeText(
53
+ descriptor?.options?.["ownership-filter"]?.defaultValue
54
+ ).toLowerCase() || "auto";
55
+ const OWNERSHIP_FILTER_VALUES = new Set(OWNERSHIP_FILTER_ALLOWED_VALUES);
25
56
 
26
57
  function resolveGlobalScaffoldCache() {
27
58
  const globalObject = globalThis;
@@ -54,7 +85,7 @@ function normalizeRequestedOwnershipFilter(value, { strict = false } = {}) {
54
85
  }
55
86
  if (strict) {
56
87
  throw new Error(
57
- `Invalid ownership filter "${normalized || String(value || "")}". Use: auto, public, user, workspace, workspace_user.`
88
+ `Invalid ownership filter "${normalized || String(value || "")}". Use: ${OWNERSHIP_FILTER_ALLOWED_VALUES.join(", ")}.`
58
89
  );
59
90
  }
60
91
  return OWNERSHIP_FILTER_AUTO;
@@ -187,7 +218,7 @@ async function importModuleFromApp(appRequire, moduleId, contextLabel) {
187
218
  }
188
219
 
189
220
  try {
190
- return await import(`${pathToFileURL(resolvedPath).href}?t=${Date.now()}_${Math.random()}`);
221
+ return await importFreshModuleFromAbsolutePath(resolvedPath);
191
222
  } catch (error) {
192
223
  throw new Error(
193
224
  `${contextLabel} failed loading "${moduleId}": ${String(error?.message || error || "unknown error")}`
@@ -195,6 +226,99 @@ async function importModuleFromApp(appRequire, moduleId, contextLabel) {
195
226
  }
196
227
  }
197
228
 
229
+ async function resolveCrudSurfaceRequiresWorkspace({
230
+ appRoot,
231
+ options,
232
+ surface = ""
233
+ } = {}) {
234
+ const namespace = normalizeText(options?.namespace);
235
+ const resolvedSurface = normalizeText(surface || options?.surface);
236
+ if (!namespace) {
237
+ throw new Error('crud template context requires option "namespace".');
238
+ }
239
+ if (!resolvedSurface) {
240
+ throw new Error('crud template context requires option "surface".');
241
+ }
242
+
243
+ const appConfig = await loadCrudAppConfig(appRoot);
244
+ const crudPolicy = resolveCrudSurfacePolicyFromAppConfig(
245
+ {
246
+ namespace,
247
+ surface: resolvedSurface,
248
+ ownershipFilter: options?.["ownership-filter"]
249
+ },
250
+ appConfig,
251
+ {
252
+ context: "crud template context"
253
+ }
254
+ );
255
+
256
+ return crudPolicy?.surfaceDefinition?.requiresWorkspace === true;
257
+ }
258
+
259
+ async function loadCrudAppConfig(appRoot = "") {
260
+ const resolvedAppRoot = resolveRequiredAppRoot(appRoot, {
261
+ context: "crud template context"
262
+ });
263
+ return loadAppConfigFromModuleUrl({
264
+ moduleUrl: pathToFileURL(path.join(resolvedAppRoot, "config", "public.js")).href
265
+ });
266
+ }
267
+
268
+ function resolveSurfaceDefinitions(appConfig = {}) {
269
+ const definitions = asRecord(appConfig?.surfaceDefinitions);
270
+ const resolved = {};
271
+ for (const [key, rawValue] of Object.entries(definitions)) {
272
+ const definition = asRecord(rawValue);
273
+ const id = normalizeText(definition.id || key).toLowerCase();
274
+ if (!id) {
275
+ continue;
276
+ }
277
+ resolved[id] = Object.freeze({
278
+ id,
279
+ enabled: definition.enabled !== false,
280
+ requiresWorkspace: definition.requiresWorkspace === true
281
+ });
282
+ }
283
+ return Object.freeze(resolved);
284
+ }
285
+
286
+ function resolveDefaultCrudSurfaceIdFromAppConfig(appConfig = {}) {
287
+ const surfaceDefinitions = resolveSurfaceDefinitions(appConfig);
288
+ const enabledSurfaceDefinitions = Object.values(surfaceDefinitions).filter((entry) => entry.enabled === true);
289
+ const hasEnabledWorkspaceSurface = enabledSurfaceDefinitions.some((entry) => entry.requiresWorkspace === true);
290
+ if (hasEnabledWorkspaceSurface) {
291
+ return "";
292
+ }
293
+
294
+ const homeSurface = surfaceDefinitions.home;
295
+ if (homeSurface?.enabled === true && homeSurface.requiresWorkspace !== true) {
296
+ return "home";
297
+ }
298
+
299
+ return "";
300
+ }
301
+
302
+ async function resolveCrudGenerationSurfaceId({
303
+ appRoot,
304
+ options
305
+ } = {}) {
306
+ const explicitSurface = normalizeText(options?.surface).toLowerCase();
307
+ if (explicitSurface) {
308
+ return explicitSurface;
309
+ }
310
+
311
+ const appConfig = await loadCrudAppConfig(appRoot);
312
+ const defaultSurface = resolveDefaultCrudSurfaceIdFromAppConfig(appConfig);
313
+ if (defaultSurface) {
314
+ return defaultSurface;
315
+ }
316
+
317
+ throw new Error(
318
+ 'crud template context requires option "surface" when the app has any enabled workspace surface or no enabled non-workspace "home" surface.'
319
+ );
320
+ }
321
+
198
322
  function resolveKnexFactory(moduleNamespace) {
199
323
  if (typeof moduleNamespace === "function") {
200
324
  return moduleNamespace;
@@ -1231,10 +1355,184 @@ function renderRepositoryListConfigLines(snapshot = {}) {
1231
1355
  ].join("\n");
1232
1356
  }
1233
1357
 
1358
+ function buildCrudPermissionIds(namespace = "") {
1359
+ const permissionNamespace = toSnakeCase(namespace);
1360
+ if (!permissionNamespace) {
1361
+ return null;
1362
+ }
1363
+
1364
+ return Object.freeze(
1365
+ Object.fromEntries(
1366
+ CRUD_PERMISSION_OPERATIONS.map((operation) => [operation, `crud.${permissionNamespace}.${operation}`])
1367
+ )
1368
+ );
1369
+ }
1370
+
1371
+ function normalizeCrudOperation(operation = "", context = "CRUD operation") {
1372
+ const normalizedOperation = normalizeText(operation).toLowerCase();
1373
+ if (!CRUD_PERMISSION_OPERATIONS.includes(normalizedOperation)) {
1374
+ throw new Error(`Unknown ${context} "${normalizedOperation || String(operation || "")}".`);
1375
+ }
1376
+ return normalizedOperation;
1377
+ }
1378
+
1379
+ function renderRoleCatalogPermissionGrants(namespace = "", { requiresNamedPermissions = true } = {}) {
1380
+ const permissionIds = buildCrudPermissionIds(namespace);
1381
+ if (!requiresNamedPermissions || !permissionIds) {
1382
+ return "";
1383
+ }
1384
+
1385
+ return [
1386
+ "roleCatalog.roles.member.permissions.push(",
1387
+ ` ${JSON.stringify(permissionIds.list)},`,
1388
+ ` ${JSON.stringify(permissionIds.view)},`,
1389
+ ` ${JSON.stringify(permissionIds.create)},`,
1390
+ ` ${JSON.stringify(permissionIds.update)},`,
1391
+ ` ${JSON.stringify(permissionIds.delete)}`,
1392
+ ");"
1393
+ ].join("\n");
1394
+ }
1395
+
1396
+ function renderActionPermissionSupport(namespace = "", { requiresNamedPermissions = true } = {}) {
1397
+ if (!requiresNamedPermissions) {
1398
+ return [
1399
+ "const authenticatedPermission = Object.freeze({",
1400
+ ' require: "authenticated"',
1401
+ "});"
1402
+ ].join("\n");
1403
+ }
1404
+
1405
+ const permissionIds = buildCrudPermissionIds(namespace);
1406
+ if (!permissionIds) {
1407
+ return "";
1408
+ }
1409
+
1410
+ return [
1411
+ "const actionPermissions = Object.freeze({",
1412
+ ` list: ${JSON.stringify(permissionIds.list)},`,
1413
+ ` view: ${JSON.stringify(permissionIds.view)},`,
1414
+ ` create: ${JSON.stringify(permissionIds.create)},`,
1415
+ ` update: ${JSON.stringify(permissionIds.update)},`,
1416
+ ` delete: ${JSON.stringify(permissionIds.delete)}`,
1417
+ "});"
1418
+ ].join("\n");
1419
+ }
1420
+
1421
+ function renderActionPermissionExpression(operation = "", { requiresNamedPermissions = true } = {}) {
1422
+ const normalizedOperation = normalizeCrudOperation(operation, "CRUD permission operation");
1423
+
1424
+ if (!requiresNamedPermissions) {
1425
+ return "authenticatedPermission";
1426
+ }
1427
+
1428
+ return `{ require: "all", permissions: [actionPermissions.${normalizedOperation}] }`;
1429
+ }
1430
+
1431
+ function renderRouteWorkspaceSupportImports({ surfaceRequiresWorkspace = true } = {}) {
1432
+ if (!surfaceRequiresWorkspace) {
1433
+ return "";
1434
+ }
1435
+
1436
+ return [
1437
+ 'import { routeParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";',
1438
+ 'import { buildWorkspaceInputFromRouteParams } from "@jskit-ai/users-core/server/support/workspaceRouteInput";'
1439
+ ].join("\n");
1440
+ }
1441
+
1442
+ function renderActionWorkspaceValidatorImport({ surfaceRequiresWorkspace = true } = {}) {
1443
+ if (!surfaceRequiresWorkspace) {
1444
+ return "";
1445
+ }
1446
+
1447
+ return 'import { workspaceSlugParamsValidator } from "@jskit-ai/users-core/server/validators/routeParamsValidator";';
1448
+ }
1449
+
1450
+ function renderRouteParamsValidatorLine(operation = "", { surfaceRequiresWorkspace = true } = {}) {
1451
+ const normalizedOperation = normalizeCrudOperation(operation, "CRUD route params validator operation");
1452
+ if (normalizedOperation === "list" || normalizedOperation === "create") {
1453
+ if (!surfaceRequiresWorkspace) {
1454
+ return "";
1455
+ }
1456
+ return " paramsValidator: routeParamsValidator,";
1457
+ }
1458
+
1459
+ if (!surfaceRequiresWorkspace) {
1460
+ return " paramsValidator: recordIdParamsValidator,";
1461
+ }
1462
+
1463
+ return " paramsValidator: [routeParamsValidator, recordIdParamsValidator],";
1464
+ }
1465
+
1466
+ function renderRouteInputLines(operation = "", { surfaceRequiresWorkspace = true } = {}) {
1467
+ const normalizedOperation = normalizeCrudOperation(operation, "CRUD route input operation");
1468
+ const lines = [];
1469
+
1470
+ if (surfaceRequiresWorkspace) {
1471
+ lines.push(" ...buildWorkspaceInputFromRouteParams(request.input.params),");
1472
+ }
1473
+
1474
+ if (normalizedOperation === "list") {
1475
+ lines.push(" ...(request.input.query || {})");
1476
+ return lines.join("\n");
1477
+ }
1478
+
1479
+ if (normalizedOperation === "view") {
1480
+ lines.push(" recordId: request.input.params.recordId,");
1481
+ lines.push(" ...(request.input.query || {})");
1482
+ return lines.join("\n");
1483
+ }
1484
+
1485
+ if (normalizedOperation === "create") {
1486
+ lines.push(" payload: request.input.body");
1487
+ return lines.join("\n");
1488
+ }
1489
+
1490
+ if (normalizedOperation === "update") {
1491
+ lines.push(" recordId: request.input.params.recordId,");
1492
+ lines.push(" patch: request.input.body");
1493
+ return lines.join("\n");
1494
+ }
1495
+
1496
+ lines.push(" recordId: request.input.params.recordId");
1497
+ return lines.join("\n");
1498
+ }
1499
+
1500
+ function renderActionInputValidatorExpression(operation = "", { surfaceRequiresWorkspace = true } = {}) {
1501
+ const normalizedOperation = normalizeCrudOperation(operation, "CRUD action input validator operation");
1502
+ const validators = [];
1503
+
1504
+ if (surfaceRequiresWorkspace) {
1505
+ validators.push("workspaceSlugParamsValidator");
1506
+ }
1507
+
1508
+ if (normalizedOperation === "list") {
1509
+ validators.push(
1510
+ "listCursorPaginationQueryValidator",
1511
+ "listSearchQueryValidator",
1512
+ "listParentFilterQueryValidator",
1513
+ "lookupIncludeQueryValidator"
1514
+ );
1515
+ } else if (normalizedOperation === "view") {
1516
+ validators.push("recordIdParamsValidator", "lookupIncludeQueryValidator");
1517
+ } else if (normalizedOperation === "create") {
1518
+ validators.push("{ payload: resource.operations.create.bodyValidator }");
1519
+ } else if (normalizedOperation === "update") {
1520
+ validators.push("recordIdParamsValidator", "{ patch: resource.operations.patch.bodyValidator }");
1521
+ } else {
1522
+ validators.push("recordIdParamsValidator");
1523
+ }
1524
+
1525
+ return validators.length === 1 ? validators[0] : `[${validators.join(", ")}]`;
1526
+ }
1527
+
1234
1528
  function buildReplacementsFromSnapshot({
1529
+ namespace = "",
1235
1530
  snapshot,
1236
- resolvedOwnershipFilter
1531
+ resolvedOwnershipFilter,
1532
+ surfaceRequiresWorkspace = true,
1533
+ surfaceId = ""
1237
1534
  }) {
1535
+ const requiresNamedPermissions = surfaceRequiresWorkspace === true;
1238
1536
  const scaffoldColumns = resolveScaffoldColumns(snapshot);
1239
1537
  const outputColumns = scaffoldColumns.filter((column) => !column.isOwnerColumn);
1240
1538
  const writableColumns = scaffoldColumns.filter((column) => column.writable);
@@ -1275,7 +1573,81 @@ function buildReplacementsFromSnapshot({
1275
1573
  const replacements = Object.freeze({
1276
1574
  __JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
1277
1575
  __JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
1576
+ __JSKIT_CRUD_SURFACE_ID__: JSON.stringify(normalizeText(surfaceId).toLowerCase()),
1278
1577
  __JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__: resolvedOwnershipFilter,
1578
+ __JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__: renderActionPermissionSupport(namespace, {
1579
+ requiresNamedPermissions
1580
+ }),
1581
+ __JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__: renderActionWorkspaceValidatorImport({
1582
+ surfaceRequiresWorkspace
1583
+ }),
1584
+ __JSKIT_CRUD_LIST_ACTION_PERMISSION__: renderActionPermissionExpression("list", {
1585
+ requiresNamedPermissions
1586
+ }),
1587
+ __JSKIT_CRUD_LIST_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("list", {
1588
+ surfaceRequiresWorkspace
1589
+ }),
1590
+ __JSKIT_CRUD_VIEW_ACTION_PERMISSION__: renderActionPermissionExpression("view", {
1591
+ requiresNamedPermissions
1592
+ }),
1593
+ __JSKIT_CRUD_VIEW_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("view", {
1594
+ surfaceRequiresWorkspace
1595
+ }),
1596
+ __JSKIT_CRUD_CREATE_ACTION_PERMISSION__: renderActionPermissionExpression("create", {
1597
+ requiresNamedPermissions
1598
+ }),
1599
+ __JSKIT_CRUD_CREATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("create", {
1600
+ surfaceRequiresWorkspace
1601
+ }),
1602
+ __JSKIT_CRUD_UPDATE_ACTION_PERMISSION__: renderActionPermissionExpression("update", {
1603
+ requiresNamedPermissions
1604
+ }),
1605
+ __JSKIT_CRUD_UPDATE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("update", {
1606
+ surfaceRequiresWorkspace
1607
+ }),
1608
+ __JSKIT_CRUD_DELETE_ACTION_PERMISSION__: renderActionPermissionExpression("delete", {
1609
+ requiresNamedPermissions
1610
+ }),
1611
+ __JSKIT_CRUD_DELETE_ACTION_INPUT_VALIDATOR__: renderActionInputValidatorExpression("delete", {
1612
+ surfaceRequiresWorkspace
1613
+ }),
1614
+ __JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__: renderRoleCatalogPermissionGrants(namespace, {
1615
+ requiresNamedPermissions
1616
+ }),
1617
+ __JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__: String(surfaceRequiresWorkspace === true),
1618
+ __JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__: renderRouteWorkspaceSupportImports({
1619
+ surfaceRequiresWorkspace
1620
+ }),
1621
+ __JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("list", {
1622
+ surfaceRequiresWorkspace
1623
+ }),
1624
+ __JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("view", {
1625
+ surfaceRequiresWorkspace
1626
+ }),
1627
+ __JSKIT_CRUD_CREATE_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("create", {
1628
+ surfaceRequiresWorkspace
1629
+ }),
1630
+ __JSKIT_CRUD_UPDATE_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("update", {
1631
+ surfaceRequiresWorkspace
1632
+ }),
1633
+ __JSKIT_CRUD_DELETE_ROUTE_PARAMS_VALIDATOR_LINE__: renderRouteParamsValidatorLine("delete", {
1634
+ surfaceRequiresWorkspace
1635
+ }),
1636
+ __JSKIT_CRUD_LIST_ROUTE_INPUT_LINES__: renderRouteInputLines("list", {
1637
+ surfaceRequiresWorkspace
1638
+ }),
1639
+ __JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__: renderRouteInputLines("view", {
1640
+ surfaceRequiresWorkspace
1641
+ }),
1642
+ __JSKIT_CRUD_CREATE_ROUTE_INPUT_LINES__: renderRouteInputLines("create", {
1643
+ surfaceRequiresWorkspace
1644
+ }),
1645
+ __JSKIT_CRUD_UPDATE_ROUTE_INPUT_LINES__: renderRouteInputLines("update", {
1646
+ surfaceRequiresWorkspace
1647
+ }),
1648
+ __JSKIT_CRUD_DELETE_ROUTE_INPUT_LINES__: renderRouteInputLines("delete", {
1649
+ surfaceRequiresWorkspace
1650
+ }),
1279
1651
  __JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
1280
1652
  needsHtmlTimeSchemas,
1281
1653
  needsRecordIdSchemas
@@ -1334,11 +1706,16 @@ async function resolveGenerationSnapshot({
1334
1706
  });
1335
1707
  }
1336
1708
 
1709
+ function resolveCrudGenerationTableName(options = {}) {
1710
+ return normalizeText(options?.["table-name"] || options?.namespace);
1711
+ }
1712
+
1337
1713
  function createCacheKey({ appRoot, options }) {
1338
1714
  const payload = {
1339
1715
  appRoot: path.resolve(String(appRoot || "")),
1340
1716
  options: {
1341
1717
  namespace: normalizeText(options?.namespace),
1718
+ surface: normalizeText(options?.surface),
1342
1719
  ownershipFilter: normalizeText(options?.["ownership-filter"]),
1343
1720
  tableName: normalizeText(options?.["table-name"]),
1344
1721
  idColumn: normalizeText(options?.["id-column"])
@@ -1356,10 +1733,14 @@ async function buildCrudTemplateContext(input = {}) {
1356
1733
  if (!namespace) {
1357
1734
  throw new Error('crud template context requires option "namespace".');
1358
1735
  }
1359
- const tableName = normalizeText(options["table-name"]);
1736
+ const tableName = resolveCrudGenerationTableName(options);
1360
1737
  if (!tableName) {
1361
1738
  throw new Error('crud template context requires option "table-name".');
1362
1739
  }
1740
+ const resolvedSurface = await resolveCrudGenerationSurfaceId({
1741
+ appRoot,
1742
+ options
1743
+ });
1363
1744
  const snapshot = await resolveGenerationSnapshot({
1364
1745
  appRoot,
1365
1746
  tableName,
@@ -1373,10 +1754,18 @@ async function buildCrudTemplateContext(input = {}) {
1373
1754
  enforceTableColumns: true
1374
1755
  }
1375
1756
  );
1757
+ const surfaceRequiresWorkspace = await resolveCrudSurfaceRequiresWorkspace({
1758
+ appRoot,
1759
+ options,
1760
+ surface: resolvedSurface
1761
+ });
1376
1762
 
1377
1763
  return buildReplacementsFromSnapshot({
1764
+ namespace,
1378
1765
  snapshot,
1379
- resolvedOwnershipFilter
1766
+ resolvedOwnershipFilter,
1767
+ surfaceRequiresWorkspace,
1768
+ surfaceId: resolvedSurface
1380
1769
  });
1381
1770
  }
1382
1771
 
@@ -1408,8 +1797,19 @@ const __testables = Object.freeze({
1408
1797
  renderResourceFieldSchema,
1409
1798
  renderInputNormalizer,
1410
1799
  renderOutputNormalizerExpression,
1800
+ resolveCrudGenerationTableName,
1411
1801
  resolveGenerationSnapshot,
1412
- buildFieldMetaEntries
1802
+ buildFieldMetaEntries,
1803
+ resolveDefaultCrudSurfaceIdFromAppConfig,
1804
+ resolveCrudGenerationSurfaceId,
1805
+ resolveCrudSurfaceRequiresWorkspace,
1806
+ buildCrudPermissionIds,
1807
+ renderRoleCatalogPermissionGrants,
1808
+ renderActionPermissionSupport,
1809
+ renderActionPermissionExpression,
1810
+ renderActionInputValidatorExpression,
1811
+ renderRouteParamsValidatorLine,
1812
+ renderRouteInputLines
1413
1813
  });
1414
1814
 
1415
1815
  export {
@@ -1421,5 +1821,6 @@ export {
1421
1821
  renderInputNormalizer,
1422
1822
  renderOutputNormalizerExpression,
1423
1823
  buildFieldMetaEntries,
1824
+ resolveCrudGenerationSurfaceId,
1424
1825
  __testables
1425
1826
  };
@@ -14,7 +14,7 @@ import { createActions } from "./actions.js";
14
14
  import { registerRoutes } from "./registerRoutes.js";
15
15
  const CRUD_MODULE_CONFIG = Object.freeze({
16
16
  namespace: "${option:namespace|snake}",
17
- surface: "${option:surface|lower}",
17
+ surface: __JSKIT_CRUD_SURFACE_ID__,
18
18
  ownershipFilter: "__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__",
19
19
  relativePath: "/${option:directory-prefix|pathprefix}${option:namespace|kebab}"
20
20
  });
@@ -82,7 +82,6 @@ class ${option:namespace|pascal}Provider {
82
82
  registerRoutes(app, {
83
83
  routeOwnershipFilter: crudPolicy.ownershipFilter,
84
84
  routeSurface: crudPolicy.surfaceId,
85
- routeSurfaceRequiresWorkspace: crudPolicy.surfaceDefinition.requiresWorkspace === true,
86
85
  routeRelativePath: crudPolicy.relativePath
87
86
  });
88
87
  }