@mandujs/core 0.53.1 → 0.53.2

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.
@@ -35,7 +35,7 @@
35
35
  * return {
36
36
  * async findById(id): User | null { ... },
37
37
  * async findMany(limit = 100, offset = 0): User[] { ... },
38
- * async create(row): User { ... }, // PG/SQLite: RETURNING *; MySQL: INSERT + SELECT
38
+ * async create(row): User { ... }, // PG/SQLite: RETURNING *; MySQL: INSERT + re-select
39
39
  * async update(id, patch): User | null { ... },
40
40
  * async delete(id): boolean { ... },
41
41
  * };
@@ -65,7 +65,7 @@ import type { ParsedResource } from "./parser";
65
65
  import type { ResourceField } from "./schema";
66
66
  import { quoteIdent } from "./ddl/emit";
67
67
  import { toSnakeCase } from "./ddl/snapshot";
68
- import { asPersistence, type ExtendedResourcePersistence } from "./ddl/persistence-types";
68
+ import { asPersistence, type ExtendedResourcePersistence, type FieldOverride } from "./ddl/persistence-types";
69
69
  import type { SqlProvider } from "./ddl/types";
70
70
 
71
71
  // ============================================
@@ -172,9 +172,10 @@ function renderRepoFile(
172
172
  const primaryKeyColumn = columns.find((c) => c.primary)?.column ?? "id";
173
173
  const primaryKeyField = columns.find((c) => c.primary)?.field ?? "id";
174
174
  const insertColumns = columns.filter((c) => !c.omitOnInsert);
175
+ const createInputType = createMethodInputType(pascalName, columns);
175
176
 
176
177
  // Quoted identifiers — precomputed so each builder reads cleanly.
177
- const q = (name: string) => quoteIdent(name, provider);
178
+ const q = (name: string) => escapeSqlTemplateText(quoteIdent(name, provider));
178
179
  const qTable = q(table);
179
180
 
180
181
  // Column list for SELECT: we don't use `*` because aliasing
@@ -194,7 +195,7 @@ function renderRepoFile(
194
195
  const body = [
195
196
  findByIdMethod(pascalName, qTable, selectList, q, primaryKeyColumn, primaryKeyField),
196
197
  findManyMethod(pascalName, qTable, selectList),
197
- createMethod(pascalName, table, qTable, selectList, insertColumns, primaryKeyColumn, primaryKeyField, q, provider),
198
+ createMethod(pascalName, table, qTable, selectList, insertColumns, createInputType, primaryKeyColumn, primaryKeyField, q, provider),
198
199
  updateMethod(pascalName, table, qTable, selectList, columns, primaryKeyColumn, primaryKeyField, q, provider),
199
200
  deleteMethod(pascalName, qTable, q, primaryKeyColumn, primaryKeyField, provider),
200
201
  ].join(",\n\n");
@@ -232,7 +233,7 @@ function renderHeader(
232
233
  // - The repo never constructs a Db itself — callers (slots, scripts) pass
233
234
  // in either \`ctx.deps.db\` or a module-level singleton.
234
235
  // - Provider-specific SQL (INSERT ... RETURNING * on PG/SQLite vs
235
- // INSERT + SELECT LAST_INSERT_ID on MySQL) is resolved at generation
236
+ // INSERT + re-select on MySQL) is resolved at generation
236
237
  // time; the generated file has one code path per provider.
237
238
  `;
238
239
  }
@@ -348,6 +349,7 @@ function createMethod(
348
349
  qTable: string,
349
350
  selectList: string,
350
351
  insertColumns: ResolvedColumn[],
352
+ createInputType: string,
351
353
  pkColumn: string,
352
354
  pkField: string,
353
355
  q: (n: string) => string,
@@ -371,7 +373,7 @@ function createMethod(
371
373
  * Insert a new row and return the inserted record. Errors from
372
374
  * constraint violations (UNIQUE, NOT NULL, FK) bubble unchanged.
373
375
  */
374
- async create(input: Omit<${pascalName}, "${pkField}">): Promise<${pascalName}> {
376
+ async create(input: ${createInputType}): Promise<${pascalName}> {
375
377
  const row = await db.one<${pascalName}>\`
376
378
  INSERT INTO ${qTable} (${columnList})
377
379
  VALUES (${valuePlaceholders})
@@ -382,17 +384,20 @@ function createMethod(
382
384
  }`;
383
385
  }
384
386
 
385
- // MySQL: no RETURNING. INSERT then SELECT by LAST_INSERT_ID() OR by the
386
- // primary key if caller provided one. For v1 LCD we use a post-INSERT
387
- // SELECT that matches on the primary-key the caller supplied; if the
388
- // caller omitted it (AUTO_INCREMENT), we fall back to LAST_INSERT_ID().
387
+ const mysqlLookup = insertColumns.some((c) => c.field === pkField)
388
+ ? `${q(pkColumn)} = \${input.${pkField}}`
389
+ : `${q(pkColumn)} = LAST_INSERT_ID()`;
390
+
391
+ // MySQL: no RETURNING. INSERT then SELECT by the supplied primary key when
392
+ // the caller provides it; if the primary key is DB-generated, fall back to
393
+ // LAST_INSERT_ID().
389
394
  return ` /**
390
395
  * Insert a new row and return the inserted record via a follow-up SELECT.
391
396
  * MySQL lacks RETURNING; the generator uses LAST_INSERT_ID() when the
392
397
  * primary key is server-generated, otherwise it re-selects by the
393
398
  * provided primary key value.
394
399
  */
395
- async create(input: Omit<${pascalName}, "${pkField}">): Promise<${pascalName}> {
400
+ async create(input: ${createInputType}): Promise<${pascalName}> {
396
401
  await db\`
397
402
  INSERT INTO ${qTable} (${columnList})
398
403
  VALUES (${valuePlaceholders})
@@ -400,7 +405,7 @@ function createMethod(
400
405
  const row = await db.one<${pascalName}>\`
401
406
  SELECT ${selectList}
402
407
  FROM ${qTable}
403
- WHERE ${q(pkColumn)} = LAST_INSERT_ID()
408
+ WHERE ${mysqlLookup}
404
409
  \`;
405
410
  if (!row) throw new Error("${tableName} create: follow-up SELECT returned no row");
406
411
  return row;
@@ -541,14 +546,7 @@ function resolveColumns(
541
546
  const primary = declaredPkMatch || fieldLevelPk;
542
547
  if (primary) pkCount++;
543
548
 
544
- // Omit PK from INSERT list only when:
545
- // - it's a UUID with a DB-side default (we don't know this at
546
- // generation time for all providers); OR
547
- // - it's an integer and the user didn't explicitly opt in.
548
- // Safe default: let the user supply the PK. The generated
549
- // `Omit<T, pkField>` on `create` means the caller CAN'T pass it
550
- // anyway, so we omit PK from the insert column list unconditionally.
551
- const omitOnInsert = primary;
549
+ const omitOnInsert = primary && hasDbDefault(field, override);
552
550
 
553
551
  columns.push({ field: fieldKey, column, primary, omitOnInsert });
554
552
  }
@@ -575,6 +573,28 @@ function resolveDeclaredPrimaryKey(
575
573
  return declared[0];
576
574
  }
577
575
 
576
+ function createMethodInputType(
577
+ pascalName: string,
578
+ columns: ResolvedColumn[],
579
+ ): string {
580
+ const omittedFields = columns
581
+ .filter((c) => c.omitOnInsert)
582
+ .map((c) => JSON.stringify(c.field));
583
+ if (omittedFields.length === 0) return pascalName;
584
+ return `Omit<${pascalName}, ${omittedFields.join(" | ")}>`;
585
+ }
586
+
587
+ function hasDbDefault(
588
+ field: ResourceField,
589
+ override: FieldOverride | undefined,
590
+ ): boolean {
591
+ return override?.default !== undefined || field.default !== undefined;
592
+ }
593
+
594
+ function escapeSqlTemplateText(text: string): string {
595
+ return text.replace(/`/g, "\\`");
596
+ }
597
+
578
598
  function resolveTableName(
579
599
  resource: ParsedResource,
580
600
  persistence: ExtendedResourcePersistence,