@jskit-ai/crud-server-generator 0.1.40 → 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 +42 -17
- package/package.json +6 -6
- package/src/server/buildTemplateContext.js +583 -46
- package/src/shared/crud/crudResource.js +7 -5
- package/templates/migrations/crud_initial.cjs +1 -0
- 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/templates/src/local-package/server/repository.js +4 -0
- package/templates/src/local-package/shared/crudResource.js +2 -2
- package/test/addFieldSubcommand.test.js +12 -6
- package/test/buildTemplateContext.test.js +384 -39
- package/test/crudResource.test.js +1 -1
- package/test/crudServerGuards.test.js +21 -18
- package/test/packageDescriptor.test.js +52 -0
- package/test/routeInputContracts.test.js +63 -16
- package/test-support/templateServerFixture.js +134 -23
|
@@ -5,23 +5,54 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import {
|
|
6
6
|
normalizeText,
|
|
7
7
|
resolveDatabaseClientFromEnvironment,
|
|
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,15 +85,15 @@ 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;
|
|
61
92
|
}
|
|
62
93
|
|
|
63
94
|
function inferOwnershipFilterFromSnapshot(snapshot) {
|
|
64
|
-
const hasWorkspace = snapshot?.
|
|
65
|
-
const hasUser = snapshot?.
|
|
95
|
+
const hasWorkspace = snapshot?.hasWorkspaceIdColumn === true;
|
|
96
|
+
const hasUser = snapshot?.hasUserIdColumn === true;
|
|
66
97
|
if (hasWorkspace && hasUser) {
|
|
67
98
|
return "workspace_user";
|
|
68
99
|
}
|
|
@@ -76,24 +107,24 @@ function inferOwnershipFilterFromSnapshot(snapshot) {
|
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
function assertOwnershipColumnsForFilter(snapshot, filter) {
|
|
79
|
-
const hasWorkspace = snapshot?.
|
|
80
|
-
const hasUser = snapshot?.
|
|
110
|
+
const hasWorkspace = snapshot?.hasWorkspaceIdColumn === true;
|
|
111
|
+
const hasUser = snapshot?.hasUserIdColumn === true;
|
|
81
112
|
if (filter === "public") {
|
|
82
113
|
return;
|
|
83
114
|
}
|
|
84
115
|
if (filter === "workspace" && !hasWorkspace) {
|
|
85
116
|
throw new Error(
|
|
86
|
-
'Ownership filter "workspace" requires column "
|
|
117
|
+
'Ownership filter "workspace" requires column "workspace_id".'
|
|
87
118
|
);
|
|
88
119
|
}
|
|
89
120
|
if (filter === "user" && !hasUser) {
|
|
90
121
|
throw new Error(
|
|
91
|
-
'Ownership filter "user" requires column "
|
|
122
|
+
'Ownership filter "user" requires column "user_id".'
|
|
92
123
|
);
|
|
93
124
|
}
|
|
94
125
|
if (filter === "workspace_user" && (!hasWorkspace || !hasUser)) {
|
|
95
126
|
throw new Error(
|
|
96
|
-
'Ownership filter "workspace_user" requires both columns "
|
|
127
|
+
'Ownership filter "workspace_user" requires both columns "workspace_id" and "user_id".'
|
|
97
128
|
);
|
|
98
129
|
}
|
|
99
130
|
}
|
|
@@ -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;
|
|
@@ -221,7 +345,8 @@ async function resolveMysqlSnapshotFromDatabase({
|
|
|
221
345
|
);
|
|
222
346
|
}
|
|
223
347
|
|
|
224
|
-
const connection =
|
|
348
|
+
const connection = resolveKnexConnectionFromEnvironment(env, {
|
|
349
|
+
client: dbClient,
|
|
225
350
|
defaultPort: 3306,
|
|
226
351
|
context: "crud table introspection"
|
|
227
352
|
});
|
|
@@ -269,16 +394,24 @@ function resolveColumnKey(column, idColumn) {
|
|
|
269
394
|
function resolveScaffoldColumns(snapshot) {
|
|
270
395
|
const idColumn = String(snapshot.idColumn || DEFAULT_ID_COLUMN);
|
|
271
396
|
const sourceColumns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
397
|
+
const foreignKeyColumnNames = new Set(
|
|
398
|
+
(Array.isArray(snapshot.foreignKeys) ? snapshot.foreignKeys : [])
|
|
399
|
+
.flatMap((foreignKey) => Array.isArray(foreignKey?.columns) ? foreignKey.columns : [])
|
|
400
|
+
.map((entry) => String(entry?.name || "").trim())
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
);
|
|
272
403
|
const seenKeys = new Set();
|
|
273
404
|
|
|
274
405
|
const columns = sourceColumns.map((column) => {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const isOwnerColumn =
|
|
406
|
+
const isWorkspaceIdColumn = column.name === "workspace_id";
|
|
407
|
+
const isUserIdColumn = column.name === "user_id";
|
|
408
|
+
const isOwnerColumn = isWorkspaceIdColumn || isUserIdColumn;
|
|
278
409
|
const isIdColumn = column.name === idColumn;
|
|
410
|
+
const isForeignIdColumn = foreignKeyColumnNames.has(column.name) || /_id$/i.test(String(column.name || ""));
|
|
279
411
|
const isCreatedAtColumn = column.name === "created_at";
|
|
280
412
|
const isUpdatedAtColumn = column.name === "updated_at";
|
|
281
413
|
const key = resolveColumnKey(column, idColumn);
|
|
414
|
+
const isRecordIdColumn = isIdColumn || isOwnerColumn || isForeignIdColumn || /Id$/.test(String(key || ""));
|
|
282
415
|
if (!key) {
|
|
283
416
|
throw new Error(`Could not derive API field key for column "${column.name}".`);
|
|
284
417
|
}
|
|
@@ -297,6 +430,8 @@ function resolveScaffoldColumns(snapshot) {
|
|
|
297
430
|
key,
|
|
298
431
|
isOwnerColumn,
|
|
299
432
|
isIdColumn,
|
|
433
|
+
isForeignIdColumn,
|
|
434
|
+
isRecordIdColumn,
|
|
300
435
|
isCreatedAtColumn,
|
|
301
436
|
isUpdatedAtColumn,
|
|
302
437
|
writable: !isOwnerColumn && !isIdColumn && !isCreatedAtColumn && !isUpdatedAtColumn
|
|
@@ -327,9 +462,7 @@ function renderPropertyAccess(sourceName, key) {
|
|
|
327
462
|
|
|
328
463
|
function renderIntegerSchema(column) {
|
|
329
464
|
const options = [];
|
|
330
|
-
if (column.
|
|
331
|
-
options.push("minimum: 1");
|
|
332
|
-
} else if (column.unsigned === true) {
|
|
465
|
+
if (column.unsigned === true) {
|
|
333
466
|
options.push("minimum: 0");
|
|
334
467
|
}
|
|
335
468
|
if (options.length > 0) {
|
|
@@ -359,6 +492,11 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
359
492
|
if (typeKind === "string") {
|
|
360
493
|
schemaExpression = renderStringSchema(column, { forOutput });
|
|
361
494
|
} else if (typeKind === "integer") {
|
|
495
|
+
if (column?.isRecordIdColumn === true) {
|
|
496
|
+
return forOutput
|
|
497
|
+
? (column.nullable === true ? "nullableRecordIdSchema" : "recordIdSchema")
|
|
498
|
+
: (column.nullable === true ? "nullableRecordIdInputSchema" : "recordIdInputSchema");
|
|
499
|
+
}
|
|
362
500
|
schemaExpression = renderIntegerSchema(column);
|
|
363
501
|
} else if (typeKind === "number") {
|
|
364
502
|
schemaExpression = "Type.Number()";
|
|
@@ -382,11 +520,14 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
382
520
|
return schemaExpression;
|
|
383
521
|
}
|
|
384
522
|
|
|
385
|
-
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false } = {}) {
|
|
523
|
+
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, needsRecordIdSchemas = false } = {}) {
|
|
386
524
|
const imports = [
|
|
387
525
|
"normalizeObjectInput",
|
|
388
526
|
"createCursorListValidator"
|
|
389
527
|
];
|
|
528
|
+
if (needsRecordIdSchemas) {
|
|
529
|
+
imports.push("recordIdSchema", "recordIdInputSchema", "nullableRecordIdSchema", "nullableRecordIdInputSchema");
|
|
530
|
+
}
|
|
390
531
|
if (needsHtmlTimeSchemas) {
|
|
391
532
|
imports.push("HTML_TIME_STRING_SCHEMA", "NULLABLE_HTML_TIME_STRING_SCHEMA");
|
|
392
533
|
}
|
|
@@ -406,6 +547,12 @@ function renderInputNormalizer(column) {
|
|
|
406
547
|
return "normalizeText";
|
|
407
548
|
}
|
|
408
549
|
if (typeKind === "integer") {
|
|
550
|
+
if (column?.isRecordIdColumn === true) {
|
|
551
|
+
if (nullable) {
|
|
552
|
+
return "(value) => normalizeRecordId(value, { fallback: null })";
|
|
553
|
+
}
|
|
554
|
+
return "normalizeRecordId";
|
|
555
|
+
}
|
|
409
556
|
return "normalizeFiniteInteger";
|
|
410
557
|
}
|
|
411
558
|
if (typeKind === "number") {
|
|
@@ -434,10 +581,17 @@ function renderInputNormalizer(column) {
|
|
|
434
581
|
|
|
435
582
|
function renderOutputNormalizerExpression(column) {
|
|
436
583
|
const typeKind = String(column.typeKind || "");
|
|
584
|
+
const nullable = column?.nullable === true;
|
|
437
585
|
if (typeKind === "string" || typeKind === "time") {
|
|
438
586
|
return "normalizeText";
|
|
439
587
|
}
|
|
440
588
|
if (typeKind === "integer") {
|
|
589
|
+
if (column?.isRecordIdColumn === true) {
|
|
590
|
+
if (nullable) {
|
|
591
|
+
return "(value) => normalizeRecordId(value, { fallback: null })";
|
|
592
|
+
}
|
|
593
|
+
return "normalizeRecordId";
|
|
594
|
+
}
|
|
441
595
|
return "normalizeFiniteInteger";
|
|
442
596
|
}
|
|
443
597
|
if (typeKind === "number") {
|
|
@@ -522,6 +676,7 @@ function renderResourceNormalizeSupportImport({
|
|
|
522
676
|
needsNormalizeBoolean = false,
|
|
523
677
|
needsNormalizeFiniteNumber = false,
|
|
524
678
|
needsNormalizeFiniteInteger = false,
|
|
679
|
+
needsNormalizeRecordId = false,
|
|
525
680
|
needsNormalizeIfInSource = false,
|
|
526
681
|
needsNormalizeIfPresent = false,
|
|
527
682
|
needsNormalizeOrNull = false
|
|
@@ -539,6 +694,9 @@ function renderResourceNormalizeSupportImport({
|
|
|
539
694
|
if (needsNormalizeFiniteInteger) {
|
|
540
695
|
imports.push("normalizeFiniteInteger");
|
|
541
696
|
}
|
|
697
|
+
if (needsNormalizeRecordId) {
|
|
698
|
+
imports.push("normalizeRecordId");
|
|
699
|
+
}
|
|
542
700
|
if (needsNormalizeIfInSource) {
|
|
543
701
|
imports.push("normalizeIfInSource");
|
|
544
702
|
}
|
|
@@ -596,15 +754,57 @@ function renderMigrationDefaultClause(column) {
|
|
|
596
754
|
}
|
|
597
755
|
}
|
|
598
756
|
|
|
757
|
+
if (column.typeKind === "string" && normalized.startsWith("'") && normalized.endsWith("'")) {
|
|
758
|
+
const unquoted = normalized
|
|
759
|
+
.slice(1, -1)
|
|
760
|
+
.replace(/\\'/g, "'")
|
|
761
|
+
.replace(/''/g, "'");
|
|
762
|
+
return `.defaultTo(${JSON.stringify(unquoted)})`;
|
|
763
|
+
}
|
|
764
|
+
|
|
599
765
|
return `.defaultTo(${JSON.stringify(rawDefault)})`;
|
|
600
766
|
}
|
|
601
767
|
|
|
602
|
-
function
|
|
768
|
+
function renderMigrationSpecificStringType(column, { tableCollation = "" } = {}) {
|
|
769
|
+
const baseType = normalizeText(column?.columnType);
|
|
770
|
+
if (!baseType) {
|
|
771
|
+
return "";
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const characterSetName = normalizeText(column?.characterSetName);
|
|
775
|
+
const collationName = normalizeText(column?.collationName);
|
|
776
|
+
const normalizedTableCollation = normalizeText(tableCollation);
|
|
777
|
+
if (!collationName || collationName === normalizedTableCollation) {
|
|
778
|
+
return "";
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const parts = [baseType];
|
|
782
|
+
if (characterSetName) {
|
|
783
|
+
parts.push(`CHARACTER SET ${characterSetName}`);
|
|
784
|
+
}
|
|
785
|
+
parts.push(`COLLATE ${collationName}`);
|
|
786
|
+
return parts.join(" ");
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function renderTemporalColumnBuilder(column, methodName) {
|
|
790
|
+
if (Number.isFinite(column?.datetimePrecision) && column.datetimePrecision > 0) {
|
|
791
|
+
return `table.${methodName}(${JSON.stringify(column.name)}, { precision: ${column.datetimePrecision} })`;
|
|
792
|
+
}
|
|
793
|
+
return `table.${methodName}(${JSON.stringify(column.name)})`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderMigrationColumnLine(column, {
|
|
797
|
+
idColumn = DEFAULT_ID_COLUMN,
|
|
798
|
+
primaryKeyColumns = [],
|
|
799
|
+
foreignKeyColumnNames = new Set(),
|
|
800
|
+
tableCollation = ""
|
|
801
|
+
} = {}) {
|
|
603
802
|
const isPrimary = Array.isArray(primaryKeyColumns) && primaryKeyColumns.includes(column.name);
|
|
604
803
|
const isIdColumn = column.name === idColumn;
|
|
804
|
+
const isRecordIdColumn = isIdColumn || column.name === "workspace_id" || column.name === "user_id" || foreignKeyColumnNames.has(column.name) || /_id$/i.test(String(column.name || ""));
|
|
605
805
|
|
|
606
806
|
if (isIdColumn && column.autoIncrement) {
|
|
607
|
-
let line = `table.
|
|
807
|
+
let line = `table.bigIncrements(${JSON.stringify(column.name)})`;
|
|
608
808
|
if (column.unsigned) {
|
|
609
809
|
line += ".unsigned()";
|
|
610
810
|
}
|
|
@@ -615,25 +815,40 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
615
815
|
let line = "";
|
|
616
816
|
const nameLiteral = JSON.stringify(column.name);
|
|
617
817
|
const dataType = String(column.dataType || "").toLowerCase();
|
|
818
|
+
const specificStringType = renderMigrationSpecificStringType(column, {
|
|
819
|
+
tableCollation
|
|
820
|
+
});
|
|
618
821
|
|
|
619
822
|
if (dataType === "varchar") {
|
|
620
|
-
|
|
621
|
-
|
|
823
|
+
if (specificStringType) {
|
|
824
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
825
|
+
} else {
|
|
826
|
+
const maxLength = Number.isFinite(column.maxLength) ? column.maxLength : 255;
|
|
827
|
+
line = `table.string(${nameLiteral}, ${maxLength})`;
|
|
828
|
+
}
|
|
622
829
|
} else if (dataType === "char") {
|
|
623
|
-
line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "char(255)")})`;
|
|
830
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType || column.columnType || "char(255)")})`;
|
|
624
831
|
} else if (dataType === "text") {
|
|
625
|
-
|
|
832
|
+
if (specificStringType) {
|
|
833
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
834
|
+
} else {
|
|
835
|
+
line = `table.text(${nameLiteral})`;
|
|
836
|
+
}
|
|
626
837
|
} else if (dataType === "tinytext" || dataType === "mediumtext" || dataType === "longtext") {
|
|
627
|
-
|
|
838
|
+
if (specificStringType) {
|
|
839
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
840
|
+
} else {
|
|
841
|
+
line = `table.text(${nameLiteral}, ${JSON.stringify(dataType)})`;
|
|
842
|
+
}
|
|
628
843
|
} else if (dataType === "enum") {
|
|
629
844
|
const enumValues = Array.isArray(column.enumValues) ? column.enumValues : [];
|
|
630
845
|
line = `table.enu(${nameLiteral}, ${JSON.stringify(enumValues)})`;
|
|
631
846
|
} else if (dataType === "set") {
|
|
632
|
-
line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "set")})`;
|
|
847
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType || column.columnType || "set")})`;
|
|
633
848
|
} else if (column.typeKind === "boolean") {
|
|
634
849
|
line = `table.boolean(${nameLiteral})`;
|
|
635
850
|
} else if (dataType === "int" || dataType === "integer") {
|
|
636
|
-
line = `table.integer(${nameLiteral})`;
|
|
851
|
+
line = isRecordIdColumn ? `table.bigInteger(${nameLiteral})` : `table.integer(${nameLiteral})`;
|
|
637
852
|
} else if (dataType === "smallint") {
|
|
638
853
|
line = `table.smallint(${nameLiteral})`;
|
|
639
854
|
} else if (dataType === "bigint") {
|
|
@@ -657,11 +872,11 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
657
872
|
} else if (dataType === "date") {
|
|
658
873
|
line = `table.date(${nameLiteral})`;
|
|
659
874
|
} else if (dataType === "time") {
|
|
660
|
-
line =
|
|
875
|
+
line = renderTemporalColumnBuilder(column, "time");
|
|
661
876
|
} else if (dataType === "datetime") {
|
|
662
|
-
line =
|
|
877
|
+
line = renderTemporalColumnBuilder(column, "dateTime");
|
|
663
878
|
} else if (dataType === "timestamp") {
|
|
664
|
-
line =
|
|
879
|
+
line = renderTemporalColumnBuilder(column, "timestamp");
|
|
665
880
|
} else {
|
|
666
881
|
throw new Error(
|
|
667
882
|
`Unsupported MySQL type "${dataType}" in migration renderer for column "${column.name}".`
|
|
@@ -682,10 +897,18 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
682
897
|
|
|
683
898
|
function renderMigrationColumnLines(snapshot) {
|
|
684
899
|
const columns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
900
|
+
const foreignKeyColumnNames = new Set(
|
|
901
|
+
(Array.isArray(snapshot.foreignKeys) ? snapshot.foreignKeys : [])
|
|
902
|
+
.flatMap((foreignKey) => Array.isArray(foreignKey?.columns) ? foreignKey.columns : [])
|
|
903
|
+
.map((entry) => String(entry?.name || "").trim())
|
|
904
|
+
.filter(Boolean)
|
|
905
|
+
);
|
|
685
906
|
const lines = columns.map((column) =>
|
|
686
907
|
` ${renderMigrationColumnLine(column, {
|
|
687
908
|
idColumn: snapshot.idColumn,
|
|
688
|
-
primaryKeyColumns: snapshot.primaryKeyColumns
|
|
909
|
+
primaryKeyColumns: snapshot.primaryKeyColumns,
|
|
910
|
+
foreignKeyColumnNames,
|
|
911
|
+
tableCollation: snapshot.tableCollation
|
|
689
912
|
})}`
|
|
690
913
|
);
|
|
691
914
|
return lines.join("\n");
|
|
@@ -699,13 +922,23 @@ function renderMigrationIndexLine(index) {
|
|
|
699
922
|
|
|
700
923
|
const columnsLiteral = JSON.stringify(columns);
|
|
701
924
|
const indexName = normalizeText(index?.name);
|
|
925
|
+
const normalizedIndexType = normalizeText(index?.indexType).toUpperCase();
|
|
926
|
+
const storageEngineIndexType = normalizedIndexType && normalizedIndexType !== "BTREE"
|
|
927
|
+
? normalizedIndexType.toLowerCase()
|
|
928
|
+
: "";
|
|
702
929
|
if (index?.unique === true) {
|
|
930
|
+
if (indexName && storageEngineIndexType) {
|
|
931
|
+
return ` table.unique(${columnsLiteral}, { indexName: ${JSON.stringify(indexName)}, storageEngineIndexType: ${JSON.stringify(storageEngineIndexType)} });`;
|
|
932
|
+
}
|
|
703
933
|
if (indexName) {
|
|
704
934
|
return ` table.unique(${columnsLiteral}, ${JSON.stringify(indexName)});`;
|
|
705
935
|
}
|
|
706
936
|
return ` table.unique(${columnsLiteral});`;
|
|
707
937
|
}
|
|
708
938
|
|
|
939
|
+
if (indexName && normalizedIndexType && normalizedIndexType !== "BTREE") {
|
|
940
|
+
return ` table.index(${columnsLiteral}, ${JSON.stringify(indexName)}, ${JSON.stringify(normalizedIndexType)});`;
|
|
941
|
+
}
|
|
709
942
|
if (indexName) {
|
|
710
943
|
return ` table.index(${columnsLiteral}, ${JSON.stringify(indexName)});`;
|
|
711
944
|
}
|
|
@@ -764,6 +997,28 @@ function renderMigrationForeignKeyLines(snapshot) {
|
|
|
764
997
|
return lines.join("\n");
|
|
765
998
|
}
|
|
766
999
|
|
|
1000
|
+
function renderMigrationCheckConstraintLines(snapshot) {
|
|
1001
|
+
const tableName = normalizeText(snapshot?.tableName);
|
|
1002
|
+
const checkConstraints = Array.isArray(snapshot?.checkConstraints) ? snapshot.checkConstraints : [];
|
|
1003
|
+
if (!tableName || checkConstraints.length < 1) {
|
|
1004
|
+
return "";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return checkConstraints
|
|
1008
|
+
.map((constraint) => {
|
|
1009
|
+
const name = normalizeText(constraint?.name);
|
|
1010
|
+
const clause = normalizeText(constraint?.clause);
|
|
1011
|
+
if (!name || !clause) {
|
|
1012
|
+
return "";
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const sql = `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${name}\` CHECK (${clause})`;
|
|
1016
|
+
return ` await knex.raw(${JSON.stringify(sql)});`;
|
|
1017
|
+
})
|
|
1018
|
+
.filter(Boolean)
|
|
1019
|
+
.join("\n");
|
|
1020
|
+
}
|
|
1021
|
+
|
|
767
1022
|
function mergeFieldMetaEntries(...entryGroups) {
|
|
768
1023
|
const mergedByKey = new Map();
|
|
769
1024
|
for (const sourceEntries of entryGroups) {
|
|
@@ -1100,10 +1355,184 @@ function renderRepositoryListConfigLines(snapshot = {}) {
|
|
|
1100
1355
|
].join("\n");
|
|
1101
1356
|
}
|
|
1102
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
|
+
|
|
1103
1528
|
function buildReplacementsFromSnapshot({
|
|
1529
|
+
namespace = "",
|
|
1104
1530
|
snapshot,
|
|
1105
|
-
resolvedOwnershipFilter
|
|
1531
|
+
resolvedOwnershipFilter,
|
|
1532
|
+
surfaceRequiresWorkspace = true,
|
|
1533
|
+
surfaceId = ""
|
|
1106
1534
|
}) {
|
|
1535
|
+
const requiresNamedPermissions = surfaceRequiresWorkspace === true;
|
|
1107
1536
|
const scaffoldColumns = resolveScaffoldColumns(snapshot);
|
|
1108
1537
|
const outputColumns = scaffoldColumns.filter((column) => !column.isOwnerColumn);
|
|
1109
1538
|
const writableColumns = scaffoldColumns.filter((column) => column.writable);
|
|
@@ -1116,7 +1545,8 @@ function buildReplacementsFromSnapshot({
|
|
|
1116
1545
|
writableColumns,
|
|
1117
1546
|
snapshot
|
|
1118
1547
|
});
|
|
1119
|
-
const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer");
|
|
1548
|
+
const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn !== true);
|
|
1549
|
+
const needsRecordIdSchemas = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn === true);
|
|
1120
1550
|
const needsFiniteNumber = resourceColumns.some((column) => column.typeKind === "number");
|
|
1121
1551
|
const needsDateTimeOutput = outputColumns.some((column) => column.typeKind === "datetime");
|
|
1122
1552
|
const needsDateTimeInput = writableColumns.some((column) => column.typeKind === "datetime");
|
|
@@ -1143,9 +1573,84 @@ function buildReplacementsFromSnapshot({
|
|
|
1143
1573
|
const replacements = Object.freeze({
|
|
1144
1574
|
__JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
|
|
1145
1575
|
__JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
|
|
1576
|
+
__JSKIT_CRUD_SURFACE_ID__: JSON.stringify(normalizeText(surfaceId).toLowerCase()),
|
|
1146
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
|
+
}),
|
|
1147
1651
|
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
|
|
1148
|
-
needsHtmlTimeSchemas
|
|
1652
|
+
needsHtmlTimeSchemas,
|
|
1653
|
+
needsRecordIdSchemas
|
|
1149
1654
|
}),
|
|
1150
1655
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
|
|
1151
1656
|
needsToIsoString: needsDateTimeOutput || needsDate,
|
|
@@ -1156,6 +1661,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1156
1661
|
needsNormalizeBoolean,
|
|
1157
1662
|
needsNormalizeFiniteNumber: needsFiniteNumber,
|
|
1158
1663
|
needsNormalizeFiniteInteger: needsFiniteInteger,
|
|
1664
|
+
needsNormalizeRecordId: needsRecordIdSchemas,
|
|
1159
1665
|
needsNormalizeIfInSource,
|
|
1160
1666
|
needsNormalizeIfPresent,
|
|
1161
1667
|
needsNormalizeOrNull
|
|
@@ -1176,7 +1682,8 @@ function buildReplacementsFromSnapshot({
|
|
|
1176
1682
|
__JSKIT_CRUD_LIST_CONFIG_LINES__: renderRepositoryListConfigLines(snapshot),
|
|
1177
1683
|
__JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
|
|
1178
1684
|
__JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
|
|
1179
|
-
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot)
|
|
1685
|
+
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot),
|
|
1686
|
+
__JSKIT_CRUD_MIGRATION_CHECK_CONSTRAINT_LINES__: renderMigrationCheckConstraintLines(snapshot)
|
|
1180
1687
|
});
|
|
1181
1688
|
|
|
1182
1689
|
return replacements;
|
|
@@ -1199,11 +1706,16 @@ async function resolveGenerationSnapshot({
|
|
|
1199
1706
|
});
|
|
1200
1707
|
}
|
|
1201
1708
|
|
|
1709
|
+
function resolveCrudGenerationTableName(options = {}) {
|
|
1710
|
+
return normalizeText(options?.["table-name"] || options?.namespace);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1202
1713
|
function createCacheKey({ appRoot, options }) {
|
|
1203
1714
|
const payload = {
|
|
1204
1715
|
appRoot: path.resolve(String(appRoot || "")),
|
|
1205
1716
|
options: {
|
|
1206
1717
|
namespace: normalizeText(options?.namespace),
|
|
1718
|
+
surface: normalizeText(options?.surface),
|
|
1207
1719
|
ownershipFilter: normalizeText(options?.["ownership-filter"]),
|
|
1208
1720
|
tableName: normalizeText(options?.["table-name"]),
|
|
1209
1721
|
idColumn: normalizeText(options?.["id-column"])
|
|
@@ -1221,10 +1733,14 @@ async function buildCrudTemplateContext(input = {}) {
|
|
|
1221
1733
|
if (!namespace) {
|
|
1222
1734
|
throw new Error('crud template context requires option "namespace".');
|
|
1223
1735
|
}
|
|
1224
|
-
const tableName =
|
|
1736
|
+
const tableName = resolveCrudGenerationTableName(options);
|
|
1225
1737
|
if (!tableName) {
|
|
1226
1738
|
throw new Error('crud template context requires option "table-name".');
|
|
1227
1739
|
}
|
|
1740
|
+
const resolvedSurface = await resolveCrudGenerationSurfaceId({
|
|
1741
|
+
appRoot,
|
|
1742
|
+
options
|
|
1743
|
+
});
|
|
1228
1744
|
const snapshot = await resolveGenerationSnapshot({
|
|
1229
1745
|
appRoot,
|
|
1230
1746
|
tableName,
|
|
@@ -1238,10 +1754,18 @@ async function buildCrudTemplateContext(input = {}) {
|
|
|
1238
1754
|
enforceTableColumns: true
|
|
1239
1755
|
}
|
|
1240
1756
|
);
|
|
1757
|
+
const surfaceRequiresWorkspace = await resolveCrudSurfaceRequiresWorkspace({
|
|
1758
|
+
appRoot,
|
|
1759
|
+
options,
|
|
1760
|
+
surface: resolvedSurface
|
|
1761
|
+
});
|
|
1241
1762
|
|
|
1242
1763
|
return buildReplacementsFromSnapshot({
|
|
1764
|
+
namespace,
|
|
1243
1765
|
snapshot,
|
|
1244
|
-
resolvedOwnershipFilter
|
|
1766
|
+
resolvedOwnershipFilter,
|
|
1767
|
+
surfaceRequiresWorkspace,
|
|
1768
|
+
surfaceId: resolvedSurface
|
|
1245
1769
|
});
|
|
1246
1770
|
}
|
|
1247
1771
|
|
|
@@ -1266,14 +1790,26 @@ const __testables = Object.freeze({
|
|
|
1266
1790
|
buildReplacementsFromSnapshot,
|
|
1267
1791
|
parseDotEnvLine,
|
|
1268
1792
|
renderMigrationColumnLine,
|
|
1793
|
+
renderMigrationCheckConstraintLines,
|
|
1269
1794
|
renderMigrationForeignKeyLine,
|
|
1270
1795
|
resolveScaffoldColumns,
|
|
1271
1796
|
renderPropertyAccess,
|
|
1272
1797
|
renderResourceFieldSchema,
|
|
1273
1798
|
renderInputNormalizer,
|
|
1274
1799
|
renderOutputNormalizerExpression,
|
|
1800
|
+
resolveCrudGenerationTableName,
|
|
1275
1801
|
resolveGenerationSnapshot,
|
|
1276
|
-
buildFieldMetaEntries
|
|
1802
|
+
buildFieldMetaEntries,
|
|
1803
|
+
resolveDefaultCrudSurfaceIdFromAppConfig,
|
|
1804
|
+
resolveCrudGenerationSurfaceId,
|
|
1805
|
+
resolveCrudSurfaceRequiresWorkspace,
|
|
1806
|
+
buildCrudPermissionIds,
|
|
1807
|
+
renderRoleCatalogPermissionGrants,
|
|
1808
|
+
renderActionPermissionSupport,
|
|
1809
|
+
renderActionPermissionExpression,
|
|
1810
|
+
renderActionInputValidatorExpression,
|
|
1811
|
+
renderRouteParamsValidatorLine,
|
|
1812
|
+
renderRouteInputLines
|
|
1277
1813
|
});
|
|
1278
1814
|
|
|
1279
1815
|
export {
|
|
@@ -1285,5 +1821,6 @@ export {
|
|
|
1285
1821
|
renderInputNormalizer,
|
|
1286
1822
|
renderOutputNormalizerExpression,
|
|
1287
1823
|
buildFieldMetaEntries,
|
|
1824
|
+
resolveCrudGenerationSurfaceId,
|
|
1288
1825
|
__testables
|
|
1289
1826
|
};
|