@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.
- package/db.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +54 -2
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseEntity.d.ts +7 -0
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js +100 -34
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseRepository.d.ts +1 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +26 -14
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ModelQuery.d.ts +35 -4
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +60 -19
- package/dist/ModelQuery.js.map +1 -1
- package/dist/decorators/entity.d.ts +10 -0
- package/dist/decorators/entity.d.ts.map +1 -1
- package/dist/decorators/entity.js +13 -0
- package/dist/decorators/entity.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/naming/NamingStrategy.d.ts +9 -1
- package/dist/naming/NamingStrategy.d.ts.map +1 -1
- package/dist/naming/NamingStrategy.js +6 -1
- package/dist/naming/NamingStrategy.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
- package/src/AtlasProvider.ts +81 -2
- package/src/BaseEntity.ts +140 -43
- package/src/BaseRepository.ts +27 -13
- package/src/ModelQuery.ts +82 -20
- package/src/decorators/entity.ts +21 -0
- package/src/index.ts +28 -1
- package/src/naming/NamingStrategy.ts +15 -2
package/src/BaseRepository.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
?
|
|
2167
|
-
:
|
|
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 ??
|
|
2375
|
+
pivot.foreignKey ??
|
|
2376
|
+
defaultRelationForeignKey("manyToMany", this.#entityClass);
|
|
2367
2377
|
const pivotOther =
|
|
2368
|
-
pivot.otherKey ??
|
|
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 ??
|
|
2477
|
+
relation.firstKey ??
|
|
2478
|
+
defaultRelationForeignKey("hasMany", this.#entityClass);
|
|
2467
2479
|
const secondKey =
|
|
2468
|
-
relation.secondKey ??
|
|
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 ??
|
|
2623
|
+
pivot.foreignKey ??
|
|
2624
|
+
defaultRelationForeignKey("manyToMany", this.#entityClass);
|
|
2611
2625
|
const pivotOther =
|
|
2612
|
-
pivot.otherKey ??
|
|
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
|
-
|
|
3343
|
-
const
|
|
3344
|
-
|
|
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 ??
|
|
3611
|
+
relation.firstKey ??
|
|
3612
|
+
defaultRelationForeignKey("hasMany", this.#entityClass);
|
|
3568
3613
|
const secondKey =
|
|
3569
|
-
relation.secondKey ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
3790
|
+
pivot.foreignKey ??
|
|
3791
|
+
defaultRelationForeignKey("manyToMany", this.#entityClass);
|
|
3743
3792
|
const otherKey =
|
|
3744
|
-
pivot.otherKey ??
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
4221
|
+
pivot.foreignKey ??
|
|
4222
|
+
defaultRelationForeignKey("manyToMany", this.#entityClass);
|
|
4170
4223
|
const otherKey =
|
|
4171
|
-
pivot.otherKey ??
|
|
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 ??
|
|
4255
|
+
relation.firstKey ??
|
|
4256
|
+
defaultRelationForeignKey("hasMany", this.#entityClass);
|
|
4202
4257
|
const secondKey =
|
|
4203
|
-
relation.secondKey ??
|
|
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
|
-
/**
|
|
4466
|
-
|
|
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
|
|
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:
|
package/src/decorators/entity.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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 {
|