@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.
- 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
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
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:
|
|
16
|
+
required: false,
|
|
17
17
|
inputType: "text",
|
|
18
18
|
validationType: "enabled-surface-id",
|
|
19
19
|
promptLabel: "Target surface",
|
|
20
|
-
promptHint: "
|
|
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:
|
|
39
|
+
required: false,
|
|
38
40
|
inputType: "text",
|
|
39
41
|
defaultValue: "",
|
|
42
|
+
defaultFromOptionTemplate: "${option:namespace}",
|
|
40
43
|
promptLabel: "Table name",
|
|
41
|
-
promptHint: "
|
|
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.
|
|
152
|
-
"@jskit-ai/crud-core": "0.1.
|
|
153
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
154
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
155
|
-
"@jskit-ai/kernel": "0.1.
|
|
156
|
-
"@jskit-ai/realtime": "0.1.
|
|
157
|
-
"@jskit-ai/users-core": "0.1.
|
|
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.
|
|
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.
|
|
17
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
18
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
19
|
-
"@jskit-ai/kernel": "0.1.
|
|
20
|
-
"@jskit-ai/users-core": "0.1.
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
}
|