@jskit-ai/crud-server-generator 0.1.26

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.
@@ -0,0 +1,871 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import {
6
+ normalizeText,
7
+ resolveDatabaseClientFromEnvironment,
8
+ resolveDatabaseConnectionFromEnvironment,
9
+ toKnexClientId
10
+ } from "@jskit-ai/database-runtime/shared";
11
+ import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
12
+
13
+ const DEFAULT_ID_COLUMN = "id";
14
+ const OWNERSHIP_FILTER_AUTO = "auto";
15
+ const OWNERSHIP_FILTER_VALUES = new Set([
16
+ OWNERSHIP_FILTER_AUTO,
17
+ "public",
18
+ "user",
19
+ "workspace",
20
+ "workspace_user"
21
+ ]);
22
+ const MYSQL_CLIENT_ID = "mysql2";
23
+
24
+ function resolveGlobalScaffoldCache() {
25
+ const globalObject = globalThis;
26
+ const cacheKey = "__jskitCrudTemplateContextCache";
27
+ const existing = globalObject?.[cacheKey];
28
+ if (existing instanceof Map) {
29
+ return existing;
30
+ }
31
+
32
+ const nextCache = new Map();
33
+ if (globalObject && typeof globalObject === "object") {
34
+ globalObject[cacheKey] = nextCache;
35
+ }
36
+ return nextCache;
37
+ }
38
+
39
+ const scaffoldCache = resolveGlobalScaffoldCache();
40
+
41
+ function asRecord(value) {
42
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
43
+ return {};
44
+ }
45
+ return value;
46
+ }
47
+
48
+ function normalizeRequestedOwnershipFilter(value, { strict = false } = {}) {
49
+ const normalized = normalizeText(value).toLowerCase();
50
+ if (OWNERSHIP_FILTER_VALUES.has(normalized)) {
51
+ return normalized;
52
+ }
53
+ if (strict) {
54
+ throw new Error(
55
+ `Invalid ownership filter "${normalized || String(value || "")}". Use: auto, public, user, workspace, workspace_user.`
56
+ );
57
+ }
58
+ return OWNERSHIP_FILTER_AUTO;
59
+ }
60
+
61
+ function inferOwnershipFilterFromSnapshot(snapshot) {
62
+ const hasWorkspace = snapshot?.hasWorkspaceOwnerColumn === true;
63
+ const hasUser = snapshot?.hasUserOwnerColumn === true;
64
+ if (hasWorkspace && hasUser) {
65
+ return "workspace_user";
66
+ }
67
+ if (hasWorkspace) {
68
+ return "workspace";
69
+ }
70
+ if (hasUser) {
71
+ return "user";
72
+ }
73
+ return "public";
74
+ }
75
+
76
+ function assertOwnershipColumnsForFilter(snapshot, filter) {
77
+ const hasWorkspace = snapshot?.hasWorkspaceOwnerColumn === true;
78
+ const hasUser = snapshot?.hasUserOwnerColumn === true;
79
+ if (filter === "public") {
80
+ return;
81
+ }
82
+ if (filter === "workspace" && !hasWorkspace) {
83
+ throw new Error(
84
+ 'Ownership filter "workspace" requires column "workspace_owner_id".'
85
+ );
86
+ }
87
+ if (filter === "user" && !hasUser) {
88
+ throw new Error(
89
+ 'Ownership filter "user" requires column "user_owner_id".'
90
+ );
91
+ }
92
+ if (filter === "workspace_user" && (!hasWorkspace || !hasUser)) {
93
+ throw new Error(
94
+ 'Ownership filter "workspace_user" requires both columns "workspace_owner_id" and "user_owner_id".'
95
+ );
96
+ }
97
+ }
98
+
99
+ function resolveOwnershipFilterForGeneration(snapshot, requestedOwnershipFilter, { enforceTableColumns = false } = {}) {
100
+ const requested = normalizeRequestedOwnershipFilter(requestedOwnershipFilter, {
101
+ strict: enforceTableColumns
102
+ });
103
+ if (!enforceTableColumns) {
104
+ return requested;
105
+ }
106
+ if (requested === OWNERSHIP_FILTER_AUTO) {
107
+ return inferOwnershipFilterFromSnapshot(snapshot);
108
+ }
109
+ assertOwnershipColumnsForFilter(snapshot, requested);
110
+ return requested;
111
+ }
112
+
113
+ function parseDotEnvLine(line = "") {
114
+ const source = String(line || "").trim();
115
+ if (!source || source.startsWith("#")) {
116
+ return null;
117
+ }
118
+ const separatorIndex = source.indexOf("=");
119
+ if (separatorIndex < 0) {
120
+ return null;
121
+ }
122
+
123
+ const key = normalizeText(source.slice(0, separatorIndex));
124
+ if (!key) {
125
+ return null;
126
+ }
127
+
128
+ let value = String(source.slice(separatorIndex + 1) || "").trim();
129
+ if (
130
+ (value.startsWith('"') && value.endsWith('"')) ||
131
+ (value.startsWith("'") && value.endsWith("'"))
132
+ ) {
133
+ value = value.slice(1, -1);
134
+ } else {
135
+ const commentIndex = value.indexOf(" #");
136
+ if (commentIndex >= 0) {
137
+ value = value.slice(0, commentIndex).trim();
138
+ }
139
+ }
140
+
141
+ return {
142
+ key,
143
+ value
144
+ };
145
+ }
146
+
147
+ async function loadEnvFromApp(appRoot) {
148
+ const envPath = path.join(path.resolve(String(appRoot || "")), ".env");
149
+ let envContent = "";
150
+ try {
151
+ envContent = await readFile(envPath, "utf8");
152
+ } catch {
153
+ envContent = "";
154
+ }
155
+
156
+ const parsed = {};
157
+ for (const line of String(envContent || "").split(/\r?\n/)) {
158
+ const parsedLine = parseDotEnvLine(line);
159
+ if (!parsedLine) {
160
+ continue;
161
+ }
162
+ parsed[parsedLine.key] = parsedLine.value;
163
+ }
164
+
165
+ return {
166
+ ...parsed,
167
+ ...process.env
168
+ };
169
+ }
170
+
171
+ function createAppRequire(appRoot) {
172
+ const resolvedAppRoot = path.resolve(String(appRoot || ""));
173
+ const packageJsonPath = path.join(resolvedAppRoot, "package.json");
174
+ return createRequire(packageJsonPath);
175
+ }
176
+
177
+ async function importModuleFromApp(appRequire, moduleId, contextLabel) {
178
+ let resolvedPath = "";
179
+ try {
180
+ resolvedPath = appRequire.resolve(moduleId);
181
+ } catch {
182
+ throw new Error(
183
+ `${contextLabel} requires dependency "${moduleId}" installed in the app.`
184
+ );
185
+ }
186
+
187
+ try {
188
+ return await import(`${pathToFileURL(resolvedPath).href}?t=${Date.now()}_${Math.random()}`);
189
+ } catch (error) {
190
+ throw new Error(
191
+ `${contextLabel} failed loading "${moduleId}": ${String(error?.message || error || "unknown error")}`
192
+ );
193
+ }
194
+ }
195
+
196
+ function resolveKnexFactory(moduleNamespace) {
197
+ if (typeof moduleNamespace === "function") {
198
+ return moduleNamespace;
199
+ }
200
+ if (typeof moduleNamespace?.default === "function") {
201
+ return moduleNamespace.default;
202
+ }
203
+ if (typeof moduleNamespace?.knex === "function") {
204
+ return moduleNamespace.knex;
205
+ }
206
+ throw new Error("Resolved knex module did not expose a callable factory.");
207
+ }
208
+
209
+ async function resolveMysqlSnapshotFromDatabase({
210
+ appRoot,
211
+ tableName,
212
+ idColumn
213
+ } = {}) {
214
+ const env = await loadEnvFromApp(appRoot);
215
+ const dbClient = resolveDatabaseClientFromEnvironment(env);
216
+ if (dbClient !== MYSQL_CLIENT_ID) {
217
+ throw new Error(
218
+ `CRUD table introspection currently supports only DB_CLIENT=${MYSQL_CLIENT_ID}. Found "${dbClient}".`
219
+ );
220
+ }
221
+
222
+ const connection = resolveDatabaseConnectionFromEnvironment(env, {
223
+ defaultPort: 3306,
224
+ context: "crud table introspection"
225
+ });
226
+ const knexClientId = toKnexClientId(dbClient);
227
+ const appRequire = createAppRequire(appRoot);
228
+
229
+ const knexModule = await importModuleFromApp(appRequire, "knex", "CRUD table introspection");
230
+ const mysqlSharedModule = await importModuleFromApp(
231
+ appRequire,
232
+ "@jskit-ai/database-runtime-mysql/shared",
233
+ "CRUD table introspection"
234
+ );
235
+
236
+ const knexFactory = resolveKnexFactory(knexModule);
237
+ const introspectCrudTableSnapshot = mysqlSharedModule?.introspectCrudTableSnapshot;
238
+ if (typeof introspectCrudTableSnapshot !== "function") {
239
+ throw new Error(
240
+ "CRUD table introspection requires @jskit-ai/database-runtime-mysql/shared export introspectCrudTableSnapshot()."
241
+ );
242
+ }
243
+
244
+ const knex = knexFactory({
245
+ client: knexClientId,
246
+ connection
247
+ });
248
+ try {
249
+ return await introspectCrudTableSnapshot(knex, {
250
+ tableName,
251
+ idColumn
252
+ });
253
+ } finally {
254
+ if (knex && typeof knex.destroy === "function") {
255
+ await knex.destroy();
256
+ }
257
+ }
258
+ }
259
+
260
+ function resolveColumnKey(column, idColumn) {
261
+ if (column.name === idColumn) {
262
+ return "id";
263
+ }
264
+ return String(column.key || "");
265
+ }
266
+
267
+ function resolveScaffoldColumns(snapshot) {
268
+ const idColumn = String(snapshot.idColumn || DEFAULT_ID_COLUMN);
269
+ const sourceColumns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
270
+ const seenKeys = new Set();
271
+
272
+ const columns = sourceColumns.map((column) => {
273
+ const isWorkspaceOwnerColumn = column.name === "workspace_owner_id";
274
+ const isUserOwnerColumn = column.name === "user_owner_id";
275
+ const isOwnerColumn = isWorkspaceOwnerColumn || isUserOwnerColumn;
276
+ const isIdColumn = column.name === idColumn;
277
+ const isCreatedAtColumn = column.name === "created_at";
278
+ const isUpdatedAtColumn = column.name === "updated_at";
279
+ const key = resolveColumnKey(column, idColumn);
280
+ if (!key) {
281
+ throw new Error(`Could not derive API field key for column "${column.name}".`);
282
+ }
283
+
284
+ if (!isOwnerColumn) {
285
+ if (seenKeys.has(key)) {
286
+ throw new Error(
287
+ `Generated API field key "${key}" is duplicated. Rename columns or choose a different id column.`
288
+ );
289
+ }
290
+ seenKeys.add(key);
291
+ }
292
+
293
+ return Object.freeze({
294
+ ...column,
295
+ key,
296
+ isOwnerColumn,
297
+ isIdColumn,
298
+ isCreatedAtColumn,
299
+ isUpdatedAtColumn,
300
+ writable: !isOwnerColumn && !isIdColumn && !isCreatedAtColumn && !isUpdatedAtColumn
301
+ });
302
+ });
303
+
304
+ return Object.freeze(columns);
305
+ }
306
+
307
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
308
+
309
+ function isIdentifier(value) {
310
+ return IDENTIFIER_PATTERN.test(String(value || ""));
311
+ }
312
+
313
+ function renderObjectPropertyKey(value) {
314
+ const key = String(value || "");
315
+ return isIdentifier(key) ? key : JSON.stringify(key);
316
+ }
317
+
318
+ function renderPropertyAccess(sourceName, key) {
319
+ const normalizedKey = String(key || "");
320
+ if (isIdentifier(normalizedKey)) {
321
+ return `${sourceName}.${normalizedKey}`;
322
+ }
323
+ return `${sourceName}[${JSON.stringify(normalizedKey)}]`;
324
+ }
325
+
326
+ function renderIntegerSchema(column) {
327
+ const options = [];
328
+ if (column.isIdColumn === true) {
329
+ options.push("minimum: 1");
330
+ } else if (column.unsigned === true) {
331
+ options.push("minimum: 0");
332
+ }
333
+ if (options.length > 0) {
334
+ return `Type.Integer({ ${options.join(", ")} })`;
335
+ }
336
+ return "Type.Integer()";
337
+ }
338
+
339
+ function renderStringSchema(column, { forOutput = false } = {}) {
340
+ const options = [];
341
+ if (!forOutput && Number.isInteger(column.maxLength) && column.maxLength > 0) {
342
+ options.push(`maxLength: ${column.maxLength}`);
343
+ }
344
+ const enumValues = Array.isArray(column.enumValues) ? column.enumValues.filter((entry) => entry != null) : [];
345
+ if (!forOutput && enumValues.length > 0) {
346
+ options.push(`enum: ${JSON.stringify(enumValues)}`);
347
+ }
348
+ if (options.length > 0) {
349
+ return `Type.String({ ${options.join(", ")} })`;
350
+ }
351
+ return "Type.String()";
352
+ }
353
+
354
+ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
355
+ let schemaExpression = "Type.Any()";
356
+ const typeKind = String(column.typeKind || "");
357
+ if (typeKind === "string") {
358
+ schemaExpression = renderStringSchema(column, { forOutput });
359
+ } else if (typeKind === "integer") {
360
+ schemaExpression = renderIntegerSchema(column);
361
+ } else if (typeKind === "number") {
362
+ schemaExpression = "Type.Number()";
363
+ } else if (typeKind === "boolean") {
364
+ schemaExpression = "Type.Boolean()";
365
+ } else if (typeKind === "datetime") {
366
+ schemaExpression = 'Type.String({ format: "date-time", minLength: 1 })';
367
+ } else if (typeKind === "date") {
368
+ schemaExpression = 'Type.String({ format: "date", minLength: 1 })';
369
+ } else if (typeKind === "time") {
370
+ schemaExpression = 'Type.String({ format: "time", minLength: 1 })';
371
+ } else if (typeKind === "json") {
372
+ schemaExpression = "Type.Any()";
373
+ }
374
+
375
+ if (column.nullable === true) {
376
+ return `Type.Union([${schemaExpression}, Type.Null()])`;
377
+ }
378
+ return schemaExpression;
379
+ }
380
+
381
+ function renderInputNormalizer(column) {
382
+ const typeKind = String(column.typeKind || "");
383
+ if (typeKind === "string" || typeKind === "time") {
384
+ return "normalizeText";
385
+ }
386
+ if (typeKind === "integer") {
387
+ return "normalizeFiniteInteger";
388
+ }
389
+ if (typeKind === "number") {
390
+ return "normalizeFiniteNumber";
391
+ }
392
+ if (typeKind === "boolean") {
393
+ return "normalizeBoolean";
394
+ }
395
+ if (typeKind === "datetime") {
396
+ return "toDatabaseDateTimeUtc";
397
+ }
398
+ if (typeKind === "date") {
399
+ return "(value) => toIsoString(value).slice(0, 10)";
400
+ }
401
+ if (typeKind === "json") {
402
+ return "(value) => parseJsonValue(value, null, { fallback: null, allowNull: true })";
403
+ }
404
+ return "(value) => value";
405
+ }
406
+
407
+ function renderOutputNormalizerExpression(column) {
408
+ const typeKind = String(column.typeKind || "");
409
+ if (typeKind === "string" || typeKind === "time") {
410
+ return "normalizeText";
411
+ }
412
+ if (typeKind === "integer") {
413
+ return "normalizeFiniteInteger";
414
+ }
415
+ if (typeKind === "number") {
416
+ return "normalizeFiniteNumber";
417
+ }
418
+ if (typeKind === "boolean") {
419
+ return "normalizeBoolean";
420
+ }
421
+ if (typeKind === "datetime") {
422
+ return "toIsoString";
423
+ }
424
+ if (typeKind === "date") {
425
+ return "(value) => toIsoString(value).slice(0, 10)";
426
+ }
427
+ if (typeKind === "json") {
428
+ return "(value) => parseJsonValue(value, null, { fallback: null, allowNull: true })";
429
+ }
430
+ return "";
431
+ }
432
+
433
+ function renderResourceSchemaPropertyLines(columns, { forOutput = false } = {}) {
434
+ const sourceColumns = Array.isArray(columns) ? columns : [];
435
+ return sourceColumns
436
+ .map((column) => {
437
+ const key = renderObjectPropertyKey(column.key);
438
+ const schemaExpression = renderResourceFieldSchema(column, { forOutput });
439
+ return ` ${key}: ${schemaExpression},`;
440
+ })
441
+ .join("\n");
442
+ }
443
+
444
+ function renderResourceInputNormalizationLines(columns) {
445
+ const sourceColumns = Array.isArray(columns) ? columns : [];
446
+ return sourceColumns
447
+ .map((column) => {
448
+ const keyLiteral = JSON.stringify(String(column.key || ""));
449
+ const normalizer = renderInputNormalizer(column);
450
+ return ` normalizeIfInSource(source, normalized, ${keyLiteral}, ${normalizer});`;
451
+ })
452
+ .join("\n");
453
+ }
454
+
455
+ function renderResourceOutputNormalizationLines(columns) {
456
+ const sourceColumns = Array.isArray(columns) ? columns : [];
457
+ return sourceColumns
458
+ .map((column) => {
459
+ const key = renderObjectPropertyKey(column.key);
460
+ const sourceAccess = renderPropertyAccess("source", column.key);
461
+ const normalizer = renderOutputNormalizerExpression(column);
462
+ if (!normalizer) {
463
+ return ` ${key}: ${sourceAccess},`;
464
+ }
465
+ const nullishNormalizer = column.nullable === true ? "normalizeOrNull" : "normalizeIfPresent";
466
+ return ` ${key}: ${nullishNormalizer}(${sourceAccess}, ${normalizer}),`;
467
+ })
468
+ .join("\n");
469
+ }
470
+
471
+ function renderResourceDatabaseRuntimeImport({ needsToIsoString = false, needsToDatabaseDateTimeUtc = false } = {}) {
472
+ const imports = [];
473
+ if (needsToIsoString) {
474
+ imports.push("toIsoString");
475
+ }
476
+ if (needsToDatabaseDateTimeUtc) {
477
+ imports.push("toDatabaseDateTimeUtc");
478
+ }
479
+ if (imports.length < 1) {
480
+ return "";
481
+ }
482
+ return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/database-runtime/shared";`;
483
+ }
484
+
485
+ function renderResourceJsonImport({ needsJson = false } = {}) {
486
+ if (!needsJson) {
487
+ return "";
488
+ }
489
+ return 'import { parseJsonValue } from "@jskit-ai/database-runtime/shared/repositoryOptions";';
490
+ }
491
+
492
+ function renderResourceNormalizeSupportImport({
493
+ needsNormalizeText = false,
494
+ needsNormalizeBoolean = false,
495
+ needsNormalizeFiniteNumber = false,
496
+ needsNormalizeFiniteInteger = false,
497
+ needsNormalizeIfInSource = false,
498
+ needsNormalizeIfPresent = false,
499
+ needsNormalizeOrNull = false
500
+ } = {}) {
501
+ const imports = [];
502
+ if (needsNormalizeText) {
503
+ imports.push("normalizeText");
504
+ }
505
+ if (needsNormalizeBoolean) {
506
+ imports.push("normalizeBoolean");
507
+ }
508
+ if (needsNormalizeFiniteNumber) {
509
+ imports.push("normalizeFiniteNumber");
510
+ }
511
+ if (needsNormalizeFiniteInteger) {
512
+ imports.push("normalizeFiniteInteger");
513
+ }
514
+ if (needsNormalizeIfInSource) {
515
+ imports.push("normalizeIfInSource");
516
+ }
517
+ if (needsNormalizeIfPresent) {
518
+ imports.push("normalizeIfPresent");
519
+ }
520
+ if (needsNormalizeOrNull) {
521
+ imports.push("normalizeOrNull");
522
+ }
523
+ if (imports.length < 1) {
524
+ return "";
525
+ }
526
+ return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/support/normalize";`;
527
+ }
528
+
529
+ function renderMigrationDefaultClause(column) {
530
+ if (column.hasDefault !== true) {
531
+ return "";
532
+ }
533
+
534
+ const rawDefault = column.defaultValue;
535
+ if (rawDefault == null) {
536
+ return "";
537
+ }
538
+ const normalized = String(rawDefault).trim();
539
+ if (!normalized) {
540
+ return '.defaultTo("")';
541
+ }
542
+
543
+ const normalizedLower = normalized.toLowerCase();
544
+ if (normalizedLower === "null") {
545
+ return "";
546
+ }
547
+ const extraLower = String(column.extra || "").toLowerCase();
548
+ if (normalizedLower === "current_timestamp" || normalizedLower === "current_timestamp()") {
549
+ if (extraLower.includes("on update current_timestamp")) {
550
+ return '.defaultTo(knex.raw("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"))';
551
+ }
552
+ return ".defaultTo(knex.fn.now())";
553
+ }
554
+
555
+ if (column.typeKind === "boolean") {
556
+ if (normalizedLower === "1" || normalizedLower === "true") {
557
+ return ".defaultTo(true)";
558
+ }
559
+ if (normalizedLower === "0" || normalizedLower === "false") {
560
+ return ".defaultTo(false)";
561
+ }
562
+ }
563
+
564
+ if (column.typeKind === "integer" || column.typeKind === "number") {
565
+ const parsed = Number(normalized);
566
+ if (Number.isFinite(parsed)) {
567
+ return `.defaultTo(${parsed})`;
568
+ }
569
+ }
570
+
571
+ return `.defaultTo(${JSON.stringify(rawDefault)})`;
572
+ }
573
+
574
+ function renderMigrationColumnLine(column, { idColumn = DEFAULT_ID_COLUMN, primaryKeyColumns = [] } = {}) {
575
+ const isPrimary = Array.isArray(primaryKeyColumns) && primaryKeyColumns.includes(column.name);
576
+ const isIdColumn = column.name === idColumn;
577
+
578
+ if (isIdColumn && column.autoIncrement) {
579
+ let line = `table.increments(${JSON.stringify(column.name)})`;
580
+ if (column.unsigned) {
581
+ line += ".unsigned()";
582
+ }
583
+ line += ".primary();";
584
+ return line;
585
+ }
586
+
587
+ let line = "";
588
+ const nameLiteral = JSON.stringify(column.name);
589
+ const dataType = String(column.dataType || "").toLowerCase();
590
+
591
+ if (dataType === "varchar") {
592
+ const maxLength = Number.isFinite(column.maxLength) ? column.maxLength : 255;
593
+ line = `table.string(${nameLiteral}, ${maxLength})`;
594
+ } else if (dataType === "char") {
595
+ line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "char(255)")})`;
596
+ } else if (dataType === "text") {
597
+ line = `table.text(${nameLiteral})`;
598
+ } else if (dataType === "tinytext" || dataType === "mediumtext" || dataType === "longtext") {
599
+ line = `table.text(${nameLiteral}, ${JSON.stringify(dataType)})`;
600
+ } else if (dataType === "enum") {
601
+ const enumValues = Array.isArray(column.enumValues) ? column.enumValues : [];
602
+ line = `table.enu(${nameLiteral}, ${JSON.stringify(enumValues)})`;
603
+ } else if (dataType === "set") {
604
+ line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || "set")})`;
605
+ } else if (column.typeKind === "boolean") {
606
+ line = `table.boolean(${nameLiteral})`;
607
+ } else if (dataType === "int" || dataType === "integer") {
608
+ line = `table.integer(${nameLiteral})`;
609
+ } else if (dataType === "smallint") {
610
+ line = `table.smallint(${nameLiteral})`;
611
+ } else if (dataType === "bigint") {
612
+ line = `table.bigInteger(${nameLiteral})`;
613
+ } else if (dataType === "mediumint" || dataType === "tinyint") {
614
+ line = `table.specificType(${nameLiteral}, ${JSON.stringify(column.columnType || dataType)})`;
615
+ } else if (dataType === "decimal" || dataType === "numeric") {
616
+ if (Number.isFinite(column.numericPrecision) && Number.isFinite(column.numericScale)) {
617
+ line = `table.decimal(${nameLiteral}, ${column.numericPrecision}, ${column.numericScale})`;
618
+ } else if (Number.isFinite(column.numericPrecision)) {
619
+ line = `table.decimal(${nameLiteral}, ${column.numericPrecision})`;
620
+ } else {
621
+ line = `table.decimal(${nameLiteral})`;
622
+ }
623
+ } else if (dataType === "float") {
624
+ line = `table.float(${nameLiteral})`;
625
+ } else if (dataType === "double" || dataType === "real") {
626
+ line = `table.double(${nameLiteral})`;
627
+ } else if (dataType === "json") {
628
+ line = `table.json(${nameLiteral})`;
629
+ } else if (dataType === "date") {
630
+ line = `table.date(${nameLiteral})`;
631
+ } else if (dataType === "time") {
632
+ line = `table.time(${nameLiteral})`;
633
+ } else if (dataType === "datetime") {
634
+ line = `table.dateTime(${nameLiteral})`;
635
+ } else if (dataType === "timestamp") {
636
+ line = `table.timestamp(${nameLiteral})`;
637
+ } else {
638
+ throw new Error(
639
+ `Unsupported MySQL type "${dataType}" in migration renderer for column "${column.name}".`
640
+ );
641
+ }
642
+
643
+ if (column.unsigned && (line.includes(".integer(") || line.includes(".smallint(") || line.includes(".bigInteger("))) {
644
+ line += ".unsigned()";
645
+ }
646
+ line += column.nullable ? ".nullable()" : ".notNullable()";
647
+ line += renderMigrationDefaultClause(column);
648
+ if (isPrimary) {
649
+ line += ".primary()";
650
+ }
651
+ line += ";";
652
+ return line;
653
+ }
654
+
655
+ function renderMigrationColumnLines(snapshot) {
656
+ const columns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
657
+ const lines = columns.map((column) =>
658
+ ` ${renderMigrationColumnLine(column, {
659
+ idColumn: snapshot.idColumn,
660
+ primaryKeyColumns: snapshot.primaryKeyColumns
661
+ })}`
662
+ );
663
+ return lines.join("\n");
664
+ }
665
+
666
+ function renderMigrationIndexLine(index) {
667
+ const columns = Array.isArray(index?.columns) ? index.columns.filter(Boolean) : [];
668
+ if (columns.length < 1) {
669
+ return "";
670
+ }
671
+
672
+ const columnsLiteral = JSON.stringify(columns);
673
+ const indexName = normalizeText(index?.name);
674
+ if (index?.unique === true) {
675
+ if (indexName) {
676
+ return ` table.unique(${columnsLiteral}, ${JSON.stringify(indexName)});`;
677
+ }
678
+ return ` table.unique(${columnsLiteral});`;
679
+ }
680
+
681
+ if (indexName) {
682
+ return ` table.index(${columnsLiteral}, ${JSON.stringify(indexName)});`;
683
+ }
684
+ return ` table.index(${columnsLiteral});`;
685
+ }
686
+
687
+ function renderMigrationIndexLines(snapshot) {
688
+ const indexes = Array.isArray(snapshot.indexes) ? snapshot.indexes : [];
689
+ const lines = indexes
690
+ .map((index) => renderMigrationIndexLine(index))
691
+ .filter(Boolean);
692
+ return lines.join("\n");
693
+ }
694
+
695
+ function buildReplacementsFromSnapshot({
696
+ namespace,
697
+ snapshot,
698
+ resolvedOwnershipFilter
699
+ }) {
700
+ const scaffoldColumns = resolveScaffoldColumns(snapshot);
701
+ const outputColumns = scaffoldColumns.filter((column) => !column.isOwnerColumn);
702
+ const writableColumns = scaffoldColumns.filter((column) => column.writable);
703
+ const createRequiredFieldKeys = writableColumns
704
+ .filter((column) => !column.nullable && column.hasDefault !== true)
705
+ .map((column) => column.key);
706
+ const resourceColumns = [...outputColumns, ...writableColumns];
707
+
708
+ const outputKeys = outputColumns.map((column) => column.key);
709
+ const writeKeys = writableColumns.map((column) => column.key);
710
+ const columnOverrides = {};
711
+ const seenOverrideKeys = new Set();
712
+ for (const column of [...outputColumns, ...writableColumns]) {
713
+ const key = column.key;
714
+ if (!key || seenOverrideKeys.has(key)) {
715
+ continue;
716
+ }
717
+ seenOverrideKeys.add(key);
718
+ const guessedColumn = toSnakeCase(key);
719
+ const actualColumn = column.name;
720
+ if (typeof actualColumn === "string" && actualColumn && actualColumn !== guessedColumn) {
721
+ columnOverrides[key] = actualColumn;
722
+ }
723
+ }
724
+ const createdAtColumn = scaffoldColumns.find((column) => column.isCreatedAtColumn)?.name || "";
725
+ const updatedAtColumn = scaffoldColumns.find((column) => column.isUpdatedAtColumn)?.name || "";
726
+ const needsFiniteInteger = resourceColumns.some((column) => column.typeKind === "integer");
727
+ const needsFiniteNumber = resourceColumns.some((column) => column.typeKind === "number");
728
+ const needsDateTimeOutput = outputColumns.some((column) => column.typeKind === "datetime");
729
+ const needsDateTimeInput = writableColumns.some((column) => column.typeKind === "datetime");
730
+ const needsDate = resourceColumns.some((column) => column.typeKind === "date");
731
+ const needsJson = resourceColumns.some((column) => column.typeKind === "json");
732
+ const needsNormalizeText = resourceColumns.some((column) =>
733
+ column.typeKind === "string" || column.typeKind === "time"
734
+ );
735
+ const needsNormalizeBoolean = resourceColumns.some((column) => column.typeKind === "boolean");
736
+ const needsNormalizeIfInSource = writableColumns.length > 0;
737
+ const outputColumnsWithNormalizer = outputColumns.filter(
738
+ (column) => Boolean(renderOutputNormalizerExpression(column))
739
+ );
740
+ const needsNormalizeIfPresent = outputColumnsWithNormalizer.some((column) => column.nullable !== true);
741
+ const needsNormalizeOrNull = outputColumnsWithNormalizer.some((column) => column.nullable === true);
742
+
743
+ const replacements = Object.freeze({
744
+ __JSKIT_CRUD_TABLE_NAME__: JSON.stringify(snapshot.tableName),
745
+ __JSKIT_CRUD_ID_COLUMN__: JSON.stringify(snapshot.idColumn || DEFAULT_ID_COLUMN),
746
+ __JSKIT_CRUD_RESOLVED_OWNERSHIP_FILTER__: resolvedOwnershipFilter,
747
+ __JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
748
+ needsToIsoString: needsDateTimeOutput || needsDate,
749
+ needsToDatabaseDateTimeUtc: needsDateTimeInput
750
+ }),
751
+ __JSKIT_CRUD_RESOURCE_NORMALIZE_SUPPORT_IMPORT__: renderResourceNormalizeSupportImport({
752
+ needsNormalizeText,
753
+ needsNormalizeBoolean,
754
+ needsNormalizeFiniteNumber: needsFiniteNumber,
755
+ needsNormalizeFiniteInteger: needsFiniteInteger,
756
+ needsNormalizeIfInSource,
757
+ needsNormalizeIfPresent,
758
+ needsNormalizeOrNull
759
+ }),
760
+ __JSKIT_CRUD_RESOURCE_JSON_IMPORT__: renderResourceJsonImport({
761
+ needsJson
762
+ }),
763
+ __JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(outputColumns, {
764
+ forOutput: true
765
+ }),
766
+ __JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__: renderResourceSchemaPropertyLines(writableColumns, {
767
+ forOutput: false
768
+ }),
769
+ __JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__: renderResourceInputNormalizationLines(writableColumns),
770
+ __JSKIT_CRUD_RESOURCE_OUTPUT_NORMALIZATION_LINES__: renderResourceOutputNormalizationLines(outputColumns),
771
+ __JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__: JSON.stringify(createRequiredFieldKeys),
772
+ __JSKIT_CRUD_REPOSITORY_OUTPUT_KEYS__: JSON.stringify(outputKeys),
773
+ __JSKIT_CRUD_REPOSITORY_WRITE_KEYS__: JSON.stringify(writeKeys),
774
+ __JSKIT_CRUD_REPOSITORY_COLUMN_OVERRIDES__: JSON.stringify(columnOverrides),
775
+ __JSKIT_CRUD_REPOSITORY_CREATED_AT_COLUMN__: JSON.stringify(createdAtColumn),
776
+ __JSKIT_CRUD_REPOSITORY_UPDATED_AT_COLUMN__: JSON.stringify(updatedAtColumn),
777
+ __JSKIT_CRUD_MIGRATION_COLUMN_LINES__: renderMigrationColumnLines(snapshot),
778
+ __JSKIT_CRUD_MIGRATION_INDEX_LINES__: renderMigrationIndexLines(snapshot)
779
+ });
780
+
781
+ return replacements;
782
+ }
783
+
784
+ async function resolveGenerationSnapshot({
785
+ appRoot,
786
+ tableName,
787
+ idColumnOption
788
+ } = {}) {
789
+ const resolvedTableName = normalizeText(tableName);
790
+ if (!resolvedTableName) {
791
+ throw new Error('crud template context requires option "table-name".');
792
+ }
793
+ const idColumn = normalizeText(idColumnOption) || DEFAULT_ID_COLUMN;
794
+ return resolveMysqlSnapshotFromDatabase({
795
+ appRoot,
796
+ tableName: resolvedTableName,
797
+ idColumn
798
+ });
799
+ }
800
+
801
+ function createCacheKey({ appRoot, options }) {
802
+ const payload = {
803
+ appRoot: path.resolve(String(appRoot || "")),
804
+ options: {
805
+ namespace: normalizeText(options?.namespace),
806
+ ownershipFilter: normalizeText(options?.["ownership-filter"]),
807
+ tableName: normalizeText(options?.["table-name"]),
808
+ idColumn: normalizeText(options?.["id-column"])
809
+ }
810
+ };
811
+
812
+ return JSON.stringify(payload);
813
+ }
814
+
815
+ async function buildCrudTemplateContext(input = {}) {
816
+ const source = asRecord(input);
817
+ const appRoot = path.resolve(String(source.appRoot || ""));
818
+ const options = asRecord(source.options);
819
+ const namespace = normalizeText(options.namespace);
820
+ if (!namespace) {
821
+ throw new Error('crud template context requires option "namespace".');
822
+ }
823
+ const tableName = normalizeText(options["table-name"]);
824
+ if (!tableName) {
825
+ throw new Error('crud template context requires option "table-name".');
826
+ }
827
+ const snapshot = await resolveGenerationSnapshot({
828
+ appRoot,
829
+ tableName,
830
+ idColumnOption: options["id-column"]
831
+ });
832
+
833
+ const resolvedOwnershipFilter = resolveOwnershipFilterForGeneration(
834
+ snapshot,
835
+ options["ownership-filter"],
836
+ {
837
+ enforceTableColumns: true
838
+ }
839
+ );
840
+
841
+ return buildReplacementsFromSnapshot({
842
+ namespace,
843
+ snapshot,
844
+ resolvedOwnershipFilter
845
+ });
846
+ }
847
+
848
+ async function buildTemplateContext(input = {}) {
849
+ const cacheKey = createCacheKey({
850
+ appRoot: input?.appRoot,
851
+ options: input?.options
852
+ });
853
+ if (scaffoldCache.has(cacheKey)) {
854
+ return scaffoldCache.get(cacheKey);
855
+ }
856
+
857
+ const replacements = await buildCrudTemplateContext(input);
858
+ scaffoldCache.set(cacheKey, replacements);
859
+ return replacements;
860
+ }
861
+
862
+ const __testables = Object.freeze({
863
+ normalizeRequestedOwnershipFilter,
864
+ inferOwnershipFilterFromSnapshot,
865
+ resolveOwnershipFilterForGeneration,
866
+ buildReplacementsFromSnapshot,
867
+ parseDotEnvLine,
868
+ renderMigrationColumnLine
869
+ });
870
+
871
+ export { buildTemplateContext, __testables };