@c9up/atlas 0.2.7 → 0.3.1

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.
@@ -24,6 +24,7 @@ import type {
24
24
  import { REPO_REF } from "./BaseEntity.js";
25
25
  import {
26
26
  type DateColumnConfig,
27
+ defaultRelationForeignKey,
27
28
  ensureEntityMetadata,
28
29
  getColumnMetadata,
29
30
  getDateColumnConfig,
@@ -413,7 +414,8 @@ export class BaseRepository<T extends BaseEntity> {
413
414
  const related = rel.target();
414
415
  if (rel.type === "belongsTo") {
415
416
  // FK lives on THIS table, references the related (owner) PK.
416
- const fk = rel.foreignKey ?? `${camelToSnake(related.name)}_id`;
417
+ const fk =
418
+ rel.foreignKey ?? defaultRelationForeignKey("hasMany", related);
417
419
  const ownerKey = rel.ownerKey ?? getPrimaryKey(related) ?? "id";
418
420
  const ownerDb =
419
421
  getColumnMetadata(related).find((c) => c.propertyKey === ownerKey)
@@ -422,7 +424,8 @@ export class BaseRepository<T extends BaseEntity> {
422
424
  if (cast) registerColumnCast(this.#tableName, fk, cast);
423
425
  } else {
424
426
  // hasOne / hasMany: FK lives on the RELATED table, references THIS PK.
425
- const fk = rel.foreignKey ?? `${camelToSnake(entityClass.name)}_id`;
427
+ const fk =
428
+ rel.foreignKey ?? defaultRelationForeignKey("hasMany", entityClass);
426
429
  const localKey = rel.localKey ?? this.#primaryKey;
427
430
  const cast = this.#castTypes[this.#dbColumn(localKey)];
428
431
  // Boot the related model on demand (Lucid lazy-boot): a related model
@@ -660,7 +663,7 @@ export class BaseRepository<T extends BaseEntity> {
660
663
  * the specific hook runs before the general `beforeSave`).
661
664
  *
662
665
  * An unknown key throws by default (Lucid); pass `{ allowExtraProperties: true }`
663
- * to drop unknown keys instead. A bare boolean is the legacy `quiet` flag.
666
+ * to keep it in `$extras` instead. A bare boolean is the legacy `quiet` flag.
664
667
  */
665
668
  async create(
666
669
  data: Partial<Record<string, unknown>>,
@@ -681,14 +684,20 @@ export class BaseRepository<T extends BaseEntity> {
681
684
  } else if (key.startsWith("$")) {
682
685
  // Framework-internal (`$extras`, `$trx`, …) — leaks in when an entity
683
686
  // instance is passed as data. Not a user column and not a typo; skip.
684
- } else if (!allowExtraProperties) {
687
+ } else if (allowExtraProperties) {
688
+ // Kept, not dropped: Lucid puts an unknown key in `$extras`
689
+ // (`this.$extras[key] = value`), which is where an aggregate or a
690
+ // pivot value travelling with the payload belongs. It is not a
691
+ // declared column, so it can never reach the INSERT.
692
+ entity.$extras[key] = value;
693
+ } else {
685
694
  // Adonis Lucid throws on an unknown property by default (a typo'd or
686
695
  // stray key is a bug, not something to silently drop). Opt out with
687
696
  // `create(data, { allowExtraProperties: true })`.
688
697
  throw new AtlasError(
689
698
  "E_UNKNOWN_COLUMN",
690
699
  `Cannot assign '${key}' — it is not a column on ${this.#entityClass.name}. ` +
691
- "Pass { allowExtraProperties: true } to drop unknown keys instead.",
700
+ "Pass { allowExtraProperties: true } to keep unknown keys in $extras.",
692
701
  );
693
702
  }
694
703
  }
@@ -2163,8 +2172,8 @@ export class BaseRepository<T extends BaseEntity> {
2163
2172
  const fkCol =
2164
2173
  relation.foreignKey ??
2165
2174
  (relation.type === "belongsTo"
2166
- ? `${camelToSnake(relatedClass.name)}_id`
2167
- : `${camelToSnake(this.#entityClass.name)}_id`);
2175
+ ? defaultRelationForeignKey("belongsTo", relatedClass)
2176
+ : defaultRelationForeignKey("hasMany", this.#entityClass));
2168
2177
  const fkProp = snakeToCamel(fkCol);
2169
2178
 
2170
2179
  const injectFk = (
@@ -2363,9 +2372,11 @@ export class BaseRepository<T extends BaseEntity> {
2363
2372
  throw new Error(`@ManyToMany ${relationName} requires pivot options`);
2364
2373
  const pivot = relation.pivot;
2365
2374
  const pivotFk =
2366
- pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
2375
+ pivot.foreignKey ??
2376
+ defaultRelationForeignKey("manyToMany", this.#entityClass);
2367
2377
  const pivotOther =
2368
- pivot.otherKey ?? `${camelToSnake(relatedClass.name)}_id`;
2378
+ pivot.otherKey ??
2379
+ defaultRelationForeignKey("manyToMany", relatedClass);
2369
2380
  // Resolve the related PK to its DB column (multi-word / columnName),
2370
2381
  // mirroring the eager-preload fix — a raw property name here targets
2371
2382
  // the wrong column in the correlated EXISTS.
@@ -2463,9 +2474,11 @@ export class BaseRepository<T extends BaseEntity> {
2463
2474
  const parentLocal =
2464
2475
  relation.localKey ?? getPrimaryKey(this.#entityClass) ?? "id";
2465
2476
  const firstKey =
2466
- relation.firstKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
2477
+ relation.firstKey ??
2478
+ defaultRelationForeignKey("hasMany", this.#entityClass);
2467
2479
  const secondKey =
2468
- relation.secondKey ?? `${camelToSnake(throughClass.name)}_id`;
2480
+ relation.secondKey ??
2481
+ defaultRelationForeignKey("hasMany", throughClass);
2469
2482
  const secondLocal = relation.secondLocalKey ?? throughPk;
2470
2483
  q.whereIn(
2471
2484
  secondKey,
@@ -2607,9 +2620,10 @@ export class BaseRepository<T extends BaseEntity> {
2607
2620
  const pivot = relation.pivot;
2608
2621
  const pivotTable = pivot.pivotTable;
2609
2622
  const pivotFk =
2610
- pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
2623
+ pivot.foreignKey ??
2624
+ defaultRelationForeignKey("manyToMany", this.#entityClass);
2611
2625
  const pivotOther =
2612
- pivot.otherKey ?? `${camelToSnake(relatedClass.name)}_id`;
2626
+ pivot.otherKey ?? defaultRelationForeignKey("manyToMany", relatedClass);
2613
2627
  const tsConfig = pivot.pivotTimestamps;
2614
2628
  const pivotAdapters = pivot.pivotColumnAdapters;
2615
2629
  const dialect = this.#dialect;
package/src/ModelQuery.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  wrapAdapterError,
20
20
  } from "./BaseRepository.js";
21
21
  import {
22
+ defaultRelationForeignKey,
22
23
  ensureEntityMetadata,
23
24
  getColumnMetadata,
24
25
  getDateColumnConfig,
@@ -750,6 +751,22 @@ function structuredCloneSafe<T>(value: T): T {
750
751
  return structuredClone(value);
751
752
  }
752
753
 
754
+ /**
755
+ * What {@link ModelQuery.pojo} hands back: awaitable for the rows, and
756
+ * chainable into `first()`.
757
+ *
758
+ * NAMED DEVIATION — Lucid's `pojo()` is a flag on the builder
759
+ * (`pojo(): this`), so the whole builder surface stays available after it.
760
+ * `ModelQuery<T>` is constrained to `T extends BaseEntity` and cannot be
761
+ * re-parametrised to a plain record, so ours is a terminal view instead. It
762
+ * covers what Lucid's own code does with it — `query.pojo().first()` — and
763
+ * `await query.pojo()` is unchanged.
764
+ */
765
+ export interface PojoQuery<R> extends PromiseLike<R[]> {
766
+ /** The first row, or `null`. */
767
+ first(): Promise<R | null>;
768
+ }
769
+
753
770
  export class ModelQuery<T extends BaseEntity> {
754
771
  #tableName: string;
755
772
  #db: DatabaseConnection;
@@ -3012,6 +3029,24 @@ export class ModelQuery<T extends BaseEntity> {
3012
3029
  return this.exec().then(onfulfilled, onrejected);
3013
3030
  }
3014
3031
 
3032
+ /**
3033
+ * The rest of the promise protocol, so a builder is not a half-promise.
3034
+ *
3035
+ * `await query` worked while `query.catch(fn)` and `query.finally(fn)` threw
3036
+ * "not a function" — a value that answers to `then` and nothing else
3037
+ * surprises anyone who treats it as the promise it looks like. Lucid's
3038
+ * builder carries all three.
3039
+ */
3040
+ catch<TResult = never>(
3041
+ onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null,
3042
+ ): Promise<T[] | TResult> {
3043
+ return this.exec().catch(onrejected);
3044
+ }
3045
+
3046
+ finally(onfinally?: (() => void) | null): Promise<T[]> {
3047
+ return this.exec().finally(onfinally);
3048
+ }
3049
+
3015
3050
  /** Build the spec object that gets sent to the Rust compiler. Extracted so whereHas can reuse it for sub-queries. */
3016
3051
  /**
3017
3052
  * DB column backing the soft-delete `deletedAt` property — honours a
@@ -3339,9 +3374,18 @@ export class ModelQuery<T extends BaseEntity> {
3339
3374
  * AdonisJS Lucid `pojo()`. Fast read path for reports/exports where model
3340
3375
  * instances aren't needed.
3341
3376
  */
3342
- async pojo<R = Record<string, unknown>>(): Promise<R[]> {
3343
- const { sql, params } = this.#compiledNative();
3344
- return this.#db.query<R>(sql, params);
3377
+ pojo<R = Record<string, unknown>>(): PojoQuery<R> {
3378
+ const run = async (limitOne: boolean): Promise<R[]> => {
3379
+ if (limitOne) this.#limit = 1;
3380
+ const { sql, params } = this.#compiledNative();
3381
+ return this.#db.query<R>(sql, params);
3382
+ };
3383
+ return {
3384
+ // biome-ignore lint/suspicious/noThenProperty: deliberately thenable, which is what PromiseLike means — the rule exists to catch ACCIDENTAL thenables. `await query.pojo()` is the existing API and must keep working; same justification as ModelQuery.then above.
3385
+ then: (onfulfilled, onrejected) =>
3386
+ run(false).then(onfulfilled, onrejected),
3387
+ first: async () => (await run(true))[0] ?? null,
3388
+ };
3345
3389
  }
3346
3390
 
3347
3391
  /**
@@ -3564,9 +3608,10 @@ export class ModelQuery<T extends BaseEntity> {
3564
3608
  const parentLocal =
3565
3609
  relation.localKey ?? getPrimaryKey(this.#entityClass) ?? "id";
3566
3610
  const firstKey =
3567
- relation.firstKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
3611
+ relation.firstKey ??
3612
+ defaultRelationForeignKey("hasMany", this.#entityClass);
3568
3613
  const secondKey =
3569
- relation.secondKey ?? `${camelToSnake(throughClass.name)}_id`;
3614
+ relation.secondKey ?? defaultRelationForeignKey("hasMany", throughClass);
3570
3615
  // secondLocal indexes the THROUGH row (`row[secondLocal]`), so it must be a
3571
3616
  // DB column — resolve the through model's key (default: its PK), honouring a
3572
3617
  // multi-word / columnName PK. (parentLocal stays a property: it's read off
@@ -3628,7 +3673,8 @@ export class ModelQuery<T extends BaseEntity> {
3628
3673
  ctx: PreloadContext,
3629
3674
  ): Promise<BaseEntity[]> {
3630
3675
  const fk =
3631
- ctx.relation.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
3676
+ ctx.relation.foreignKey ??
3677
+ defaultRelationForeignKey("hasMany", this.#entityClass);
3632
3678
  const pk =
3633
3679
  ctx.relation.localKey ?? getPrimaryKey(this.#entityClass) ?? "id";
3634
3680
  const ids = entities.map((e) => e[pk]).filter((v) => v != null);
@@ -3670,7 +3716,8 @@ export class ModelQuery<T extends BaseEntity> {
3670
3716
  ctx: PreloadContext,
3671
3717
  ): Promise<BaseEntity[]> {
3672
3718
  const fk =
3673
- ctx.relation.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
3719
+ ctx.relation.foreignKey ??
3720
+ defaultRelationForeignKey("hasMany", this.#entityClass);
3674
3721
  const pk =
3675
3722
  ctx.relation.localKey ?? getPrimaryKey(this.#entityClass) ?? "id";
3676
3723
  const ids = entities.map((e) => e[pk]).filter((v) => v != null);
@@ -3699,7 +3746,8 @@ export class ModelQuery<T extends BaseEntity> {
3699
3746
  ctx: PreloadContext,
3700
3747
  ): Promise<BaseEntity[]> {
3701
3748
  const fk =
3702
- ctx.relation.foreignKey ?? `${camelToSnake(ctx.relatedClass.name)}_id`;
3749
+ ctx.relation.foreignKey ??
3750
+ defaultRelationForeignKey("belongsTo", ctx.relatedClass);
3703
3751
  const fkProp = `${relationName}Id`;
3704
3752
  const ids = entities
3705
3753
  .map((e) => e[fkProp] ?? e[fk])
@@ -3739,9 +3787,11 @@ export class ModelQuery<T extends BaseEntity> {
3739
3787
  // singularize the plural TABLE name by stripping a trailing `s` — that breaks
3740
3788
  // on `status`/`address`/`campus` (→ `statu_id`). Explicit pivot keys win.
3741
3789
  const foreignKey =
3742
- pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
3790
+ pivot.foreignKey ??
3791
+ defaultRelationForeignKey("manyToMany", this.#entityClass);
3743
3792
  const otherKey =
3744
- pivot.otherKey ?? `${camelToSnake(ctx.relatedClass.name)}_id`;
3793
+ pivot.otherKey ??
3794
+ defaultRelationForeignKey("manyToMany", ctx.relatedClass);
3745
3795
  // The pivot FK stores `parent[localKey]` (default PK) — attach() writes it,
3746
3796
  // so preload MUST read back with the SAME key, else a custom-localKey m2m
3747
3797
  // writes `user_code = code` but reads `user_code IN (id)` and never matches.
@@ -4138,7 +4188,8 @@ export class ModelQuery<T extends BaseEntity> {
4138
4188
  // Honour custom foreignKey/localKey exactly like the eager loader —
4139
4189
  // hard-coding them here produced silently-wrong whereHas/withCount SQL.
4140
4190
  const fk =
4141
- relation.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
4191
+ relation.foreignKey ??
4192
+ defaultRelationForeignKey("hasMany", this.#entityClass);
4142
4193
  const localKey = resolveParent(relation.localKey ?? parentPk);
4143
4194
  sub.#pushWhereRaw(
4144
4195
  `${qTable(relatedTable)}.${q(fk)} = ${qTable(parentTable)}.${q(localKey)}`,
@@ -4147,7 +4198,8 @@ export class ModelQuery<T extends BaseEntity> {
4147
4198
  }
4148
4199
  case "belongsTo": {
4149
4200
  const fk =
4150
- relation.foreignKey ?? `${camelToSnake(relatedClass.name)}_id`;
4201
+ relation.foreignKey ??
4202
+ defaultRelationForeignKey("belongsTo", relatedClass);
4151
4203
  const ownerKey = buildColumnResolver(relatedClass)(
4152
4204
  relation.ownerKey ?? getPrimaryKey(relatedClass) ?? "id",
4153
4205
  );
@@ -4166,9 +4218,11 @@ export class ModelQuery<T extends BaseEntity> {
4166
4218
  // Default pivot FK from the CLASS name (singular), not the plural table
4167
4219
  // name stripped of a trailing `s` — see the eager loader above.
4168
4220
  const foreignKey =
4169
- pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
4221
+ pivot.foreignKey ??
4222
+ defaultRelationForeignKey("manyToMany", this.#entityClass);
4170
4223
  const otherKey =
4171
- pivot.otherKey ?? `${camelToSnake(relatedClass.name)}_id`;
4224
+ pivot.otherKey ??
4225
+ defaultRelationForeignKey("manyToMany", relatedClass);
4172
4226
  const relatedPkProp = getPrimaryKey(relatedClass) ?? "id";
4173
4227
  const relatedPk =
4174
4228
  getColumnMetadata(relatedClass).find(
@@ -4198,9 +4252,11 @@ export class ModelQuery<T extends BaseEntity> {
4198
4252
  const throughPk = getPrimaryKey(throughClass) ?? "id";
4199
4253
  const parentLocal = resolveParent(relation.localKey ?? parentPk);
4200
4254
  const firstKey =
4201
- relation.firstKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
4255
+ relation.firstKey ??
4256
+ defaultRelationForeignKey("hasMany", this.#entityClass);
4202
4257
  const secondKey =
4203
- relation.secondKey ?? `${camelToSnake(throughClass.name)}_id`;
4258
+ relation.secondKey ??
4259
+ defaultRelationForeignKey("hasMany", throughClass);
4204
4260
  const secondLocal = buildColumnResolver(throughClass)(
4205
4261
  relation.secondLocalKey ?? throughPk,
4206
4262
  );
@@ -4462,8 +4518,14 @@ export class ModelQuery<T extends BaseEntity> {
4462
4518
  return !(await this.exists());
4463
4519
  }
4464
4520
 
4465
- /** Flat column projection. Rejects object/relation columns. */
4466
- async pluck(column: string): Promise<unknown[]> {
4521
+ /**
4522
+ * Flat column projection (Knex `pluck`, which Lucid inherits). Rejects
4523
+ * object/relation columns.
4524
+ *
4525
+ * Generic like Knex's, so a caller who knows the column type does not have
4526
+ * to cast the whole array at the call site.
4527
+ */
4528
+ async pluck<V = unknown>(column: string): Promise<V[]> {
4467
4529
  const col = this.#resolveColumn(column);
4468
4530
  const clone = this.clone();
4469
4531
  clone.#select = [col];
@@ -4476,7 +4538,7 @@ export class ModelQuery<T extends BaseEntity> {
4476
4538
  `pluck('${column}') rejected — column is an object/relation`,
4477
4539
  );
4478
4540
  }
4479
- return v;
4541
+ return v as V;
4480
4542
  });
4481
4543
  }
4482
4544
 
@@ -4548,7 +4610,7 @@ export class ModelQuery<T extends BaseEntity> {
4548
4610
  // === Story 29.10 — pagination =====================================================================
4549
4611
 
4550
4612
  /** Offset-based paginator. */
4551
- async paginate(page: number, perPage: number): Promise<Paginator<T>> {
4613
+ async paginate(page: number, perPage = 20): Promise<Paginator<T>> {
4552
4614
  const p = Math.max(1, Math.floor(page));
4553
4615
  const pp = Math.max(1, Math.floor(perPage));
4554
4616
  // Adonis Lucid hook order:
@@ -609,6 +609,27 @@ export function getPrimaryKey(target: Constructor): string | undefined {
609
609
  return Reflect.getMetadata(PRIMARY_KEY, target);
610
610
  }
611
611
 
612
+ /**
613
+ * The foreign key a relation falls back to when none is declared.
614
+ *
615
+ * Goes through the entity's naming strategy rather than hardcoding `_id`, so a
616
+ * model whose primary key is not `id` gets `user_uuid` instead of a `user_id`
617
+ * column that does not exist. Lucid derives it the same way
618
+ * (`relationForeignKey` / `relationPivotForeignKey`), and for the usual `id`
619
+ * the result is unchanged.
620
+ */
621
+ export function defaultRelationForeignKey(
622
+ kind: "belongsTo" | "hasMany" | "hasOne" | "manyToMany",
623
+ entityClass: Constructor,
624
+ ): string {
625
+ const pk = getPrimaryKey(entityClass) ?? "id";
626
+ return getNamingStrategy(entityClass).relationForeignKey(
627
+ kind,
628
+ entityClass.name,
629
+ pk,
630
+ );
631
+ }
632
+
612
633
  /** Get relation metadata for a class (returns a copy). */
613
634
  export function getRelationMetadata(target: Constructor): RelationMetadata[] {
614
635
  return [...(Reflect.getMetadata(RELATIONS_KEY, target) ?? [])];
package/src/index.ts CHANGED
@@ -10,14 +10,16 @@ export { SQLITE_PROD_PRAGMAS } from "./AtlasProvider.js";
10
10
  export type {
11
11
  AsyncDatabaseConnection,
12
12
  ConnectRetryOptions,
13
+ IsolationLevel,
13
14
  ObservabilityOptions,
14
15
  QueryMeta,
16
+ TransactionOptions,
15
17
  } from "./adapters/NapiDbAdapter.js";
16
18
  export { createNapiConnection } from "./adapters/NapiDbAdapter.js";
17
19
  export type { DomainEvent } from "./BaseEntity.js";
18
20
  export { BaseEntity } from "./BaseEntity.js";
19
21
  export { BaseModel } from "./BaseModel.js";
20
- export type { DatabaseConnection } from "./BaseRepository.js";
22
+ export type { CreateOptions, DatabaseConnection } from "./BaseRepository.js";
21
23
  export { BaseRepository } from "./BaseRepository.js";
22
24
  export { defineConfig } from "./config.js";
23
25
  export { configure } from "./configure.js";
@@ -140,6 +142,9 @@ export {
140
142
  export {
141
143
  isAtlasStrictMode,
142
144
  ModelQuery,
145
+ // What `paginate()` hands back. Unexported, an app could not name the value
146
+ // it had just been given.
147
+ Paginator,
143
148
  setAtlasStrictMode,
144
149
  } from "./ModelQuery.js";
145
150
  export type { NamingStrategy } from "./naming/NamingStrategy.js";
@@ -208,7 +213,29 @@ export {
208
213
  runSeeders,
209
214
  Seeder,
210
215
  } from "./schema/Seeder.js";
216
+ /**
217
+ * The vocabulary of a migration callback.
218
+ *
219
+ * `onDelete(action)`, `foreign(...)` and the alter operations all take these,
220
+ * so a migration that pulls one into a named constant — or a helper that wraps
221
+ * a column definition — needs to be able to spell the type.
222
+ */
223
+ export type {
224
+ AlterOperation,
225
+ CheckExpression,
226
+ CheckOperator,
227
+ CheckValue,
228
+ ColumnPosition,
229
+ ForeignKeyReference,
230
+ IndexDefinition,
231
+ ReferentialAction,
232
+ TableConstraintSpec,
233
+ TableOptionsSpec,
234
+ TextVariant,
235
+ } from "./schema/types.js";
211
236
  export type { TransactionClient } from "./Transaction.js";
212
237
  export { transaction } from "./Transaction.js";
238
+ export type { TestTransaction } from "./testing/DatabaseCleanup.js";
213
239
  export { truncateAll, useTransaction } from "./testing/DatabaseCleanup.js";
240
+ export type { FactoryContext } from "./testing/Factory.js";
214
241
  export { Factory, factory } from "./testing/Factory.js";
@@ -30,7 +30,15 @@ export interface NamingStrategy {
30
30
  kind: "belongsTo" | "hasMany" | "hasOne" | "manyToMany",
31
31
  parentPk: string,
32
32
  ): string;
33
- /** Foreign key column name on the owning side of a relation. */
33
+ /**
34
+ * Foreign key COLUMN name on the owning side of a relation.
35
+ *
36
+ * NAMED DEVIATION — Lucid's method of the same name returns the model
37
+ * ATTRIBUTE (camelCase), which it then runs through `columnName()`; the one
38
+ * that returns a column upstream is `relationPivotForeignKey`. Atlas
39
+ * resolves relations by column throughout, so one method answers for both
40
+ * and it answers in snake_case. An override must return a column name.
41
+ */
34
42
  relationForeignKey(
35
43
  kind: "belongsTo" | "hasMany" | "hasOne" | "manyToMany",
36
44
  parentClass: string,
@@ -82,7 +90,12 @@ export class CamelCaseNamingStrategy implements NamingStrategy {
82
90
  parentClass: string,
83
91
  parentPk: string,
84
92
  ): string {
85
- return `${camelToSnake(parentClass)}_${parentPk}`;
93
+ // The PK is snake_cased too, as Lucid does
94
+ // (`snakeCase(`${model.name}_${model.primaryKey}`)`). Identical to the
95
+ // old output for the usual `id`; a multi-word PK now yields a column
96
+ // name a migration would actually have created (`user_user_id`, not
97
+ // `user_userId`).
98
+ return `${camelToSnake(parentClass)}_${camelToSnake(parentPk)}`;
86
99
  }
87
100
 
88
101
  relationPivotTable(aClass: string, bClass: string): string {