@jskit-ai/crud-server-generator 0.1.39 → 0.1.41
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 +35 -10
- package/package.json +6 -6
- package/src/server/buildTemplateContext.js +168 -32
- package/src/shared/crud/crudResource.js +7 -5
- package/templates/migrations/crud_initial.cjs +1 -0
- 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 +182 -28
- package/test/crudResource.test.js +1 -1
- package/test/packageDescriptor.test.js +26 -0
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.41",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "CRUD server generator with routes, actions, and persistence scaffolding.",
|
|
7
7
|
options: {
|
|
@@ -15,6 +15,7 @@ export default Object.freeze({
|
|
|
15
15
|
surface: {
|
|
16
16
|
required: true,
|
|
17
17
|
inputType: "text",
|
|
18
|
+
validationType: "enabled-surface-id",
|
|
18
19
|
promptLabel: "Target surface",
|
|
19
20
|
promptHint: "Must match an enabled surface id."
|
|
20
21
|
},
|
|
@@ -45,6 +46,13 @@ export default Object.freeze({
|
|
|
45
46
|
defaultValue: "id",
|
|
46
47
|
promptLabel: "Id column",
|
|
47
48
|
promptHint: "Primary key column used by CRUD endpoints (default: id)."
|
|
49
|
+
},
|
|
50
|
+
force: {
|
|
51
|
+
required: false,
|
|
52
|
+
inputType: "flag",
|
|
53
|
+
defaultValue: "",
|
|
54
|
+
promptLabel: "Force overwrite",
|
|
55
|
+
promptHint: "Overwrite generated scaffold files if the namespace package directory already exists."
|
|
48
56
|
}
|
|
49
57
|
},
|
|
50
58
|
optionPolicies: {
|
|
@@ -90,8 +98,14 @@ export default Object.freeze({
|
|
|
90
98
|
"ownership-filter",
|
|
91
99
|
"table-name",
|
|
92
100
|
"id-column",
|
|
93
|
-
"directory-prefix"
|
|
94
|
-
|
|
101
|
+
"directory-prefix",
|
|
102
|
+
"force"
|
|
103
|
+
],
|
|
104
|
+
createTarget: {
|
|
105
|
+
pathTemplate: "packages/${option:namespace|kebab}",
|
|
106
|
+
label: "package directory",
|
|
107
|
+
allowExistingEmptyDirectory: false
|
|
108
|
+
}
|
|
95
109
|
},
|
|
96
110
|
"scaffold-field": {
|
|
97
111
|
entrypoint: "src/server/subcommands/addField.js",
|
|
@@ -134,13 +148,13 @@ export default Object.freeze({
|
|
|
134
148
|
mutations: {
|
|
135
149
|
dependencies: {
|
|
136
150
|
runtime: {
|
|
137
|
-
"@jskit-ai/auth-core": "0.1.
|
|
138
|
-
"@jskit-ai/crud-core": "0.1.
|
|
139
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
140
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
141
|
-
"@jskit-ai/kernel": "0.1.
|
|
142
|
-
"@jskit-ai/realtime": "0.1.
|
|
143
|
-
"@jskit-ai/users-core": "0.1.
|
|
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",
|
|
144
158
|
"@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
|
|
145
159
|
"typebox": "^1.0.81"
|
|
146
160
|
},
|
|
@@ -203,6 +217,17 @@ export default Object.freeze({
|
|
|
203
217
|
category: "crud",
|
|
204
218
|
id: "crud-local-package-server-action-ids-${option:namespace|snake}"
|
|
205
219
|
},
|
|
220
|
+
{
|
|
221
|
+
from: "templates/src/local-package/server/listConfig.js",
|
|
222
|
+
to: "packages/${option:namespace|kebab}/src/server/listConfig.js",
|
|
223
|
+
reason: "Install app-local CRUD list configuration.",
|
|
224
|
+
category: "crud",
|
|
225
|
+
id: "crud-local-package-server-list-config-${option:namespace|snake}",
|
|
226
|
+
templateContext: {
|
|
227
|
+
entrypoint: "src/server/buildTemplateContext.js",
|
|
228
|
+
export: "buildTemplateContext"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
206
231
|
{
|
|
207
232
|
from: "templates/src/local-package/server/registerRoutes.js",
|
|
208
233
|
to: "packages/${option:namespace|kebab}/src/server/registerRoutes.js",
|
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.41",
|
|
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.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",
|
|
21
21
|
"recast": "^0.23.11",
|
|
22
22
|
"typebox": "^1.0.81"
|
|
23
23
|
}
|
|
@@ -5,7 +5,7 @@ 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
11
|
import { checkCrudLookupFormControl } from "@jskit-ai/crud-core/shared/crudFieldMetaSupport";
|
|
@@ -61,8 +61,8 @@ function normalizeRequestedOwnershipFilter(value, { strict = false } = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function inferOwnershipFilterFromSnapshot(snapshot) {
|
|
64
|
-
const hasWorkspace = snapshot?.
|
|
65
|
-
const hasUser = snapshot?.
|
|
64
|
+
const hasWorkspace = snapshot?.hasWorkspaceIdColumn === true;
|
|
65
|
+
const hasUser = snapshot?.hasUserIdColumn === true;
|
|
66
66
|
if (hasWorkspace && hasUser) {
|
|
67
67
|
return "workspace_user";
|
|
68
68
|
}
|
|
@@ -76,24 +76,24 @@ function inferOwnershipFilterFromSnapshot(snapshot) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function assertOwnershipColumnsForFilter(snapshot, filter) {
|
|
79
|
-
const hasWorkspace = snapshot?.
|
|
80
|
-
const hasUser = snapshot?.
|
|
79
|
+
const hasWorkspace = snapshot?.hasWorkspaceIdColumn === true;
|
|
80
|
+
const hasUser = snapshot?.hasUserIdColumn === true;
|
|
81
81
|
if (filter === "public") {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
if (filter === "workspace" && !hasWorkspace) {
|
|
85
85
|
throw new Error(
|
|
86
|
-
'Ownership filter "workspace" requires column "
|
|
86
|
+
'Ownership filter "workspace" requires column "workspace_id".'
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
if (filter === "user" && !hasUser) {
|
|
90
90
|
throw new Error(
|
|
91
|
-
'Ownership filter "user" requires column "
|
|
91
|
+
'Ownership filter "user" requires column "user_id".'
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
if (filter === "workspace_user" && (!hasWorkspace || !hasUser)) {
|
|
95
95
|
throw new Error(
|
|
96
|
-
'Ownership filter "workspace_user" requires both columns "
|
|
96
|
+
'Ownership filter "workspace_user" requires both columns "workspace_id" and "user_id".'
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -221,7 +221,8 @@ async function resolveMysqlSnapshotFromDatabase({
|
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
const connection =
|
|
224
|
+
const connection = resolveKnexConnectionFromEnvironment(env, {
|
|
225
|
+
client: dbClient,
|
|
225
226
|
defaultPort: 3306,
|
|
226
227
|
context: "crud table introspection"
|
|
227
228
|
});
|
|
@@ -269,16 +270,24 @@ function resolveColumnKey(column, idColumn) {
|
|
|
269
270
|
function resolveScaffoldColumns(snapshot) {
|
|
270
271
|
const idColumn = String(snapshot.idColumn || DEFAULT_ID_COLUMN);
|
|
271
272
|
const sourceColumns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
273
|
+
const foreignKeyColumnNames = new Set(
|
|
274
|
+
(Array.isArray(snapshot.foreignKeys) ? snapshot.foreignKeys : [])
|
|
275
|
+
.flatMap((foreignKey) => Array.isArray(foreignKey?.columns) ? foreignKey.columns : [])
|
|
276
|
+
.map((entry) => String(entry?.name || "").trim())
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
);
|
|
272
279
|
const seenKeys = new Set();
|
|
273
280
|
|
|
274
281
|
const columns = sourceColumns.map((column) => {
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
const isOwnerColumn =
|
|
282
|
+
const isWorkspaceIdColumn = column.name === "workspace_id";
|
|
283
|
+
const isUserIdColumn = column.name === "user_id";
|
|
284
|
+
const isOwnerColumn = isWorkspaceIdColumn || isUserIdColumn;
|
|
278
285
|
const isIdColumn = column.name === idColumn;
|
|
286
|
+
const isForeignIdColumn = foreignKeyColumnNames.has(column.name) || /_id$/i.test(String(column.name || ""));
|
|
279
287
|
const isCreatedAtColumn = column.name === "created_at";
|
|
280
288
|
const isUpdatedAtColumn = column.name === "updated_at";
|
|
281
289
|
const key = resolveColumnKey(column, idColumn);
|
|
290
|
+
const isRecordIdColumn = isIdColumn || isOwnerColumn || isForeignIdColumn || /Id$/.test(String(key || ""));
|
|
282
291
|
if (!key) {
|
|
283
292
|
throw new Error(`Could not derive API field key for column "${column.name}".`);
|
|
284
293
|
}
|
|
@@ -297,6 +306,8 @@ function resolveScaffoldColumns(snapshot) {
|
|
|
297
306
|
key,
|
|
298
307
|
isOwnerColumn,
|
|
299
308
|
isIdColumn,
|
|
309
|
+
isForeignIdColumn,
|
|
310
|
+
isRecordIdColumn,
|
|
300
311
|
isCreatedAtColumn,
|
|
301
312
|
isUpdatedAtColumn,
|
|
302
313
|
writable: !isOwnerColumn && !isIdColumn && !isCreatedAtColumn && !isUpdatedAtColumn
|
|
@@ -327,9 +338,7 @@ function renderPropertyAccess(sourceName, key) {
|
|
|
327
338
|
|
|
328
339
|
function renderIntegerSchema(column) {
|
|
329
340
|
const options = [];
|
|
330
|
-
if (column.
|
|
331
|
-
options.push("minimum: 1");
|
|
332
|
-
} else if (column.unsigned === true) {
|
|
341
|
+
if (column.unsigned === true) {
|
|
333
342
|
options.push("minimum: 0");
|
|
334
343
|
}
|
|
335
344
|
if (options.length > 0) {
|
|
@@ -359,6 +368,11 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
359
368
|
if (typeKind === "string") {
|
|
360
369
|
schemaExpression = renderStringSchema(column, { forOutput });
|
|
361
370
|
} else if (typeKind === "integer") {
|
|
371
|
+
if (column?.isRecordIdColumn === true) {
|
|
372
|
+
return forOutput
|
|
373
|
+
? (column.nullable === true ? "nullableRecordIdSchema" : "recordIdSchema")
|
|
374
|
+
: (column.nullable === true ? "nullableRecordIdInputSchema" : "recordIdInputSchema");
|
|
375
|
+
}
|
|
362
376
|
schemaExpression = renderIntegerSchema(column);
|
|
363
377
|
} else if (typeKind === "number") {
|
|
364
378
|
schemaExpression = "Type.Number()";
|
|
@@ -382,11 +396,14 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
382
396
|
return schemaExpression;
|
|
383
397
|
}
|
|
384
398
|
|
|
385
|
-
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false } = {}) {
|
|
399
|
+
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, needsRecordIdSchemas = false } = {}) {
|
|
386
400
|
const imports = [
|
|
387
401
|
"normalizeObjectInput",
|
|
388
402
|
"createCursorListValidator"
|
|
389
403
|
];
|
|
404
|
+
if (needsRecordIdSchemas) {
|
|
405
|
+
imports.push("recordIdSchema", "recordIdInputSchema", "nullableRecordIdSchema", "nullableRecordIdInputSchema");
|
|
406
|
+
}
|
|
390
407
|
if (needsHtmlTimeSchemas) {
|
|
391
408
|
imports.push("HTML_TIME_STRING_SCHEMA", "NULLABLE_HTML_TIME_STRING_SCHEMA");
|
|
392
409
|
}
|
|
@@ -406,6 +423,12 @@ function renderInputNormalizer(column) {
|
|
|
406
423
|
return "normalizeText";
|
|
407
424
|
}
|
|
408
425
|
if (typeKind === "integer") {
|
|
426
|
+
if (column?.isRecordIdColumn === true) {
|
|
427
|
+
if (nullable) {
|
|
428
|
+
return "(value) => normalizeRecordId(value, { fallback: null })";
|
|
429
|
+
}
|
|
430
|
+
return "normalizeRecordId";
|
|
431
|
+
}
|
|
409
432
|
return "normalizeFiniteInteger";
|
|
410
433
|
}
|
|
411
434
|
if (typeKind === "number") {
|
|
@@ -434,10 +457,17 @@ function renderInputNormalizer(column) {
|
|
|
434
457
|
|
|
435
458
|
function renderOutputNormalizerExpression(column) {
|
|
436
459
|
const typeKind = String(column.typeKind || "");
|
|
460
|
+
const nullable = column?.nullable === true;
|
|
437
461
|
if (typeKind === "string" || typeKind === "time") {
|
|
438
462
|
return "normalizeText";
|
|
439
463
|
}
|
|
440
464
|
if (typeKind === "integer") {
|
|
465
|
+
if (column?.isRecordIdColumn === true) {
|
|
466
|
+
if (nullable) {
|
|
467
|
+
return "(value) => normalizeRecordId(value, { fallback: null })";
|
|
468
|
+
}
|
|
469
|
+
return "normalizeRecordId";
|
|
470
|
+
}
|
|
441
471
|
return "normalizeFiniteInteger";
|
|
442
472
|
}
|
|
443
473
|
if (typeKind === "number") {
|
|
@@ -522,6 +552,7 @@ function renderResourceNormalizeSupportImport({
|
|
|
522
552
|
needsNormalizeBoolean = false,
|
|
523
553
|
needsNormalizeFiniteNumber = false,
|
|
524
554
|
needsNormalizeFiniteInteger = false,
|
|
555
|
+
needsNormalizeRecordId = false,
|
|
525
556
|
needsNormalizeIfInSource = false,
|
|
526
557
|
needsNormalizeIfPresent = false,
|
|
527
558
|
needsNormalizeOrNull = false
|
|
@@ -539,6 +570,9 @@ function renderResourceNormalizeSupportImport({
|
|
|
539
570
|
if (needsNormalizeFiniteInteger) {
|
|
540
571
|
imports.push("normalizeFiniteInteger");
|
|
541
572
|
}
|
|
573
|
+
if (needsNormalizeRecordId) {
|
|
574
|
+
imports.push("normalizeRecordId");
|
|
575
|
+
}
|
|
542
576
|
if (needsNormalizeIfInSource) {
|
|
543
577
|
imports.push("normalizeIfInSource");
|
|
544
578
|
}
|
|
@@ -596,15 +630,57 @@ function renderMigrationDefaultClause(column) {
|
|
|
596
630
|
}
|
|
597
631
|
}
|
|
598
632
|
|
|
633
|
+
if (column.typeKind === "string" && normalized.startsWith("'") && normalized.endsWith("'")) {
|
|
634
|
+
const unquoted = normalized
|
|
635
|
+
.slice(1, -1)
|
|
636
|
+
.replace(/\\'/g, "'")
|
|
637
|
+
.replace(/''/g, "'");
|
|
638
|
+
return `.defaultTo(${JSON.stringify(unquoted)})`;
|
|
639
|
+
}
|
|
640
|
+
|
|
599
641
|
return `.defaultTo(${JSON.stringify(rawDefault)})`;
|
|
600
642
|
}
|
|
601
643
|
|
|
602
|
-
function
|
|
644
|
+
function renderMigrationSpecificStringType(column, { tableCollation = "" } = {}) {
|
|
645
|
+
const baseType = normalizeText(column?.columnType);
|
|
646
|
+
if (!baseType) {
|
|
647
|
+
return "";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const characterSetName = normalizeText(column?.characterSetName);
|
|
651
|
+
const collationName = normalizeText(column?.collationName);
|
|
652
|
+
const normalizedTableCollation = normalizeText(tableCollation);
|
|
653
|
+
if (!collationName || collationName === normalizedTableCollation) {
|
|
654
|
+
return "";
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const parts = [baseType];
|
|
658
|
+
if (characterSetName) {
|
|
659
|
+
parts.push(`CHARACTER SET ${characterSetName}`);
|
|
660
|
+
}
|
|
661
|
+
parts.push(`COLLATE ${collationName}`);
|
|
662
|
+
return parts.join(" ");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderTemporalColumnBuilder(column, methodName) {
|
|
666
|
+
if (Number.isFinite(column?.datetimePrecision) && column.datetimePrecision > 0) {
|
|
667
|
+
return `table.${methodName}(${JSON.stringify(column.name)}, { precision: ${column.datetimePrecision} })`;
|
|
668
|
+
}
|
|
669
|
+
return `table.${methodName}(${JSON.stringify(column.name)})`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function renderMigrationColumnLine(column, {
|
|
673
|
+
idColumn = DEFAULT_ID_COLUMN,
|
|
674
|
+
primaryKeyColumns = [],
|
|
675
|
+
foreignKeyColumnNames = new Set(),
|
|
676
|
+
tableCollation = ""
|
|
677
|
+
} = {}) {
|
|
603
678
|
const isPrimary = Array.isArray(primaryKeyColumns) && primaryKeyColumns.includes(column.name);
|
|
604
679
|
const isIdColumn = column.name === idColumn;
|
|
680
|
+
const isRecordIdColumn = isIdColumn || column.name === "workspace_id" || column.name === "user_id" || foreignKeyColumnNames.has(column.name) || /_id$/i.test(String(column.name || ""));
|
|
605
681
|
|
|
606
682
|
if (isIdColumn && column.autoIncrement) {
|
|
607
|
-
let line = `table.
|
|
683
|
+
let line = `table.bigIncrements(${JSON.stringify(column.name)})`;
|
|
608
684
|
if (column.unsigned) {
|
|
609
685
|
line += ".unsigned()";
|
|
610
686
|
}
|
|
@@ -615,25 +691,40 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
615
691
|
let line = "";
|
|
616
692
|
const nameLiteral = JSON.stringify(column.name);
|
|
617
693
|
const dataType = String(column.dataType || "").toLowerCase();
|
|
694
|
+
const specificStringType = renderMigrationSpecificStringType(column, {
|
|
695
|
+
tableCollation
|
|
696
|
+
});
|
|
618
697
|
|
|
619
698
|
if (dataType === "varchar") {
|
|
620
|
-
|
|
621
|
-
|
|
699
|
+
if (specificStringType) {
|
|
700
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
701
|
+
} else {
|
|
702
|
+
const maxLength = Number.isFinite(column.maxLength) ? column.maxLength : 255;
|
|
703
|
+
line = `table.string(${nameLiteral}, ${maxLength})`;
|
|
704
|
+
}
|
|
622
705
|
} else if (dataType === "char") {
|
|
623
|
-
line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "char(255)")})`;
|
|
706
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType || column.columnType || "char(255)")})`;
|
|
624
707
|
} else if (dataType === "text") {
|
|
625
|
-
|
|
708
|
+
if (specificStringType) {
|
|
709
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
710
|
+
} else {
|
|
711
|
+
line = `table.text(${nameLiteral})`;
|
|
712
|
+
}
|
|
626
713
|
} else if (dataType === "tinytext" || dataType === "mediumtext" || dataType === "longtext") {
|
|
627
|
-
|
|
714
|
+
if (specificStringType) {
|
|
715
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType)})`;
|
|
716
|
+
} else {
|
|
717
|
+
line = `table.text(${nameLiteral}, ${JSON.stringify(dataType)})`;
|
|
718
|
+
}
|
|
628
719
|
} else if (dataType === "enum") {
|
|
629
720
|
const enumValues = Array.isArray(column.enumValues) ? column.enumValues : [];
|
|
630
721
|
line = `table.enu(${nameLiteral}, ${JSON.stringify(enumValues)})`;
|
|
631
722
|
} else if (dataType === "set") {
|
|
632
|
-
line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "set")})`;
|
|
723
|
+
line = `table.specificType(${nameLiteral}, ${JSON.stringify(specificStringType || column.columnType || "set")})`;
|
|
633
724
|
} else if (column.typeKind === "boolean") {
|
|
634
725
|
line = `table.boolean(${nameLiteral})`;
|
|
635
726
|
} else if (dataType === "int" || dataType === "integer") {
|
|
636
|
-
line = `table.integer(${nameLiteral})`;
|
|
727
|
+
line = isRecordIdColumn ? `table.bigInteger(${nameLiteral})` : `table.integer(${nameLiteral})`;
|
|
637
728
|
} else if (dataType === "smallint") {
|
|
638
729
|
line = `table.smallint(${nameLiteral})`;
|
|
639
730
|
} else if (dataType === "bigint") {
|
|
@@ -657,11 +748,11 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
657
748
|
} else if (dataType === "date") {
|
|
658
749
|
line = `table.date(${nameLiteral})`;
|
|
659
750
|
} else if (dataType === "time") {
|
|
660
|
-
line =
|
|
751
|
+
line = renderTemporalColumnBuilder(column, "time");
|
|
661
752
|
} else if (dataType === "datetime") {
|
|
662
|
-
line =
|
|
753
|
+
line = renderTemporalColumnBuilder(column, "dateTime");
|
|
663
754
|
} else if (dataType === "timestamp") {
|
|
664
|
-
line =
|
|
755
|
+
line = renderTemporalColumnBuilder(column, "timestamp");
|
|
665
756
|
} else {
|
|
666
757
|
throw new Error(
|
|
667
758
|
`Unsupported MySQL type "${dataType}" in migration renderer for column "${column.name}".`
|
|
@@ -682,10 +773,18 @@ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, prima
|
|
|
682
773
|
|
|
683
774
|
function renderMigrationColumnLines(snapshot) {
|
|
684
775
|
const columns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
776
|
+
const foreignKeyColumnNames = new Set(
|
|
777
|
+
(Array.isArray(snapshot.foreignKeys) ? snapshot.foreignKeys : [])
|
|
778
|
+
.flatMap((foreignKey) => Array.isArray(foreignKey?.columns) ? foreignKey.columns : [])
|
|
779
|
+
.map((entry) => String(entry?.name || "").trim())
|
|
780
|
+
.filter(Boolean)
|
|
781
|
+
);
|
|
685
782
|
const lines = columns.map((column) =>
|
|
686
783
|
` ${renderMigrationColumnLine(column, {
|
|
687
784
|
idColumn: snapshot.idColumn,
|
|
688
|
-
primaryKeyColumns: snapshot.primaryKeyColumns
|
|
785
|
+
primaryKeyColumns: snapshot.primaryKeyColumns,
|
|
786
|
+
foreignKeyColumnNames,
|
|
787
|
+
tableCollation: snapshot.tableCollation
|
|
689
788
|
})}`
|
|
690
789
|
);
|
|
691
790
|
return lines.join("\n");
|
|
@@ -699,13 +798,23 @@ function renderMigrationIndexLine(index) {
|
|
|
699
798
|
|
|
700
799
|
const columnsLiteral = JSON.stringify(columns);
|
|
701
800
|
const indexName = normalizeText(index?.name);
|
|
801
|
+
const normalizedIndexType = normalizeText(index?.indexType).toUpperCase();
|
|
802
|
+
const storageEngineIndexType = normalizedIndexType && normalizedIndexType !== "BTREE"
|
|
803
|
+
? normalizedIndexType.toLowerCase()
|
|
804
|
+
: "";
|
|
702
805
|
if (index?.unique === true) {
|
|
806
|
+
if (indexName && storageEngineIndexType) {
|
|
807
|
+
return ` table.unique(${columnsLiteral}, { indexName: ${JSON.stringify(indexName)}, storageEngineIndexType: ${JSON.stringify(storageEngineIndexType)} });`;
|
|
808
|
+
}
|
|
703
809
|
if (indexName) {
|
|
704
810
|
return ` table.unique(${columnsLiteral}, ${JSON.stringify(indexName)});`;
|
|
705
811
|
}
|
|
706
812
|
return ` table.unique(${columnsLiteral});`;
|
|
707
813
|
}
|
|
708
814
|
|
|
815
|
+
if (indexName && normalizedIndexType && normalizedIndexType !== "BTREE") {
|
|
816
|
+
return ` table.index(${columnsLiteral}, ${JSON.stringify(indexName)}, ${JSON.stringify(normalizedIndexType)});`;
|
|
817
|
+
}
|
|
709
818
|
if (indexName) {
|
|
710
819
|
return ` table.index(${columnsLiteral}, ${JSON.stringify(indexName)});`;
|
|
711
820
|
}
|
|
@@ -764,6 +873,28 @@ function renderMigrationForeignKeyLines(snapshot) {
|
|
|
764
873
|
return lines.join("\n");
|
|
765
874
|
}
|
|
766
875
|
|
|
876
|
+
function renderMigrationCheckConstraintLines(snapshot) {
|
|
877
|
+
const tableName = normalizeText(snapshot?.tableName);
|
|
878
|
+
const checkConstraints = Array.isArray(snapshot?.checkConstraints) ? snapshot.checkConstraints : [];
|
|
879
|
+
if (!tableName || checkConstraints.length < 1) {
|
|
880
|
+
return "";
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return checkConstraints
|
|
884
|
+
.map((constraint) => {
|
|
885
|
+
const name = normalizeText(constraint?.name);
|
|
886
|
+
const clause = normalizeText(constraint?.clause);
|
|
887
|
+
if (!name || !clause) {
|
|
888
|
+
return "";
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const sql = `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${name}\` CHECK (${clause})`;
|
|
892
|
+
return ` await knex.raw(${JSON.stringify(sql)});`;
|
|
893
|
+
})
|
|
894
|
+
.filter(Boolean)
|
|
895
|
+
.join("\n");
|
|
896
|
+
}
|
|
897
|
+
|
|
767
898
|
function mergeFieldMetaEntries(...entryGroups) {
|
|
768
899
|
const mergedByKey = new Map();
|
|
769
900
|
for (const sourceEntries of entryGroups) {
|
|
@@ -1116,7 +1247,8 @@ function buildReplacementsFromSnapshot({
|
|
|
1116
1247
|
writableColumns,
|
|
1117
1248
|
snapshot
|
|
1118
1249
|
});
|
|
1119
|
-
const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer");
|
|
1250
|
+
const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn !== true);
|
|
1251
|
+
const needsRecordIdSchemas = resourceColumns.some((column) => column.typeKind === "integer" && column.isRecordIdColumn === true);
|
|
1120
1252
|
const needsFiniteNumber = resourceColumns.some((column) => column.typeKind === "number");
|
|
1121
1253
|
const needsDateTimeOutput = outputColumns.some((column) => column.typeKind === "datetime");
|
|
1122
1254
|
const needsDateTimeInput = writableColumns.some((column) => column.typeKind === "datetime");
|
|
@@ -1145,7 +1277,8 @@ function buildReplacementsFromSnapshot({
|
|
|
1145
1277
|
__JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
|
|
1146
1278
|
__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__: resolvedOwnershipFilter,
|
|
1147
1279
|
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
|
|
1148
|
-
needsHtmlTimeSchemas
|
|
1280
|
+
needsHtmlTimeSchemas,
|
|
1281
|
+
needsRecordIdSchemas
|
|
1149
1282
|
}),
|
|
1150
1283
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
|
|
1151
1284
|
needsToIsoString: needsDateTimeOutput || needsDate,
|
|
@@ -1156,6 +1289,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1156
1289
|
needsNormalizeBoolean,
|
|
1157
1290
|
needsNormalizeFiniteNumber: needsFiniteNumber,
|
|
1158
1291
|
needsNormalizeFiniteInteger: needsFiniteInteger,
|
|
1292
|
+
needsNormalizeRecordId: needsRecordIdSchemas,
|
|
1159
1293
|
needsNormalizeIfInSource,
|
|
1160
1294
|
needsNormalizeIfPresent,
|
|
1161
1295
|
needsNormalizeOrNull
|
|
@@ -1176,7 +1310,8 @@ function buildReplacementsFromSnapshot({
|
|
|
1176
1310
|
__JSKIT_CRUD_LIST_CONFIG_LINES__: renderRepositoryListConfigLines(snapshot),
|
|
1177
1311
|
__JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
|
|
1178
1312
|
__JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot),
|
|
1179
|
-
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot)
|
|
1313
|
+
__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__: renderMigrationForeignKeyLines(snapshot),
|
|
1314
|
+
__JSKIT_CRUD_MIGRATION_CHECK_CONSTRAINT_LINES__: renderMigrationCheckConstraintLines(snapshot)
|
|
1180
1315
|
});
|
|
1181
1316
|
|
|
1182
1317
|
return replacements;
|
|
@@ -1266,6 +1401,7 @@ const __testables = Object.freeze({
|
|
|
1266
1401
|
buildReplacementsFromSnapshot,
|
|
1267
1402
|
parseDotEnvLine,
|
|
1268
1403
|
renderMigrationColumnLine,
|
|
1404
|
+
renderMigrationCheckConstraintLines,
|
|
1269
1405
|
renderMigrationForeignKeyLine,
|
|
1270
1406
|
resolveScaffoldColumns,
|
|
1271
1407
|
renderPropertyAccess,
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
} from "@jskit-ai/database-runtime/shared";
|
|
6
6
|
import {
|
|
7
7
|
normalizeObjectInput,
|
|
8
|
-
createCursorListValidator
|
|
8
|
+
createCursorListValidator,
|
|
9
|
+
recordIdSchema
|
|
9
10
|
} from "@jskit-ai/kernel/shared/validators";
|
|
10
11
|
import {
|
|
11
12
|
normalizeText,
|
|
13
|
+
normalizeRecordId,
|
|
12
14
|
normalizeFiniteNumber,
|
|
13
15
|
normalizeIfPresent
|
|
14
16
|
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
@@ -17,7 +19,7 @@ const RESOURCE_LOOKUP_CONTAINER_KEY = "lookups";
|
|
|
17
19
|
|
|
18
20
|
const recordOutputSchema = Type.Object(
|
|
19
21
|
{
|
|
20
|
-
id:
|
|
22
|
+
id: recordIdSchema,
|
|
21
23
|
textField: Type.String({ minLength: 1, maxLength: 160 }),
|
|
22
24
|
dateField: Type.String({ minLength: 1 }),
|
|
23
25
|
numberField: Type.Number(),
|
|
@@ -71,7 +73,7 @@ const recordOutputValidator = Object.freeze({
|
|
|
71
73
|
normalize(payload = {}) {
|
|
72
74
|
const source = normalizeObjectInput(payload);
|
|
73
75
|
const normalized = {
|
|
74
|
-
id:
|
|
76
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
75
77
|
textField: normalizeText(source.textField),
|
|
76
78
|
dateField: toIsoString(source.dateField),
|
|
77
79
|
numberField: normalizeFiniteNumber(source.numberField),
|
|
@@ -116,7 +118,7 @@ const patchBodyValidator = Object.freeze({
|
|
|
116
118
|
const deleteOutputValidator = Object.freeze({
|
|
117
119
|
schema: Type.Object(
|
|
118
120
|
{
|
|
119
|
-
id:
|
|
121
|
+
id: recordIdSchema,
|
|
120
122
|
deleted: Type.Literal(true)
|
|
121
123
|
},
|
|
122
124
|
{ additionalProperties: false }
|
|
@@ -125,7 +127,7 @@ const deleteOutputValidator = Object.freeze({
|
|
|
125
127
|
const source = normalizeObjectInput(payload);
|
|
126
128
|
|
|
127
129
|
return {
|
|
128
|
-
id:
|
|
130
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
129
131
|
deleted: true
|
|
130
132
|
};
|
|
131
133
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
crudRepositoryUpdateById,
|
|
8
8
|
crudRepositoryDeleteById
|
|
9
9
|
} from "@jskit-ai/crud-core/server/repositoryMethods";
|
|
10
|
+
import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
|
|
10
11
|
import { resource } from "../shared/${option:namespace|singular|camel}Resource.js";
|
|
11
12
|
import { LIST_CONFIG } from "./listConfig.js";
|
|
12
13
|
|
|
@@ -16,6 +17,8 @@ const repositoryRuntime = createCrudRepositoryRuntime(resource, {
|
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
function createRepository(knex, options = {}) {
|
|
20
|
+
const withTransaction = createWithTransaction(knex);
|
|
21
|
+
|
|
19
22
|
async function list(query = {}, callOptions = {}) {
|
|
20
23
|
return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions);
|
|
21
24
|
}
|
|
@@ -41,6 +44,7 @@ function createRepository(knex, options = {}) {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
return Object.freeze({
|
|
47
|
+
withTransaction,
|
|
44
48
|
list,
|
|
45
49
|
findById,
|
|
46
50
|
listByIds,
|
|
@@ -65,7 +65,7 @@ const patchBodyValidator = Object.freeze({
|
|
|
65
65
|
const deleteOutputValidator = Object.freeze({
|
|
66
66
|
schema: Type.Object(
|
|
67
67
|
{
|
|
68
|
-
id:
|
|
68
|
+
id: recordIdSchema,
|
|
69
69
|
deleted: Type.Literal(true)
|
|
70
70
|
},
|
|
71
71
|
{ additionalProperties: false }
|
|
@@ -74,7 +74,7 @@ const deleteOutputValidator = Object.freeze({
|
|
|
74
74
|
const source = normalizeObjectInput(payload);
|
|
75
75
|
|
|
76
76
|
return {
|
|
77
|
-
id:
|
|
77
|
+
id: normalizeRecordId(source.id, { fallback: "" }),
|
|
78
78
|
deleted: true
|
|
79
79
|
};
|
|
80
80
|
}
|
|
@@ -107,8 +107,8 @@ function createSnapshot() {
|
|
|
107
107
|
idColumn: "id",
|
|
108
108
|
columns: [
|
|
109
109
|
{ name: "id", key: "id", typeKind: "integer", nullable: false, unsigned: true },
|
|
110
|
-
{ name: "
|
|
111
|
-
{ name: "
|
|
110
|
+
{ name: "workspace_id", key: "workspaceId", typeKind: "integer", nullable: true, unsigned: true },
|
|
111
|
+
{ name: "user_id", key: "userId", typeKind: "integer", nullable: true, unsigned: true },
|
|
112
112
|
{ name: "created_at", key: "createdAt", typeKind: "datetime", nullable: false },
|
|
113
113
|
{ name: "updated_at", key: "updatedAt", typeKind: "datetime", nullable: false },
|
|
114
114
|
{ name: "first_name", key: "firstName", typeKind: "string", nullable: true, maxLength: 160 },
|
|
@@ -145,15 +145,21 @@ test("scaffold-field patches CRUD resource file using DB snapshot metadata", asy
|
|
|
145
145
|
assert.deepEqual(result.touchedFiles, [resourceFile]);
|
|
146
146
|
|
|
147
147
|
const content = await readFile(path.join(appRoot, resourceFile), "utf8");
|
|
148
|
-
assert.match(content, /categoryId:
|
|
149
|
-
assert.match(
|
|
150
|
-
|
|
148
|
+
assert.match(content, /categoryId: nullableRecordIdSchema/);
|
|
149
|
+
assert.match(
|
|
150
|
+
content,
|
|
151
|
+
/normalizeIfInSource\(source, normalized, "categoryId", \(value\) => normalizeRecordId\(value, \{ fallback: null \}\)\);/
|
|
152
|
+
);
|
|
153
|
+
assert.match(
|
|
154
|
+
content,
|
|
155
|
+
/categoryId: normalizeOrNull\(source\.categoryId, \(value\) => normalizeRecordId\(value, \{ fallback: null \}\)\)/
|
|
156
|
+
);
|
|
151
157
|
assert.match(content, /RESOURCE_FIELD_META\.push\(\{/);
|
|
152
158
|
assert.match(content, /key: "categoryId"/);
|
|
153
159
|
assert.match(content, /namespace: "customer-categories"/);
|
|
154
160
|
assert.match(content, /valueKey: "id"/);
|
|
155
161
|
assert.match(content, /formControl: "autocomplete" \/\/ or "select"/);
|
|
156
|
-
assert.match(content, /
|
|
162
|
+
assert.match(content, /normalizeRecordId/);
|
|
157
163
|
|
|
158
164
|
const secondRun = await runGeneratorSubcommand({
|
|
159
165
|
appRoot,
|
|
@@ -8,8 +8,8 @@ import { buildTemplateContext, __testables } from "../src/server/buildTemplateCo
|
|
|
8
8
|
|
|
9
9
|
function createSnapshot({
|
|
10
10
|
tableName = "contacts",
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
hasWorkspaceIdColumn = true,
|
|
12
|
+
hasUserIdColumn = true,
|
|
13
13
|
hasCreatedAtColumn = true
|
|
14
14
|
} = {}) {
|
|
15
15
|
const createdAtColumn = hasCreatedAtColumn
|
|
@@ -29,16 +29,20 @@ function createSnapshot({
|
|
|
29
29
|
maxLength: null,
|
|
30
30
|
numericPrecision: null,
|
|
31
31
|
numericScale: null,
|
|
32
|
+
datetimePrecision: null,
|
|
33
|
+
characterSetName: "",
|
|
34
|
+
collationName: "",
|
|
32
35
|
enumValues: Object.freeze([])
|
|
33
36
|
})
|
|
34
37
|
]
|
|
35
38
|
: [];
|
|
36
39
|
return Object.freeze({
|
|
37
40
|
tableName,
|
|
41
|
+
tableCollation: "utf8mb4_general_ci",
|
|
38
42
|
idColumn: "id",
|
|
39
43
|
primaryKeyColumns: Object.freeze(["id"]),
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
hasWorkspaceIdColumn,
|
|
45
|
+
hasUserIdColumn,
|
|
42
46
|
columns: Object.freeze([
|
|
43
47
|
Object.freeze({
|
|
44
48
|
name: "id",
|
|
@@ -55,11 +59,14 @@ function createSnapshot({
|
|
|
55
59
|
maxLength: null,
|
|
56
60
|
numericPrecision: 10,
|
|
57
61
|
numericScale: 0,
|
|
62
|
+
datetimePrecision: null,
|
|
63
|
+
characterSetName: "",
|
|
64
|
+
collationName: "",
|
|
58
65
|
enumValues: Object.freeze([])
|
|
59
66
|
}),
|
|
60
67
|
Object.freeze({
|
|
61
|
-
name: "
|
|
62
|
-
key: "
|
|
68
|
+
name: "workspace_id",
|
|
69
|
+
key: "workspaceId",
|
|
63
70
|
dataType: "int",
|
|
64
71
|
columnType: "int unsigned",
|
|
65
72
|
typeKind: "integer",
|
|
@@ -72,11 +79,14 @@ function createSnapshot({
|
|
|
72
79
|
maxLength: null,
|
|
73
80
|
numericPrecision: 10,
|
|
74
81
|
numericScale: 0,
|
|
82
|
+
datetimePrecision: null,
|
|
83
|
+
characterSetName: "",
|
|
84
|
+
collationName: "",
|
|
75
85
|
enumValues: Object.freeze([])
|
|
76
86
|
}),
|
|
77
87
|
Object.freeze({
|
|
78
|
-
name: "
|
|
79
|
-
key: "
|
|
88
|
+
name: "user_id",
|
|
89
|
+
key: "userId",
|
|
80
90
|
dataType: "int",
|
|
81
91
|
columnType: "int unsigned",
|
|
82
92
|
typeKind: "integer",
|
|
@@ -89,6 +99,9 @@ function createSnapshot({
|
|
|
89
99
|
maxLength: null,
|
|
90
100
|
numericPrecision: 10,
|
|
91
101
|
numericScale: 0,
|
|
102
|
+
datetimePrecision: null,
|
|
103
|
+
characterSetName: "",
|
|
104
|
+
collationName: "",
|
|
92
105
|
enumValues: Object.freeze([])
|
|
93
106
|
}),
|
|
94
107
|
Object.freeze({
|
|
@@ -106,6 +119,9 @@ function createSnapshot({
|
|
|
106
119
|
maxLength: 160,
|
|
107
120
|
numericPrecision: null,
|
|
108
121
|
numericScale: null,
|
|
122
|
+
datetimePrecision: null,
|
|
123
|
+
characterSetName: "utf8mb4",
|
|
124
|
+
collationName: "utf8mb4_general_ci",
|
|
109
125
|
enumValues: Object.freeze([])
|
|
110
126
|
}),
|
|
111
127
|
...createdAtColumn,
|
|
@@ -124,30 +140,34 @@ function createSnapshot({
|
|
|
124
140
|
maxLength: null,
|
|
125
141
|
numericPrecision: null,
|
|
126
142
|
numericScale: null,
|
|
143
|
+
datetimePrecision: null,
|
|
144
|
+
characterSetName: "",
|
|
145
|
+
collationName: "",
|
|
127
146
|
enumValues: Object.freeze([])
|
|
128
147
|
})
|
|
129
148
|
]),
|
|
130
149
|
indexes: Object.freeze([]),
|
|
131
|
-
foreignKeys: Object.freeze([])
|
|
150
|
+
foreignKeys: Object.freeze([]),
|
|
151
|
+
checkConstraints: Object.freeze([])
|
|
132
152
|
});
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
test("resolveOwnershipFilterForGeneration infers ownership filter for table introspection mode", () => {
|
|
136
156
|
const snapshotBoth = createSnapshot({
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
hasWorkspaceIdColumn: true,
|
|
158
|
+
hasUserIdColumn: true
|
|
139
159
|
});
|
|
140
160
|
const snapshotWorkspaceOnly = createSnapshot({
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
hasWorkspaceIdColumn: true,
|
|
162
|
+
hasUserIdColumn: false
|
|
143
163
|
});
|
|
144
164
|
const snapshotUserOnly = createSnapshot({
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
hasWorkspaceIdColumn: false,
|
|
166
|
+
hasUserIdColumn: true
|
|
147
167
|
});
|
|
148
168
|
const snapshotPublic = createSnapshot({
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
hasWorkspaceIdColumn: false,
|
|
170
|
+
hasUserIdColumn: false
|
|
151
171
|
});
|
|
152
172
|
|
|
153
173
|
assert.equal(
|
|
@@ -178,8 +198,8 @@ test("resolveOwnershipFilterForGeneration infers ownership filter for table intr
|
|
|
178
198
|
|
|
179
199
|
test("resolveOwnershipFilterForGeneration rejects explicit ownership filters when required columns are missing", () => {
|
|
180
200
|
const snapshotPublic = createSnapshot({
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
hasWorkspaceIdColumn: false,
|
|
202
|
+
hasUserIdColumn: false
|
|
183
203
|
});
|
|
184
204
|
|
|
185
205
|
assert.throws(
|
|
@@ -187,14 +207,14 @@ test("resolveOwnershipFilterForGeneration rejects explicit ownership filters whe
|
|
|
187
207
|
__testables.resolveOwnershipFilterForGeneration(snapshotPublic, "workspace", {
|
|
188
208
|
enforceTableColumns: true
|
|
189
209
|
}),
|
|
190
|
-
/requires column "
|
|
210
|
+
/requires column "workspace_id"/
|
|
191
211
|
);
|
|
192
212
|
assert.throws(
|
|
193
213
|
() =>
|
|
194
214
|
__testables.resolveOwnershipFilterForGeneration(snapshotPublic, "user", {
|
|
195
215
|
enforceTableColumns: true
|
|
196
216
|
}),
|
|
197
|
-
/requires column "
|
|
217
|
+
/requires column "user_id"/
|
|
198
218
|
);
|
|
199
219
|
});
|
|
200
220
|
|
|
@@ -221,13 +241,13 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
221
241
|
assert.equal(replacements.__JSKIT_CRUD_TABLE_NAME__, "\"contacts\"");
|
|
222
242
|
assert.equal(replacements.__JSKIT_CRUD_ID_COLUMN__, "\"id\"");
|
|
223
243
|
assert.equal(replacements.__JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__, "workspace_user");
|
|
224
|
-
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.
|
|
244
|
+
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.bigIncrements\("id"\)/);
|
|
225
245
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.string\("first_name", 160\)/);
|
|
226
246
|
assert.equal(replacements.__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__, "");
|
|
227
247
|
assert.match(replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__, /updatedAt: Type\.String/);
|
|
228
248
|
assert.match(
|
|
229
249
|
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
230
|
-
/id:
|
|
250
|
+
/id: recordIdSchema,/
|
|
231
251
|
);
|
|
232
252
|
assert.match(replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__, /firstName: Type\.String/);
|
|
233
253
|
assert.match(
|
|
@@ -332,8 +352,8 @@ test("buildReplacementsFromSnapshot renders append-only field meta entries from
|
|
|
332
352
|
|
|
333
353
|
test("buildReplacementsFromSnapshot renders enum field meta options as select controls", () => {
|
|
334
354
|
const baseSnapshot = createSnapshot({
|
|
335
|
-
|
|
336
|
-
|
|
355
|
+
hasWorkspaceIdColumn: false,
|
|
356
|
+
hasUserIdColumn: false
|
|
337
357
|
});
|
|
338
358
|
const snapshot = {
|
|
339
359
|
...baseSnapshot,
|
|
@@ -374,7 +394,7 @@ test("buildReplacementsFromSnapshot renders enum field meta options as select co
|
|
|
374
394
|
test("renderMigrationColumnLine ignores SQL NULL string defaults", () => {
|
|
375
395
|
const line = __testables.renderMigrationColumnLine(
|
|
376
396
|
{
|
|
377
|
-
name: "
|
|
397
|
+
name: "workspace_id",
|
|
378
398
|
dataType: "int",
|
|
379
399
|
columnType: "int unsigned",
|
|
380
400
|
typeKind: "integer",
|
|
@@ -398,10 +418,144 @@ test("renderMigrationColumnLine ignores SQL NULL string defaults", () => {
|
|
|
398
418
|
assert.equal(line.includes(".defaultTo("), false);
|
|
399
419
|
});
|
|
400
420
|
|
|
421
|
+
test("renderMigrationColumnLine unwraps quoted string defaults", () => {
|
|
422
|
+
const stringLine = __testables.renderMigrationColumnLine({
|
|
423
|
+
name: "name",
|
|
424
|
+
dataType: "varchar",
|
|
425
|
+
columnType: "varchar(255)",
|
|
426
|
+
typeKind: "string",
|
|
427
|
+
nullable: false,
|
|
428
|
+
hasDefault: true,
|
|
429
|
+
defaultValue: "''",
|
|
430
|
+
autoIncrement: false,
|
|
431
|
+
unsigned: false,
|
|
432
|
+
extra: "",
|
|
433
|
+
maxLength: 255,
|
|
434
|
+
numericPrecision: null,
|
|
435
|
+
numericScale: null,
|
|
436
|
+
datetimePrecision: null,
|
|
437
|
+
characterSetName: "utf8mb4",
|
|
438
|
+
collationName: "utf8mb4_general_ci",
|
|
439
|
+
enumValues: []
|
|
440
|
+
});
|
|
441
|
+
const enumLine = __testables.renderMigrationColumnLine({
|
|
442
|
+
name: "temperament",
|
|
443
|
+
dataType: "enum",
|
|
444
|
+
columnType: "enum('friendly','unknown')",
|
|
445
|
+
typeKind: "string",
|
|
446
|
+
nullable: false,
|
|
447
|
+
hasDefault: true,
|
|
448
|
+
defaultValue: "'unknown'",
|
|
449
|
+
autoIncrement: false,
|
|
450
|
+
unsigned: false,
|
|
451
|
+
extra: "",
|
|
452
|
+
maxLength: null,
|
|
453
|
+
numericPrecision: null,
|
|
454
|
+
numericScale: null,
|
|
455
|
+
datetimePrecision: null,
|
|
456
|
+
characterSetName: "utf8mb4",
|
|
457
|
+
collationName: "utf8mb4_general_ci",
|
|
458
|
+
enumValues: ["friendly", "unknown"]
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
assert.match(stringLine, /\.defaultTo\(""\)/);
|
|
462
|
+
assert.match(enumLine, /\.defaultTo\("unknown"\)/);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("renderMigrationColumnLine preserves datetime precision", () => {
|
|
466
|
+
const line = __testables.renderMigrationColumnLine({
|
|
467
|
+
name: "deleted_at",
|
|
468
|
+
dataType: "datetime",
|
|
469
|
+
columnType: "datetime(3)",
|
|
470
|
+
typeKind: "datetime",
|
|
471
|
+
nullable: true,
|
|
472
|
+
hasDefault: false,
|
|
473
|
+
defaultValue: null,
|
|
474
|
+
autoIncrement: false,
|
|
475
|
+
unsigned: false,
|
|
476
|
+
extra: "",
|
|
477
|
+
maxLength: null,
|
|
478
|
+
numericPrecision: null,
|
|
479
|
+
numericScale: null,
|
|
480
|
+
datetimePrecision: 3,
|
|
481
|
+
characterSetName: "",
|
|
482
|
+
collationName: "",
|
|
483
|
+
enumValues: []
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
assert.match(line, /table\.dateTime\("deleted_at", \{ precision: 3 \}\)/);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("buildReplacementsFromSnapshot preserves custom collations, hash unique indexes, and check constraints", () => {
|
|
490
|
+
const snapshot = createSnapshot({
|
|
491
|
+
tableName: "services",
|
|
492
|
+
hasWorkspaceIdColumn: true,
|
|
493
|
+
hasUserIdColumn: false
|
|
494
|
+
});
|
|
495
|
+
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
496
|
+
snapshot: {
|
|
497
|
+
...snapshot,
|
|
498
|
+
columns: Object.freeze([
|
|
499
|
+
snapshot.columns[0],
|
|
500
|
+
snapshot.columns[1],
|
|
501
|
+
Object.freeze({
|
|
502
|
+
name: "settings_json",
|
|
503
|
+
key: "settingsJson",
|
|
504
|
+
dataType: "longtext",
|
|
505
|
+
columnType: "longtext",
|
|
506
|
+
typeKind: "string",
|
|
507
|
+
nullable: true,
|
|
508
|
+
hasDefault: false,
|
|
509
|
+
defaultValue: null,
|
|
510
|
+
autoIncrement: false,
|
|
511
|
+
unsigned: false,
|
|
512
|
+
extra: "",
|
|
513
|
+
maxLength: null,
|
|
514
|
+
numericPrecision: null,
|
|
515
|
+
numericScale: null,
|
|
516
|
+
datetimePrecision: null,
|
|
517
|
+
characterSetName: "utf8mb4",
|
|
518
|
+
collationName: "utf8mb4_bin",
|
|
519
|
+
enumValues: Object.freeze([])
|
|
520
|
+
})
|
|
521
|
+
]),
|
|
522
|
+
indexes: Object.freeze([
|
|
523
|
+
Object.freeze({
|
|
524
|
+
name: "uq_services_workspace_settings",
|
|
525
|
+
unique: true,
|
|
526
|
+
indexType: "HASH",
|
|
527
|
+
columns: Object.freeze(["workspace_id", "settings_json"])
|
|
528
|
+
})
|
|
529
|
+
]),
|
|
530
|
+
foreignKeys: Object.freeze([]),
|
|
531
|
+
checkConstraints: Object.freeze([
|
|
532
|
+
Object.freeze({
|
|
533
|
+
name: "settings_json",
|
|
534
|
+
clause: "json_valid(`settings_json`)"
|
|
535
|
+
})
|
|
536
|
+
])
|
|
537
|
+
},
|
|
538
|
+
resolvedOwnershipFilter: "workspace"
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
assert.match(
|
|
542
|
+
replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__,
|
|
543
|
+
/table\.specificType\("settings_json", "longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin"\)/
|
|
544
|
+
);
|
|
545
|
+
assert.match(
|
|
546
|
+
replacements.__JSKIT_CRUD_MIGRATION_INDEX_LINES__,
|
|
547
|
+
/storageEngineIndexType: "hash"/
|
|
548
|
+
);
|
|
549
|
+
assert.match(
|
|
550
|
+
replacements.__JSKIT_CRUD_MIGRATION_CHECK_CONSTRAINT_LINES__,
|
|
551
|
+
/ALTER TABLE `services` ADD CONSTRAINT `settings_json` CHECK \(json_valid\(`settings_json`\)\)/
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
|
|
401
555
|
test("buildReplacementsFromSnapshot normalizes nullable temporal inputs without invalid date errors", () => {
|
|
402
556
|
const snapshot = createSnapshot({
|
|
403
|
-
|
|
404
|
-
|
|
557
|
+
hasWorkspaceIdColumn: false,
|
|
558
|
+
hasUserIdColumn: false
|
|
405
559
|
});
|
|
406
560
|
const temporalColumns = [
|
|
407
561
|
...snapshot.columns.filter((column) => column.key !== "updatedAt"),
|
|
@@ -31,7 +31,7 @@ test("crudResource normalizes list output", () => {
|
|
|
31
31
|
nextCursor: " 8 "
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
assert.equal(normalized.items[0].id, 7);
|
|
34
|
+
assert.equal(normalized.items[0].id, "7");
|
|
35
35
|
assert.equal(normalized.items[0].textField, "Example text");
|
|
36
36
|
assert.equal(normalized.items[0].dateField, "2026-03-10T00:00:00.000Z");
|
|
37
37
|
assert.equal(normalized.items[0].numberField, 99);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import descriptor from "../package.descriptor.mjs";
|
|
4
|
+
|
|
5
|
+
test("crud-server-generator surface option validates against enabled surface ids", () => {
|
|
6
|
+
assert.equal(descriptor.kind, "generator");
|
|
7
|
+
assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
|
|
8
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("surface"), true);
|
|
9
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.optionNames?.includes("force"), true);
|
|
10
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.scaffold?.createTarget?.pathTemplate, "packages/${option:namespace|kebab}");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("crud-server-generator installs listConfig alongside server templates", () => {
|
|
14
|
+
const files = descriptor.mutations?.files || [];
|
|
15
|
+
const listConfigTemplate = files.find((entry) => entry.from === "templates/src/local-package/server/listConfig.js");
|
|
16
|
+
|
|
17
|
+
assert.ok(listConfigTemplate);
|
|
18
|
+
assert.equal(
|
|
19
|
+
listConfigTemplate.to,
|
|
20
|
+
"packages/${option:namespace|kebab}/src/server/listConfig.js"
|
|
21
|
+
);
|
|
22
|
+
assert.deepEqual(listConfigTemplate.templateContext, {
|
|
23
|
+
entrypoint: "src/server/buildTemplateContext.js",
|
|
24
|
+
export: "buildTemplateContext"
|
|
25
|
+
});
|
|
26
|
+
});
|