@bunyad/orm 0.2.0-beta.1 → 0.2.0-beta.3
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/README.md +1 -2
- package/dist/casts.d.ts +16 -3
- package/dist/casts.js +150 -4
- package/dist/eager.js +130 -11
- package/dist/factory.d.ts +20 -1
- package/dist/factory.js +143 -37
- package/dist/index.d.ts +21 -1
- package/dist/index.js +3 -1
- package/dist/model-events.d.ts +8 -2
- package/dist/model-events.js +27 -4
- package/dist/model-query.d.ts +75 -29
- package/dist/model-query.js +252 -133
- package/dist/model.d.ts +164 -34
- package/dist/model.js +708 -44
- package/dist/relations.d.ts +187 -11
- package/dist/relations.js +815 -89
- package/dist/schema-types.d.ts +27 -0
- package/dist/schema-types.js +145 -0
- package/dist/test-drivers.d.ts +10 -0
- package/dist/test-drivers.js +40 -0
- package/dist/typed-names.d.ts +53 -0
- package/dist/typed-names.js +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -81,11 +81,10 @@ Pick one and install it next to `@bunyad/database`: `better-sqlite3`, `pg` or `m
|
|
|
81
81
|
|
|
82
82
|
- Express on Node: [`docs/examples/orm-node-express`](https://github.com/bunyadjs/framework/tree/main/docs/examples/orm-node-express)
|
|
83
83
|
- Next.js (server-only, one shared pool): [`docs/examples/orm-node-next`](https://github.com/bunyadjs/framework/tree/main/docs/examples/orm-node-next)
|
|
84
|
-
- NestJS: `@bunyad/nestjs` (`BunyadOrmModule`), in the monorepo and not published yet.
|
|
85
84
|
|
|
86
85
|
## Status
|
|
87
86
|
|
|
88
|
-
Beta scope: SQLite, PostgreSQL and MySQL are tested on Bun and Node 20+ (the PostgreSQL and MySQL suites run when `BUNYAD_TEST_POSTGRES_URL` / `BUNYAD_TEST_MYSQL_URL` are set). SQL Server is experimental and needs `mssql` installed.
|
|
87
|
+
Beta scope: SQLite, PostgreSQL and MySQL are tested on Bun and Node 20+ (the PostgreSQL and MySQL suites run when `BUNYAD_TEST_POSTGRES_URL` / `BUNYAD_TEST_MYSQL_URL` are set). SQL Server is experimental and needs `mssql` installed.
|
|
89
88
|
|
|
90
89
|
## License
|
|
91
90
|
|
package/dist/casts.d.ts
CHANGED
|
@@ -15,21 +15,33 @@ export declare class Attribute {
|
|
|
15
15
|
}): Attribute;
|
|
16
16
|
}
|
|
17
17
|
/** Built-in cast type names. */
|
|
18
|
-
export type CastType = "boolean" | "bool" | "number" | "integer" | "int" | "float" | "double" | "real" | "decimal" | "bigint" | "string" | "json" | "date" | "datetime" | "array" | "collection" | "encrypted" | "encrypted:array" | "encrypted:json" | "encrypted:collection" | "hashed"
|
|
18
|
+
export type CastType = "boolean" | "bool" | "number" | "integer" | "int" | "float" | "double" | "real" | "decimal" | "bigint" | "string" | "json" | "date" | "datetime" | "array" | "collection" | "encrypted" | "encrypted:array" | "encrypted:json" | "encrypted:collection" | "hashed" | "object" | "immutable_date" | "immutable_datetime" | `decimal:${number}` | `date:${string}` | `datetime:${string}` | `immutable_date:${string}` | `immutable_datetime:${string}`;
|
|
19
19
|
/**
|
|
20
20
|
* A TypeScript string/number enum object (or similar value map).
|
|
21
21
|
*/
|
|
22
22
|
export type EnumCast = Record<string, string | number>;
|
|
23
23
|
/** Anything accepted in `casts` / `casts()`. */
|
|
24
24
|
export type CastDefinition = CastType | EnumCast | Attribute;
|
|
25
|
+
/** Base cast + optional argument for the parameterized forms (`decimal:2`, `date:Y-m-d`). */
|
|
26
|
+
export type ParsedCast = {
|
|
27
|
+
type: BaseCastType;
|
|
28
|
+
arg?: string;
|
|
29
|
+
};
|
|
30
|
+
type BaseCastType = "boolean" | "bool" | "number" | "integer" | "int" | "float" | "double" | "real" | "decimal" | "bigint" | "string" | "json" | "date" | "datetime" | "array" | "collection" | "encrypted" | "encrypted:array" | "encrypted:json" | "encrypted:collection" | "hashed";
|
|
31
|
+
export declare function parseCast(definition: string): ParsedCast | null;
|
|
25
32
|
export declare function isCastType(value: unknown): value is CastType;
|
|
33
|
+
/**
|
|
34
|
+
* Format a Date with a PHP-style `date()` format (`Y-m-d H:i:s`). Used by
|
|
35
|
+
* `date:FORMAT` / `datetime:FORMAT` casts when a model is serialized. UTC.
|
|
36
|
+
*/
|
|
37
|
+
export declare function formatPhpDate(date: Date, format: string): string;
|
|
26
38
|
export declare function isAttribute(value: unknown): value is Attribute;
|
|
27
39
|
export declare function isEnumCast(value: unknown): value is EnumCast;
|
|
28
40
|
/** Async bcrypt for the `hashed` cast (same output as the sync path, off the event loop). */
|
|
29
41
|
export declare function hashCastValue(value: string): Promise<string>;
|
|
30
42
|
export declare function isAlreadyHashed(value: string): boolean;
|
|
31
|
-
export declare function castFromStorage(value: unknown,
|
|
32
|
-
export declare function castToStorage(value: unknown,
|
|
43
|
+
export declare function castFromStorage(value: unknown, definition: CastType): unknown;
|
|
44
|
+
export declare function castToStorage(value: unknown, definition: CastType, driver?: string): unknown;
|
|
33
45
|
export declare function enumFromStorage(value: unknown, enumObj: EnumCast): unknown;
|
|
34
46
|
export declare function enumToStorage(value: unknown, enumObj: EnumCast): unknown;
|
|
35
47
|
/** Cast JSON column to a `Collection`. */
|
|
@@ -48,3 +60,4 @@ export declare const AsFluent: Attribute;
|
|
|
48
60
|
export declare const AsEnumCollection: {
|
|
49
61
|
of(enumObj: EnumCast): Attribute;
|
|
50
62
|
};
|
|
63
|
+
export {};
|
package/dist/casts.js
CHANGED
|
@@ -38,6 +38,18 @@ const CAST_TYPE_SET = new Set([
|
|
|
38
38
|
"encrypted:collection",
|
|
39
39
|
"hashed",
|
|
40
40
|
]);
|
|
41
|
+
/** Fixed-digit string rounded half away from zero on the decimal text, so `1.005` gives `1.01`. */
|
|
42
|
+
function toFixedDecimal(value, digits) {
|
|
43
|
+
const n = toNumber(value);
|
|
44
|
+
if (!Number.isFinite(n) || digits < 0 || digits > 20)
|
|
45
|
+
return n.toFixed(Math.max(0, digits));
|
|
46
|
+
const sign = n < 0 ? -1 : 1;
|
|
47
|
+
const shifted = Number(`${Math.abs(n)}e${digits}`);
|
|
48
|
+
if (!Number.isFinite(shifted))
|
|
49
|
+
return n.toFixed(digits);
|
|
50
|
+
const rounded = Number(`${Math.round(shifted)}e-${digits}`);
|
|
51
|
+
return (sign * rounded).toFixed(digits);
|
|
52
|
+
}
|
|
41
53
|
function toNumber(value) {
|
|
42
54
|
if (typeof value === "number") {
|
|
43
55
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -75,8 +87,122 @@ function toBoolean(value) {
|
|
|
75
87
|
}
|
|
76
88
|
return Boolean(value);
|
|
77
89
|
}
|
|
90
|
+
const parsedCastCache = new Map();
|
|
91
|
+
export function parseCast(definition) {
|
|
92
|
+
const hit = parsedCastCache.get(definition);
|
|
93
|
+
if (hit !== undefined)
|
|
94
|
+
return hit;
|
|
95
|
+
let parsed = null;
|
|
96
|
+
if (CAST_TYPE_SET.has(definition)) {
|
|
97
|
+
parsed = { type: definition };
|
|
98
|
+
}
|
|
99
|
+
else if (definition === "object") {
|
|
100
|
+
parsed = { type: "json" };
|
|
101
|
+
}
|
|
102
|
+
else if (definition === "immutable_date") {
|
|
103
|
+
parsed = { type: "date" };
|
|
104
|
+
}
|
|
105
|
+
else if (definition === "immutable_datetime") {
|
|
106
|
+
parsed = { type: "datetime" };
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const colon = definition.indexOf(":");
|
|
110
|
+
if (colon > 0) {
|
|
111
|
+
const head = definition.slice(0, colon);
|
|
112
|
+
const arg = definition.slice(colon + 1);
|
|
113
|
+
if (head === "decimal" && /^\d+$/.test(arg))
|
|
114
|
+
parsed = { type: "decimal", arg };
|
|
115
|
+
else if ((head === "date" || head === "immutable_date") && arg)
|
|
116
|
+
parsed = { type: "date", arg };
|
|
117
|
+
else if ((head === "datetime" || head === "immutable_datetime") && arg)
|
|
118
|
+
parsed = { type: "datetime", arg };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
parsedCastCache.set(definition, parsed);
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
78
124
|
export function isCastType(value) {
|
|
79
|
-
return typeof value === "string" &&
|
|
125
|
+
return typeof value === "string" && parseCast(value) !== null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Format a Date with a PHP-style `date()` format (`Y-m-d H:i:s`). Used by
|
|
129
|
+
* `date:FORMAT` / `datetime:FORMAT` casts when a model is serialized. UTC.
|
|
130
|
+
*/
|
|
131
|
+
export function formatPhpDate(date, format) {
|
|
132
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
133
|
+
const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
134
|
+
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
135
|
+
const h24 = date.getUTCHours();
|
|
136
|
+
const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
|
|
137
|
+
let out = "";
|
|
138
|
+
for (let i = 0; i < format.length; i++) {
|
|
139
|
+
const ch = format[i];
|
|
140
|
+
switch (ch) {
|
|
141
|
+
case "\\":
|
|
142
|
+
i++;
|
|
143
|
+
out += format[i] ?? "";
|
|
144
|
+
break;
|
|
145
|
+
case "d":
|
|
146
|
+
out += pad(date.getUTCDate());
|
|
147
|
+
break;
|
|
148
|
+
case "j":
|
|
149
|
+
out += date.getUTCDate();
|
|
150
|
+
break;
|
|
151
|
+
case "m":
|
|
152
|
+
out += pad(date.getUTCMonth() + 1);
|
|
153
|
+
break;
|
|
154
|
+
case "n":
|
|
155
|
+
out += date.getUTCMonth() + 1;
|
|
156
|
+
break;
|
|
157
|
+
case "Y":
|
|
158
|
+
out += pad(date.getUTCFullYear(), 4);
|
|
159
|
+
break;
|
|
160
|
+
case "y":
|
|
161
|
+
out += pad(date.getUTCFullYear() % 100);
|
|
162
|
+
break;
|
|
163
|
+
case "H":
|
|
164
|
+
out += pad(h24);
|
|
165
|
+
break;
|
|
166
|
+
case "G":
|
|
167
|
+
out += h24;
|
|
168
|
+
break;
|
|
169
|
+
case "h":
|
|
170
|
+
out += pad(h12);
|
|
171
|
+
break;
|
|
172
|
+
case "g":
|
|
173
|
+
out += h12;
|
|
174
|
+
break;
|
|
175
|
+
case "i":
|
|
176
|
+
out += pad(date.getUTCMinutes());
|
|
177
|
+
break;
|
|
178
|
+
case "s":
|
|
179
|
+
out += pad(date.getUTCSeconds());
|
|
180
|
+
break;
|
|
181
|
+
case "A":
|
|
182
|
+
out += h24 < 12 ? "AM" : "PM";
|
|
183
|
+
break;
|
|
184
|
+
case "a":
|
|
185
|
+
out += h24 < 12 ? "am" : "pm";
|
|
186
|
+
break;
|
|
187
|
+
case "U":
|
|
188
|
+
out += Math.floor(date.getTime() / 1000);
|
|
189
|
+
break;
|
|
190
|
+
case "M":
|
|
191
|
+
out += months[date.getUTCMonth()].slice(0, 3);
|
|
192
|
+
break;
|
|
193
|
+
case "F":
|
|
194
|
+
out += months[date.getUTCMonth()];
|
|
195
|
+
break;
|
|
196
|
+
case "D":
|
|
197
|
+
out += days[date.getUTCDay()].slice(0, 3);
|
|
198
|
+
break;
|
|
199
|
+
case "l":
|
|
200
|
+
out += days[date.getUTCDay()];
|
|
201
|
+
break;
|
|
202
|
+
default: out += ch;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
80
206
|
}
|
|
81
207
|
export function isAttribute(value) {
|
|
82
208
|
return value instanceof Attribute;
|
|
@@ -125,9 +251,15 @@ function decryptPayload(value) {
|
|
|
125
251
|
return String(value ?? "");
|
|
126
252
|
return Crypt.decrypt(value);
|
|
127
253
|
}
|
|
128
|
-
export function castFromStorage(value,
|
|
254
|
+
export function castFromStorage(value, definition) {
|
|
255
|
+
const parsed = parseCast(definition);
|
|
256
|
+
const type = parsed.type;
|
|
129
257
|
if (value == null)
|
|
130
258
|
return value;
|
|
259
|
+
// `decimal:2` keeps money-like values exact as a fixed-digit string.
|
|
260
|
+
if (type === "decimal" && parsed.arg !== undefined) {
|
|
261
|
+
return toFixedDecimal(value, Number(parsed.arg));
|
|
262
|
+
}
|
|
131
263
|
switch (type) {
|
|
132
264
|
case "boolean":
|
|
133
265
|
case "bool":
|
|
@@ -147,6 +279,8 @@ export function castFromStorage(value, type) {
|
|
|
147
279
|
return String(value);
|
|
148
280
|
case "json":
|
|
149
281
|
case "array":
|
|
282
|
+
if (value === "")
|
|
283
|
+
return null;
|
|
150
284
|
return typeof value === "string" ? JSON.parse(value) : value;
|
|
151
285
|
case "collection": {
|
|
152
286
|
// From the database (JSON text), or already in memory: `fill()` passes a Collection or array.
|
|
@@ -174,9 +308,19 @@ export function castFromStorage(value, type) {
|
|
|
174
308
|
return value;
|
|
175
309
|
}
|
|
176
310
|
}
|
|
177
|
-
|
|
311
|
+
function assertValidDate(value, cast) {
|
|
312
|
+
if (Number.isNaN(toDate(value).getTime())) {
|
|
313
|
+
throw new Error(`Cannot cast ${JSON.stringify(String(value))} to ${cast}: not a valid date.`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export function castToStorage(value, definition, driver) {
|
|
178
317
|
if (value == null)
|
|
179
318
|
return value;
|
|
319
|
+
const parsed = parseCast(definition);
|
|
320
|
+
const type = parsed.type;
|
|
321
|
+
if (type === "decimal" && parsed.arg !== undefined) {
|
|
322
|
+
return toFixedDecimal(value, Number(parsed.arg));
|
|
323
|
+
}
|
|
180
324
|
const driverName = (driver ?? "sqlite");
|
|
181
325
|
switch (type) {
|
|
182
326
|
case "boolean":
|
|
@@ -201,14 +345,16 @@ export function castToStorage(value, type, driver) {
|
|
|
201
345
|
return String(value);
|
|
202
346
|
case "json":
|
|
203
347
|
case "array":
|
|
204
|
-
return
|
|
348
|
+
return JSON.stringify(value);
|
|
205
349
|
case "collection": {
|
|
206
350
|
const items = collectionItems(value);
|
|
207
351
|
return JSON.stringify(items);
|
|
208
352
|
}
|
|
209
353
|
case "date":
|
|
354
|
+
assertValidDate(value, definition);
|
|
210
355
|
return dateForStorage(value);
|
|
211
356
|
case "datetime":
|
|
357
|
+
assertValidDate(value, definition);
|
|
212
358
|
return dateTimeForStorage(value, driverName);
|
|
213
359
|
case "encrypted":
|
|
214
360
|
return encryptPayload(value);
|
package/dist/eager.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import { wrapSqlName } from "@bunyad/database";
|
|
5
5
|
import { OrmCollection } from "./orm-collection.js";
|
|
6
6
|
import { softDeleteAliasSql, uniqueKeys, coerceEagerRelations, } from "./model-helpers.js";
|
|
7
|
+
import { hasGlobalScopes } from "./scopes.js";
|
|
7
8
|
import { eagerFetchByKeys } from "./eager-fetch.js";
|
|
8
9
|
import { Model } from "./model.js";
|
|
9
10
|
import { enterEagerLoad, leaveEagerLoad } from "./model-strictness.js";
|
|
10
|
-
import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, aggregateRelation, applyPivotAttributes, resolveMorphType, resolveRelation, } from "./relations.js";
|
|
11
|
+
import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, MorphToMany, MorphedByMany, aggregateRelation, applyPivotAttributes, applyPivotWheres, setInverse, resolveMorphType, resolveRelation, } from "./relations.js";
|
|
11
12
|
/** Eager-load named relations onto models (`with` / `load` / collections).
|
|
12
13
|
* Batches BelongsTo / HasMany / HasOne / BelongsToMany / Morph* / ofMany with
|
|
13
14
|
* `WHERE IN`. Nested paths like `parent.children` are supported.
|
|
@@ -110,6 +111,27 @@ function applyConstraint(q, constraint) {
|
|
|
110
111
|
constraint(q);
|
|
111
112
|
return q;
|
|
112
113
|
}
|
|
114
|
+
/** Apply the constraint, then lift its limit/offset off the batched query (see `takePaging`). */
|
|
115
|
+
function applyConstraintPaged(q, constraint, paging) {
|
|
116
|
+
applyConstraint(q, constraint);
|
|
117
|
+
if (constraint) {
|
|
118
|
+
const taken = q.takePaging();
|
|
119
|
+
if (taken.limit !== undefined)
|
|
120
|
+
paging.limit = taken.limit;
|
|
121
|
+
if (taken.offset !== undefined)
|
|
122
|
+
paging.offset = taken.offset;
|
|
123
|
+
}
|
|
124
|
+
return q;
|
|
125
|
+
}
|
|
126
|
+
/** Slice every parent's list to `offset` / `limit` (rows are already in the constraint's order). */
|
|
127
|
+
function pagePerParent(byParent, paging) {
|
|
128
|
+
if (paging.limit === undefined && paging.offset === undefined)
|
|
129
|
+
return;
|
|
130
|
+
const start = paging.offset ?? 0;
|
|
131
|
+
for (const [key, list] of byParent) {
|
|
132
|
+
byParent.set(key, paging.limit === undefined ? list.slice(start) : list.slice(start, start + paging.limit));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
113
135
|
function relationFromModel(model, relation) {
|
|
114
136
|
try {
|
|
115
137
|
return model.related(relation);
|
|
@@ -143,7 +165,7 @@ async function forEachIdChunk(ids, run) {
|
|
|
143
165
|
});
|
|
144
166
|
await Promise.all(workers);
|
|
145
167
|
}
|
|
146
|
-
function
|
|
168
|
+
function eagerLoadOneRelationRaw(models, relation, constraint) {
|
|
147
169
|
const sampleRel = relationFromModel(models[0], relation);
|
|
148
170
|
if (!sampleRel)
|
|
149
171
|
return;
|
|
@@ -168,6 +190,9 @@ function eagerLoadOneRelation(models, relation, constraint) {
|
|
|
168
190
|
if (sampleRel instanceof MorphToMany) {
|
|
169
191
|
return eagerLoadMorphToMany(models, relation, sampleRel, constraint);
|
|
170
192
|
}
|
|
193
|
+
if (sampleRel instanceof MorphedByMany) {
|
|
194
|
+
return eagerLoadMorphedByMany(models, relation, sampleRel, constraint);
|
|
195
|
+
}
|
|
171
196
|
if (sampleRel instanceof MorphMany) {
|
|
172
197
|
return eagerLoadMorphMany(models, relation, sampleRel, constraint);
|
|
173
198
|
}
|
|
@@ -184,6 +209,45 @@ function eagerLoadOneRelation(models, relation, constraint) {
|
|
|
184
209
|
return eagerLoadHasOneThrough(models, relation, sampleRel, constraint);
|
|
185
210
|
}
|
|
186
211
|
}
|
|
212
|
+
function fillDefaults(models, relation) {
|
|
213
|
+
for (const model of models) {
|
|
214
|
+
const row = model;
|
|
215
|
+
if (row[relation] == null) {
|
|
216
|
+
const rel = model.related(relation);
|
|
217
|
+
row[relation] = rel.makeDefault();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Load one relation, then fill `withDefault()` relations that matched nothing. */
|
|
222
|
+
function eagerLoadOneRelation(models, relation, constraint) {
|
|
223
|
+
const sampleRel = relationFromModel(models[0], relation);
|
|
224
|
+
const withDefault = Boolean(sampleRel?.hasDefault?.());
|
|
225
|
+
const loaded = eagerLoadOneRelationRaw(models, relation, constraint);
|
|
226
|
+
const inverseName = sampleRel?.getInverse?.();
|
|
227
|
+
if (inverseName !== undefined) {
|
|
228
|
+
const attach = () => {
|
|
229
|
+
for (const model of models) {
|
|
230
|
+
const value = model[relation];
|
|
231
|
+
const children = value == null ? [] : Array.isArray(value) ? value : typeof value.all === "function" ? value.all() : [value];
|
|
232
|
+
for (const child of children)
|
|
233
|
+
setInverse(child, inverseName, model);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
if (loaded instanceof Promise) {
|
|
237
|
+
const chained = loaded.then(attach);
|
|
238
|
+
if (!withDefault)
|
|
239
|
+
return chained;
|
|
240
|
+
return chained.then(() => fillDefaults(models, relation));
|
|
241
|
+
}
|
|
242
|
+
attach();
|
|
243
|
+
}
|
|
244
|
+
if (!withDefault)
|
|
245
|
+
return loaded;
|
|
246
|
+
const fill = () => fillDefaults(models, relation);
|
|
247
|
+
if (loaded instanceof Promise)
|
|
248
|
+
return loaded.then(fill);
|
|
249
|
+
fill();
|
|
250
|
+
}
|
|
187
251
|
/** Single-query ofMany via GROUP BY join (avoids per-row correlated subqueries). */
|
|
188
252
|
async function eagerLoadHasOneOfMany(models, relation, sample, constraint) {
|
|
189
253
|
const foreignKey = sample.getForeignKeyName();
|
|
@@ -360,11 +424,12 @@ async function eagerLoadHasMany(models, relation, sample, constraint) {
|
|
|
360
424
|
const Related = sample.getRelated();
|
|
361
425
|
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
362
426
|
const byParent = new Map();
|
|
427
|
+
const paging = {};
|
|
363
428
|
await forEachIdChunk(parentIds, async (chunk) => {
|
|
364
429
|
let related;
|
|
365
430
|
if (constraint) {
|
|
366
431
|
let q = Related.whereIn(foreignKey, chunk);
|
|
367
|
-
|
|
432
|
+
applyConstraintPaged(q, constraint, paging);
|
|
368
433
|
related = (await q.get()).all();
|
|
369
434
|
}
|
|
370
435
|
else {
|
|
@@ -377,6 +442,7 @@ async function eagerLoadHasMany(models, relation, sample, constraint) {
|
|
|
377
442
|
byParent.set(key, list);
|
|
378
443
|
}
|
|
379
444
|
});
|
|
445
|
+
pagePerParent(byParent, paging);
|
|
380
446
|
for (const model of models) {
|
|
381
447
|
const id = model[localKey];
|
|
382
448
|
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
@@ -481,11 +547,16 @@ async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
|
481
547
|
const foreignPivotKey = sample.getForeignPivotKeyName();
|
|
482
548
|
const relatedPivotKey = sample.getRelatedPivotKeyName();
|
|
483
549
|
const pivotColumns = sample.getPivotColumns();
|
|
484
|
-
const
|
|
550
|
+
const pivotWheres = sample.getPivotWheres();
|
|
551
|
+
const pivotAccessor = sample.getPivotAccessor();
|
|
552
|
+
const pivotClass = sample.getPivotClass();
|
|
553
|
+
const pivotOrders = sample.getPivotOrders();
|
|
554
|
+
const pivotKeys = pivotColumns.length > 0 || pivotClass || pivotAccessor !== "pivot"
|
|
485
555
|
? [foreignPivotKey, relatedPivotKey, ...pivotColumns.filter((c) => c !== foreignPivotKey && c !== relatedPivotKey)]
|
|
486
556
|
: [];
|
|
487
557
|
const parentIds = uniqueKeys(models.map((model) => model[parentKey]));
|
|
488
558
|
const byParent = new Map();
|
|
559
|
+
const paging = {};
|
|
489
560
|
if (parentIds.length > 0) {
|
|
490
561
|
const dialect = parent.getConnection().dialect;
|
|
491
562
|
const qRelated = wrapSqlName(dialect, Related.table);
|
|
@@ -500,7 +571,10 @@ async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
|
500
571
|
.join(", ")}`
|
|
501
572
|
: "";
|
|
502
573
|
await forEachIdChunk(parentIds, async (chunk) => {
|
|
503
|
-
if (constraint
|
|
574
|
+
if (constraint ||
|
|
575
|
+
hasGlobalScopes(Related) ||
|
|
576
|
+
pivotWheres.length > 0 ||
|
|
577
|
+
pivotOrders.length > 0) {
|
|
504
578
|
let q = Related.newQuery()
|
|
505
579
|
.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${Related.table}.${Related.primaryKey}`)
|
|
506
580
|
.whereIn(`${pivotTable}.${foreignPivotKey}`, chunk)
|
|
@@ -509,13 +583,16 @@ async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
|
509
583
|
for (const col of pivotKeys) {
|
|
510
584
|
q = q.selectRaw(`${pivotTable}.${col} as __pivot_${col}`);
|
|
511
585
|
}
|
|
512
|
-
|
|
586
|
+
applyPivotWheres(q, pivotTable, pivotWheres);
|
|
587
|
+
for (const o of pivotOrders)
|
|
588
|
+
q = q.orderBy(`${pivotTable}.${o.column}`, o.direction);
|
|
589
|
+
applyConstraintPaged(q, constraint, paging);
|
|
513
590
|
const related = await q.get();
|
|
514
591
|
for (const row of related.all()) {
|
|
515
592
|
const rec = row;
|
|
516
593
|
const parentId = rec.__bunyad_parent_id;
|
|
517
594
|
delete rec.__bunyad_parent_id;
|
|
518
|
-
applyPivotAttributes(row, rec, pivotKeys);
|
|
595
|
+
applyPivotAttributes(row, rec, pivotKeys, { accessor: pivotAccessor, pivotClass });
|
|
519
596
|
const key = String(parentId);
|
|
520
597
|
const list = byParent.get(key) ?? [];
|
|
521
598
|
list.push(row);
|
|
@@ -534,7 +611,7 @@ async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
|
534
611
|
const { __bunyad_parent_id: _, ...attrs } = row;
|
|
535
612
|
const model = new Related(attrs);
|
|
536
613
|
model.exists = true;
|
|
537
|
-
applyPivotAttributes(model, attrs, pivotKeys);
|
|
614
|
+
applyPivotAttributes(model, attrs, pivotKeys, { accessor: pivotAccessor, pivotClass });
|
|
538
615
|
const key = String(parentId);
|
|
539
616
|
const list = byParent.get(key) ?? [];
|
|
540
617
|
list.push(model);
|
|
@@ -542,6 +619,7 @@ async function eagerLoadBelongsToMany(models, relation, sample, constraint) {
|
|
|
542
619
|
}
|
|
543
620
|
});
|
|
544
621
|
}
|
|
622
|
+
pagePerParent(byParent, paging);
|
|
545
623
|
for (const model of models) {
|
|
546
624
|
const id = model[parentKey];
|
|
547
625
|
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
@@ -560,6 +638,7 @@ async function eagerLoadMorphToMany(models, relation, sample, constraint) {
|
|
|
560
638
|
const parentTenantKey = sample.getParentTenantKey();
|
|
561
639
|
const parentIds = uniqueKeys(models.map((model) => model[parentKey]));
|
|
562
640
|
const byParent = new Map();
|
|
641
|
+
const paging = {};
|
|
563
642
|
if (parentIds.length > 0 && morphTypes.length > 0) {
|
|
564
643
|
const dialect = parent.getConnection().dialect;
|
|
565
644
|
const qRelated = wrapSqlName(dialect, Related.table);
|
|
@@ -571,7 +650,7 @@ async function eagerLoadMorphToMany(models, relation, sample, constraint) {
|
|
|
571
650
|
const soft = softDeleteAliasSql(Related, qRelated);
|
|
572
651
|
const morphPlaceholders = morphTypes.map(() => "?").join(", ");
|
|
573
652
|
await forEachIdChunk(parentIds, async (chunk) => {
|
|
574
|
-
if (constraint) {
|
|
653
|
+
if (constraint || hasGlobalScopes(Related)) {
|
|
575
654
|
let q = Related.newQuery()
|
|
576
655
|
.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${Related.table}.${Related.primaryKey}`)
|
|
577
656
|
.whereIn(`${pivotTable}.${foreignPivotKey}`, chunk)
|
|
@@ -584,7 +663,7 @@ async function eagerLoadMorphToMany(models, relation, sample, constraint) {
|
|
|
584
663
|
q = q.whereIn(`${pivotTable}.${pivotTenantKey}`, tenantIds);
|
|
585
664
|
}
|
|
586
665
|
}
|
|
587
|
-
|
|
666
|
+
applyConstraintPaged(q, constraint, paging);
|
|
588
667
|
const related = await q.get();
|
|
589
668
|
for (const row of related.all()) {
|
|
590
669
|
const rec = row;
|
|
@@ -624,6 +703,42 @@ async function eagerLoadMorphToMany(models, relation, sample, constraint) {
|
|
|
624
703
|
}
|
|
625
704
|
});
|
|
626
705
|
}
|
|
706
|
+
pagePerParent(byParent, paging);
|
|
707
|
+
for (const model of models) {
|
|
708
|
+
const id = model[parentKey];
|
|
709
|
+
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function eagerLoadMorphedByMany(models, relation, sample, constraint) {
|
|
713
|
+
const Related = sample.getRelated();
|
|
714
|
+
const parent = models[0].constructor;
|
|
715
|
+
const parentKey = parent.primaryKey;
|
|
716
|
+
const pivotTable = sample.getPivotTable();
|
|
717
|
+
const foreignPivotKey = sample.getForeignPivotKeyName();
|
|
718
|
+
const relatedPivotKey = sample.getRelatedPivotKeyName();
|
|
719
|
+
const morphTypeColumn = sample.getMorphTypeColumn();
|
|
720
|
+
const morphType = sample.getMorphType();
|
|
721
|
+
const parentIds = uniqueKeys(models.map((model) => model[parentKey]));
|
|
722
|
+
const byParent = new Map();
|
|
723
|
+
const paging = {};
|
|
724
|
+
await forEachIdChunk(parentIds, async (chunk) => {
|
|
725
|
+
const q = Related.newQuery()
|
|
726
|
+
.join(pivotTable, `${pivotTable}.${relatedPivotKey}`, "=", `${Related.table}.${Related.primaryKey}`)
|
|
727
|
+
.whereIn(`${pivotTable}.${foreignPivotKey}`, chunk)
|
|
728
|
+
.where(`${pivotTable}.${morphTypeColumn}`, morphType)
|
|
729
|
+
.select(`${Related.table}.*`)
|
|
730
|
+
.selectRaw(`${pivotTable}.${foreignPivotKey} as __bunyad_parent_id`);
|
|
731
|
+
applyConstraintPaged(q, constraint, paging);
|
|
732
|
+
for (const row of (await q.get()).all()) {
|
|
733
|
+
const rec = row;
|
|
734
|
+
const key = String(rec.__bunyad_parent_id);
|
|
735
|
+
delete rec.__bunyad_parent_id;
|
|
736
|
+
const list = byParent.get(key) ?? [];
|
|
737
|
+
list.push(row);
|
|
738
|
+
byParent.set(key, list);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
pagePerParent(byParent, paging);
|
|
627
742
|
for (const model of models) {
|
|
628
743
|
const id = model[parentKey];
|
|
629
744
|
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
@@ -637,9 +752,10 @@ async function eagerLoadMorphMany(models, relation, sample, constraint) {
|
|
|
637
752
|
const localKey = sample.getLocalKeyName();
|
|
638
753
|
const parentIds = uniqueKeys(models.map((model) => model[localKey]));
|
|
639
754
|
const byParent = new Map();
|
|
755
|
+
const paging = {};
|
|
640
756
|
await forEachIdChunk(parentIds, async (chunk) => {
|
|
641
757
|
let q = Related.where(typeColumn, morphType).whereIn(idColumn, chunk);
|
|
642
|
-
|
|
758
|
+
applyConstraintPaged(q, constraint, paging);
|
|
643
759
|
const related = await q.get();
|
|
644
760
|
for (const row of related.all()) {
|
|
645
761
|
const key = String(row[idColumn]);
|
|
@@ -648,6 +764,7 @@ async function eagerLoadMorphMany(models, relation, sample, constraint) {
|
|
|
648
764
|
byParent.set(key, list);
|
|
649
765
|
}
|
|
650
766
|
});
|
|
767
|
+
pagePerParent(byParent, paging);
|
|
651
768
|
for (const model of models) {
|
|
652
769
|
const id = model[localKey];
|
|
653
770
|
model[relation] = new OrmCollection(id == null ? [] : (byParent.get(String(id)) ?? []), { owned: true });
|
|
@@ -748,6 +865,8 @@ async function eagerLoadOneAggregate(models, Parent, spec) {
|
|
|
748
865
|
const dialect = meta.related.getConnection().dialect;
|
|
749
866
|
const fk = wrapSqlName(dialect, meta.foreignKey);
|
|
750
867
|
let q = meta.related.whereIn(meta.foreignKey, parentIds).selectRaw(`${fk} as __bunyad_k`);
|
|
868
|
+
if (meta.typeColumn)
|
|
869
|
+
q = q.where(meta.typeColumn, meta.morphType);
|
|
751
870
|
if (spec.fn === "exists") {
|
|
752
871
|
q = q.selectRaw("1 as __bunyad_v");
|
|
753
872
|
}
|
package/dist/factory.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare abstract class Factory<T extends Model = Model> {
|
|
|
33
33
|
/** Public model class accessor for relationship factories. */
|
|
34
34
|
modelClass(): ModelClass;
|
|
35
35
|
definition(): FactoryAttributes | Promise<FactoryAttributes>;
|
|
36
|
+
/** Runs once, before the first `make` / `create` (`afterMaking`, default states, …). */
|
|
37
|
+
configure(): this;
|
|
36
38
|
/** Number of models to make/create. */
|
|
37
39
|
count(amount: number): this & ManyModels;
|
|
38
40
|
/** Merge attribute overrides (object or callback) into every model. */
|
|
@@ -57,12 +59,29 @@ export declare abstract class Factory<T extends Model = Model> {
|
|
|
57
59
|
/**
|
|
58
60
|
* Attach related models on a belongs-to-many after create.
|
|
59
61
|
*/
|
|
60
|
-
hasAttached(related: Factory<any> | Model | Model[], pivot?: FactoryAttributes, relationship?: string): this;
|
|
62
|
+
hasAttached(related: Factory<any> | Model | Model[], pivot?: FactoryAttributes | ((model: Model) => FactoryAttributes), relationship?: string): this;
|
|
63
|
+
/**
|
|
64
|
+
* `recycle` — reuse existing models when this factory (or its nested `for` /
|
|
65
|
+
* `has` factories) would otherwise create a parent of the same class.
|
|
66
|
+
*/
|
|
67
|
+
recycle(models: Model | Model[] | {
|
|
68
|
+
all(): Model[];
|
|
69
|
+
}): this;
|
|
61
70
|
/** `Factory::new()`. */
|
|
62
71
|
static new<T extends Factory>(this: new () => T): T;
|
|
63
72
|
/** Build an in-memory model instance (not persisted). */
|
|
64
73
|
make(attributes?: FactoryAttributes): Promise<this extends ManyModels ? T[] : T>;
|
|
65
74
|
/** Persist model(s) via `Model.forceCreate` (factories set guarded columns too). */
|
|
66
75
|
create(attributes?: FactoryAttributes): Promise<this extends ManyModels ? T[] : T>;
|
|
76
|
+
/** `makeOne`. */
|
|
77
|
+
makeOne(attributes?: FactoryAttributes): Promise<T>;
|
|
78
|
+
/** `createOne`. */
|
|
79
|
+
createOne(attributes?: FactoryAttributes): Promise<T>;
|
|
80
|
+
/** `createMany(3)` or `createMany([{ name: "a" }, { name: "b" }])`. */
|
|
81
|
+
createMany(records: number | FactoryAttributes[]): Promise<T[]>;
|
|
82
|
+
/** `createQuietly` — no model events. */
|
|
83
|
+
createQuietly(attributes?: FactoryAttributes): Promise<this extends ManyModels ? T[] : T>;
|
|
84
|
+
/** `createManyQuietly`. */
|
|
85
|
+
createManyQuietly(records: number | FactoryAttributes[]): Promise<T[]>;
|
|
67
86
|
}
|
|
68
87
|
export {};
|