@c9up/atlas 0.1.18 → 0.1.19
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.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/dist/BaseEntity.d.ts +7 -2
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js +4 -2
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseRepository.d.ts +9 -3
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +115 -17
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ModelQuery.d.ts +95 -0
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +386 -27
- package/dist/ModelQuery.js.map +1 -1
- package/dist/schema/SchemaCheck.d.ts.map +1 -1
- package/dist/schema/SchemaCheck.js +3 -1
- package/dist/schema/SchemaCheck.js.map +1 -1
- package/dist/schema/introspect.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -1
- package/src/BaseEntity.ts +23 -4
- package/src/BaseRepository.ts +135 -17
- package/src/ModelQuery.ts +503 -30
- package/src/schema/SchemaCheck.ts +7 -2
- package/src/schema/introspect.ts +3 -4
package/src/BaseRepository.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import type { TransactionOptions } from "./adapters/NapiDbAdapter.js";
|
|
8
9
|
import type {
|
|
9
10
|
BaseEntity,
|
|
10
11
|
BelongsToRelationProxy,
|
|
@@ -27,9 +28,7 @@ import {
|
|
|
27
28
|
type PrimaryKeyGenerator,
|
|
28
29
|
} from "./decorators/entity.js";
|
|
29
30
|
import { fireHooks } from "./decorators/hooks.js";
|
|
30
|
-
import type { TransactionOptions } from "./adapters/NapiDbAdapter.js";
|
|
31
31
|
import { AtlasError, EntityNotFoundError } from "./errors.js";
|
|
32
|
-
import type { TransactionClient } from "./Transaction.js";
|
|
33
32
|
import { ModelQuery, runWithAtlasInternalBypass } from "./ModelQuery.js";
|
|
34
33
|
import {
|
|
35
34
|
type AtlasDialect,
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
registerColumnCast,
|
|
39
38
|
registerTableCasts,
|
|
40
39
|
} from "./query/native.js";
|
|
40
|
+
import { type TransactionClient, transaction } from "./Transaction.js";
|
|
41
41
|
import { camelToSnake, snakeToCamel } from "./utils/casing.js";
|
|
42
42
|
|
|
43
43
|
type EntityConstructor<T extends BaseEntity> = new () => T;
|
|
@@ -155,6 +155,15 @@ const POSTGRES_CAST_TYPES = new Set([
|
|
|
155
155
|
"int",
|
|
156
156
|
"bigint",
|
|
157
157
|
"smallint",
|
|
158
|
+
// Nullable boolean / float columns hit the same text-bound-NULL issue.
|
|
159
|
+
"boolean",
|
|
160
|
+
"bool",
|
|
161
|
+
"real",
|
|
162
|
+
"float4",
|
|
163
|
+
"double precision",
|
|
164
|
+
"double",
|
|
165
|
+
"float8",
|
|
166
|
+
"float",
|
|
158
167
|
]);
|
|
159
168
|
|
|
160
169
|
/**
|
|
@@ -386,6 +395,14 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
386
395
|
// ─── Finders ──────────────────────────────────────────────
|
|
387
396
|
|
|
388
397
|
async find(id: string | number | bigint): Promise<T | null> {
|
|
398
|
+
// AdonisJS Lucid throws on `undefined`/`null` rather than silently running
|
|
399
|
+
// `WHERE pk = NULL` (which matches nothing) — a common typo footgun.
|
|
400
|
+
if (id === undefined || id === null) {
|
|
401
|
+
throw new AtlasError(
|
|
402
|
+
"E_INVALID_FIND_VALUE",
|
|
403
|
+
`${this.#entityClass.name}.find() expects a value, received ${String(id)}.`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
389
406
|
// Route through the query builder so the read hooks (beforeFind/afterFind)
|
|
390
407
|
// fire and a `beforeFind` hook can mutate the query — the previous direct
|
|
391
408
|
// `#compileSelect` fast path bypassed every read hook silently.
|
|
@@ -407,10 +424,40 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
407
424
|
return this.query().where(column, value).first();
|
|
408
425
|
}
|
|
409
426
|
|
|
427
|
+
/** Find by a column or throw `EntityNotFoundError` (AdonisJS `findByOrFail`). */
|
|
428
|
+
async findByOrFail(column: string, value: unknown): Promise<T> {
|
|
429
|
+
const entity = await this.findBy(column, value);
|
|
430
|
+
if (!entity) {
|
|
431
|
+
throw new EntityNotFoundError(this.#entityClass.name, {
|
|
432
|
+
[column]: value,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return entity;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Find many rows by primary key (AdonisJS `findMany`), ordered PK desc. */
|
|
439
|
+
async findMany(ids: Array<string | number>): Promise<T[]> {
|
|
440
|
+
if (ids.length === 0) return [];
|
|
441
|
+
return this.query()
|
|
442
|
+
.whereIn(this.#primaryKey, ids)
|
|
443
|
+
.orderBy(this.#primaryKey, "desc")
|
|
444
|
+
.exec();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Find many rows by an arbitrary column (AdonisJS `findManyBy`). */
|
|
448
|
+
async findManyBy(
|
|
449
|
+
column: string,
|
|
450
|
+
values: Array<string | number>,
|
|
451
|
+
): Promise<T[]> {
|
|
452
|
+
if (values.length === 0) return [];
|
|
453
|
+
return this.query().whereIn(column, values).exec();
|
|
454
|
+
}
|
|
455
|
+
|
|
410
456
|
async all(): Promise<T[]> {
|
|
411
457
|
// Through the builder so beforeFetch/afterFetch fire. The builder applies
|
|
412
458
|
// the soft-delete scope by default, exactly like the old fast path.
|
|
413
|
-
|
|
459
|
+
// Ordered PK desc for AdonisJS Lucid `all()` parity (newest first).
|
|
460
|
+
return this.query().orderBy(this.#primaryKey, "desc").exec();
|
|
414
461
|
}
|
|
415
462
|
|
|
416
463
|
async allWithTrashed(): Promise<T[]> {
|
|
@@ -672,9 +719,14 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
672
719
|
search: Record<string, unknown>,
|
|
673
720
|
defaults: Record<string, unknown> = {},
|
|
674
721
|
): Promise<T> {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return this
|
|
722
|
+
// Atomic (AdonisJS Lucid parity): find-under-lock then create inside one
|
|
723
|
+
// transaction, so two concurrent callers can't both miss and both INSERT.
|
|
724
|
+
return transaction(this.#db, async (trx) => {
|
|
725
|
+
const repo = this.useTransaction(trx);
|
|
726
|
+
const existing = await repo.#findBySearch(search, true);
|
|
727
|
+
if (existing) return existing;
|
|
728
|
+
return repo.create({ ...search, ...defaults });
|
|
729
|
+
});
|
|
678
730
|
}
|
|
679
731
|
|
|
680
732
|
/** Find a row or build an in-memory instance without persisting. */
|
|
@@ -692,23 +744,32 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
692
744
|
return e;
|
|
693
745
|
}
|
|
694
746
|
|
|
695
|
-
/** Atomic find-or-update-or-insert. */
|
|
747
|
+
/** Atomic find-or-update-or-insert (AdonisJS Lucid parity — locked + transactional). */
|
|
696
748
|
async updateOrCreate(
|
|
697
749
|
search: Record<string, unknown>,
|
|
698
750
|
values: Record<string, unknown>,
|
|
699
751
|
): Promise<T> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
752
|
+
return transaction(this.#db, async (trx) => {
|
|
753
|
+
const repo = this.useTransaction(trx);
|
|
754
|
+
const existing = await repo.#findBySearch(search, true);
|
|
755
|
+
if (existing) {
|
|
756
|
+
for (const [k, v] of Object.entries(values)) existing.setProp(k, v);
|
|
757
|
+
await repo.save(existing);
|
|
758
|
+
return existing;
|
|
759
|
+
}
|
|
760
|
+
return repo.create({ ...search, ...values });
|
|
761
|
+
});
|
|
707
762
|
}
|
|
708
763
|
|
|
709
|
-
async #findBySearch(
|
|
764
|
+
async #findBySearch(
|
|
765
|
+
search: Record<string, unknown>,
|
|
766
|
+
lock = false,
|
|
767
|
+
): Promise<T | null> {
|
|
710
768
|
let q = this.query();
|
|
711
769
|
for (const [k, v] of Object.entries(search)) q = q.where(k, v);
|
|
770
|
+
// `forUpdate()` row-locks the matched row so a concurrent updateOrCreate
|
|
771
|
+
// serializes behind it (no-op on SQLite, which serializes writes anyway).
|
|
772
|
+
if (lock) q = q.forUpdate();
|
|
712
773
|
return q.first();
|
|
713
774
|
}
|
|
714
775
|
|
|
@@ -1307,7 +1368,8 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1307
1368
|
[fkProp]: parentIdValue,
|
|
1308
1369
|
});
|
|
1309
1370
|
|
|
1310
|
-
// Shared "has" proxy methods (create/createMany/save/saveMany
|
|
1371
|
+
// Shared "has" proxy methods (create/createMany/save/saveMany +
|
|
1372
|
+
// firstOrCreate/updateOrCreate scoped to this parent's FK).
|
|
1311
1373
|
const hasOps = {
|
|
1312
1374
|
async create(data: Record<string, unknown>) {
|
|
1313
1375
|
return relatedRepo.create(injectFk(data));
|
|
@@ -1315,6 +1377,27 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1315
1377
|
async createMany(rows: Array<Record<string, unknown>>) {
|
|
1316
1378
|
return relatedRepo.createMany(rows.map(injectFk));
|
|
1317
1379
|
},
|
|
1380
|
+
// Scope the search to the parent's FK column so the lookup only sees
|
|
1381
|
+
// this parent's rows; inject the FK into the created/updated row. The
|
|
1382
|
+
// related repo's firstOrCreate/updateOrCreate are atomic (txn + lock).
|
|
1383
|
+
async firstOrCreate(
|
|
1384
|
+
search: Record<string, unknown>,
|
|
1385
|
+
defaults: Record<string, unknown> = {},
|
|
1386
|
+
) {
|
|
1387
|
+
return relatedRepo.firstOrCreate(
|
|
1388
|
+
{ ...search, [fkCol]: parentIdValue },
|
|
1389
|
+
injectFk(defaults),
|
|
1390
|
+
);
|
|
1391
|
+
},
|
|
1392
|
+
async updateOrCreate(
|
|
1393
|
+
search: Record<string, unknown>,
|
|
1394
|
+
values: Record<string, unknown>,
|
|
1395
|
+
) {
|
|
1396
|
+
return relatedRepo.updateOrCreate(
|
|
1397
|
+
{ ...search, [fkCol]: parentIdValue },
|
|
1398
|
+
injectFk(values),
|
|
1399
|
+
);
|
|
1400
|
+
},
|
|
1318
1401
|
async save(related: BaseEntity) {
|
|
1319
1402
|
related.setProp(fkCol, parentIdValue);
|
|
1320
1403
|
related.setProp(fkProp, parentIdValue);
|
|
@@ -1654,9 +1737,42 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1654
1737
|
}
|
|
1655
1738
|
};
|
|
1656
1739
|
|
|
1740
|
+
// m2m create/save persist the related row THEN insert a pivot row —
|
|
1741
|
+
// NOT `hasOps.injectFk`, which would write a bogus `<parent>_id` column
|
|
1742
|
+
// onto the related table and never touch the pivot (silent corruption).
|
|
1743
|
+
const relatedPkProp = getPrimaryKey(relatedClass) ?? "id";
|
|
1744
|
+
const attachIds = (rows: BaseEntity[]): Promise<void> => {
|
|
1745
|
+
if (rows.length === 0) return Promise.resolve();
|
|
1746
|
+
const arg: Record<string, Record<string, unknown>> = {};
|
|
1747
|
+
for (const r of rows) arg[String(r[relatedPkProp])] = {};
|
|
1748
|
+
return attach(arg);
|
|
1749
|
+
};
|
|
1750
|
+
const m2mOps = {
|
|
1751
|
+
async create(data: Record<string, unknown>): Promise<BaseEntity> {
|
|
1752
|
+
const created = await relatedRepo.create(data);
|
|
1753
|
+
await attachIds([created]);
|
|
1754
|
+
return created;
|
|
1755
|
+
},
|
|
1756
|
+
async createMany(
|
|
1757
|
+
rows: Array<Record<string, unknown>>,
|
|
1758
|
+
): Promise<BaseEntity[]> {
|
|
1759
|
+
const created = await relatedRepo.createMany(rows);
|
|
1760
|
+
await attachIds(created);
|
|
1761
|
+
return created;
|
|
1762
|
+
},
|
|
1763
|
+
async save(related: BaseEntity): Promise<void> {
|
|
1764
|
+
await relatedRepo.save(related);
|
|
1765
|
+
await attachIds([related]);
|
|
1766
|
+
},
|
|
1767
|
+
async saveMany(related: BaseEntity[]): Promise<BaseEntity[]> {
|
|
1768
|
+
const saved = await relatedRepo.saveMany(related);
|
|
1769
|
+
await attachIds(saved);
|
|
1770
|
+
return saved;
|
|
1771
|
+
},
|
|
1772
|
+
};
|
|
1657
1773
|
const proxy: ManyToManyRelationProxy = {
|
|
1658
1774
|
type: "manyToMany",
|
|
1659
|
-
...
|
|
1775
|
+
...m2mOps,
|
|
1660
1776
|
query: scopedQuery,
|
|
1661
1777
|
attach,
|
|
1662
1778
|
detach,
|
|
@@ -1682,6 +1798,8 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1682
1798
|
type: "hasOne",
|
|
1683
1799
|
create: hasOps.create,
|
|
1684
1800
|
save: hasOps.save,
|
|
1801
|
+
firstOrCreate: hasOps.firstOrCreate,
|
|
1802
|
+
updateOrCreate: hasOps.updateOrCreate,
|
|
1685
1803
|
createMany: () => reject("createMany"),
|
|
1686
1804
|
saveMany: () => reject("saveMany"),
|
|
1687
1805
|
query: scopedQuery,
|