@c9up/atlas 0.2.7 → 0.3.0
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/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/naming/NamingStrategy.ts +15 -2
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) ?? [])];
|
|
@@ -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 {
|