@bunnykit/orm 0.1.26 → 0.1.28
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 +160 -5
- package/dist/src/index.d.ts +1 -1
- package/dist/src/model/Model.d.ts +30 -5
- package/dist/src/model/Model.js +188 -27
- package/dist/src/query/Builder.d.ts +4 -3
- package/dist/src/query/Builder.js +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -644,10 +644,10 @@ User.where("name", "Alice").dd(); // logs SQL and throws
|
|
|
644
644
|
| `each(n, fn)` | Per-item iterate |
|
|
645
645
|
| `cursor()` | Lazy async generator |
|
|
646
646
|
| `lazy(n?)` | Chunked lazy generator |
|
|
647
|
-
| `insert(data)`
|
|
648
|
-
| `insertGetId(data, col?)`
|
|
649
|
-
| `insertOrIgnore(data)`
|
|
650
|
-
| `upsert(data, uniqueBy, updateCols?)`
|
|
647
|
+
| `insert(data, options?)` | Insert row(s) with optional chunking |
|
|
648
|
+
| `insertGetId(data, col?)` | Insert and return ID |
|
|
649
|
+
| `insertOrIgnore(data)` | Insert, ignore conflicts |
|
|
650
|
+
| `upsert(data, uniqueBy, updateCols?, options?)` | Insert or update on conflict, optional chunking |
|
|
651
651
|
| `update(data)` | Update matched rows |
|
|
652
652
|
| `updateFrom(tbl, a, op, b)` | Update with JOIN |
|
|
653
653
|
| `delete()` | Delete matched rows |
|
|
@@ -739,10 +739,123 @@ const user = await User.firstOrCreate(
|
|
|
739
739
|
{ email: "alice@example.com" },
|
|
740
740
|
{ name: "Alice" },
|
|
741
741
|
);
|
|
742
|
-
const user = await User.
|
|
742
|
+
const user = await User.updateOrInsert(
|
|
743
743
|
{ email: "alice@example.com" },
|
|
744
744
|
{ name: "Alice Smith" },
|
|
745
745
|
);
|
|
746
|
+
|
|
747
|
+
// Bulk insert / upsert (apply fillable, casts, timestamps, UUID keys)
|
|
748
|
+
await User.insert([
|
|
749
|
+
{ name: "Alice", email: "alice1@example.com" },
|
|
750
|
+
{ name: "Bob", email: "alice2@example.com" },
|
|
751
|
+
], { chunkSize: 100 });
|
|
752
|
+
|
|
753
|
+
await User.upsert(
|
|
754
|
+
[{ email: "alice@example.com", name: "Alice Updated" }],
|
|
755
|
+
"email",
|
|
756
|
+
["name"],
|
|
757
|
+
{ chunkSize: 100 },
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
// Bulk create / save (fire model events by default)
|
|
761
|
+
const users = await User.createMany([
|
|
762
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
763
|
+
{ name: "Bob", email: "bob@example.com" },
|
|
764
|
+
]);
|
|
765
|
+
|
|
766
|
+
await User.saveMany(users);
|
|
767
|
+
|
|
768
|
+
// Bypass observers with { events: false }
|
|
769
|
+
await User.createMany(records, { events: false });
|
|
770
|
+
await User.saveMany(models, { events: false });
|
|
771
|
+
model.save({ events: false });
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Bulk Operations
|
|
775
|
+
|
|
776
|
+
Bunny provides bulk methods for inserting, upserting, and creating multiple records efficiently. All bulk insert and upsert operations apply fillable rules, attribute casts, timestamps, and UUID key generation automatically.
|
|
777
|
+
|
|
778
|
+
#### Model.insert(records, options?)
|
|
779
|
+
|
|
780
|
+
Insert raw records with automatic processing. Respects `fillable` guard, applies casts and timestamps, and generates UUID keys for `primaryKey = "uuid"` models.
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
await User.insert([
|
|
784
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
785
|
+
{ name: "Bob", email: "bob@example.com" },
|
|
786
|
+
], { chunkSize: 500 });
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
`chunkSize` batches large inserts to avoid exceeding query size limits.
|
|
790
|
+
|
|
791
|
+
#### Model.upsert(records, uniqueBy, updateColumns?, options?)
|
|
792
|
+
|
|
793
|
+
Insert or update records based on a unique key. On insert: applies fillable, casts, timestamps, and UUID generation. On update: only the specified `updateColumns` are modified.
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
// Insert or update by email, updating only the name
|
|
797
|
+
await User.upsert(
|
|
798
|
+
[{ email: "alice@example.com", name: "Alice Updated" }],
|
|
799
|
+
"email",
|
|
800
|
+
["name"],
|
|
801
|
+
{ chunkSize: 500 },
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// Insert or update by email, updating all columns except the unique key
|
|
805
|
+
await User.upsert(
|
|
806
|
+
[{ email: "alice@example.com", name: "Alice", active: true }],
|
|
807
|
+
"email",
|
|
808
|
+
);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
#### Model.updateOrInsert(attributes, values)
|
|
812
|
+
|
|
813
|
+
Find a record by `attributes` and update with `values`, or create a new record if not found. Combines `firstOrCreate` logic with the ability to pass explicit create values.
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
const user = await User.updateOrInsert(
|
|
817
|
+
{ email: "alice@example.com" },
|
|
818
|
+
{ name: "Alice Smith", active: true },
|
|
819
|
+
);
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
#### Model.createMany(records, options?)
|
|
823
|
+
|
|
824
|
+
Create multiple model instances with full ORM support. Fires `creating` / `created` observers by default. Pass `{ events: false }` to bypass observers for better performance.
|
|
825
|
+
|
|
826
|
+
```ts
|
|
827
|
+
const users = await User.createMany([
|
|
828
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
829
|
+
{ name: "Bob", email: "bob@example.com" },
|
|
830
|
+
]);
|
|
831
|
+
|
|
832
|
+
// Bypass observers for bulk create
|
|
833
|
+
await User.createMany(records, { events: false });
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
#### Model.saveMany(models, options?)
|
|
837
|
+
|
|
838
|
+
Persist an array of new or existing model instances. Existing models trigger update observers and update `updated_at`. New models trigger create observers. Auto-increment IDs are preserved when saving new models silently one-by-one where needed. Pass `{ events: false }` to bypass all observers.
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
const users = [
|
|
842
|
+
new User({ name: "Alice" }),
|
|
843
|
+
new User({ id: 5, name: "Bob" }), // existing record
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
await User.saveMany(users);
|
|
847
|
+
|
|
848
|
+
// Bypass observers for bulk save
|
|
849
|
+
await User.saveMany(models, { events: false });
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
#### model.save(options?)
|
|
853
|
+
|
|
854
|
+
Save an existing model instance. Pass `{ events: false }` to bypass `updating` / `updated` observers.
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
user.name = "Charlie";
|
|
858
|
+
await user.save({ events: false });
|
|
746
859
|
```
|
|
747
860
|
|
|
748
861
|
### Default Attributes
|
|
@@ -972,6 +1085,48 @@ class User extends Model {
|
|
|
972
1085
|
const post = await user.latestPost().getResults();
|
|
973
1086
|
```
|
|
974
1087
|
|
|
1088
|
+
### Eager Loading
|
|
1089
|
+
|
|
1090
|
+
Use `with()` to eager load relations for query results:
|
|
1091
|
+
|
|
1092
|
+
```ts
|
|
1093
|
+
const users = await User.with("posts", "profile").get();
|
|
1094
|
+
const posts = await Post.with("author").get();
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Nested relations use dot notation:
|
|
1098
|
+
|
|
1099
|
+
```ts
|
|
1100
|
+
const users = await User.with("posts.comments").get();
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
You can constrain an eager-loaded relation by passing an object whose key is the relation name and whose value is a query callback:
|
|
1104
|
+
|
|
1105
|
+
```ts
|
|
1106
|
+
const users = await User.with({
|
|
1107
|
+
posts: (query) => {
|
|
1108
|
+
query.where("status", "published").orderBy("created_at", "desc");
|
|
1109
|
+
},
|
|
1110
|
+
}).get();
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
Nested eager loads can be constrained too:
|
|
1114
|
+
|
|
1115
|
+
```ts
|
|
1116
|
+
const users = await User.with(
|
|
1117
|
+
{ posts: (query) => query.where("status", "published") },
|
|
1118
|
+
{ "posts.comments": (query) => query.where("approved", true) },
|
|
1119
|
+
).get();
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
Constrained eager loading is also available when loading relations onto an existing model:
|
|
1123
|
+
|
|
1124
|
+
```ts
|
|
1125
|
+
await user.load({
|
|
1126
|
+
posts: (query) => query.where("status", "published"),
|
|
1127
|
+
});
|
|
1128
|
+
```
|
|
1129
|
+
|
|
975
1130
|
### Relation Queries and Aggregates
|
|
976
1131
|
|
|
977
1132
|
Filter models by related records:
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { MySqlGrammar } from "./schema/grammars/MySqlGrammar.js";
|
|
|
14
14
|
export { PostgresGrammar } from "./schema/grammars/PostgresGrammar.js";
|
|
15
15
|
export { Builder } from "./query/Builder.js";
|
|
16
16
|
export { Model, HasMany, BelongsTo, HasOne, HasManyThrough, HasOneThrough } from "./model/Model.js";
|
|
17
|
-
export type { ModelAttributeInput, ModelAttributes, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
|
|
17
|
+
export type { ModelAttributeInput, ModelAttributes, BulkModelOptions, SaveOptions, ModelColumn, ModelColumnValue, ModelConstructor, ModelRelationName, EagerLoadConstraint, EagerLoadDefinition, EagerLoadInput, GlobalScope, CastDefinition, CastsAttributes, } from "./model/Model.js";
|
|
18
18
|
export { ModelNotFoundError } from "./model/ModelNotFoundError.js";
|
|
19
19
|
export { ObserverRegistry, type ObserverContract } from "./model/Observer.js";
|
|
20
20
|
export { MorphMap } from "./model/MorphMap.js";
|
|
@@ -5,6 +5,12 @@ import { BelongsToMany } from "./BelongsToMany.js";
|
|
|
5
5
|
export type ModelConstructor<T extends Model = Model> = (new (...args: any[]) => T) & Omit<typeof Model, "prototype">;
|
|
6
6
|
export type GlobalScope = (builder: Builder<any>, model: ModelConstructor) => void;
|
|
7
7
|
export type LiteralUnion<T extends string> = T | (string & {});
|
|
8
|
+
export type EagerLoadConstraint = (query: Builder<any>) => void | Builder<any>;
|
|
9
|
+
export interface EagerLoadDefinition {
|
|
10
|
+
name: string;
|
|
11
|
+
constraint?: EagerLoadConstraint;
|
|
12
|
+
}
|
|
13
|
+
export type EagerLoadInput = string | EagerLoadDefinition | Record<string, EagerLoadConstraint | undefined>;
|
|
8
14
|
type BaseModelInstanceKey = "$attributes" | "$original" | "$exists" | "$relations" | "$casts" | "$castCache" | "$connection" | "fill" | "setConnection" | "getConnection" | "isFillable" | "getAttribute" | "setAttribute" | "castAttribute" | "serializeCastAttribute" | "mergeCasts" | "getDirty" | "isDirty" | "save" | "updateTimestamps" | "touch" | "increment" | "decrement" | "load" | "delete" | "restore" | "forceDelete" | "refresh" | "toJSON" | "toString" | "freshTimestamp" | "setRelation" | "getRelation" | "hasMany" | "belongsTo" | "hasOne" | "hasManyThrough" | "hasOneThrough" | "belongsToMany" | "morphTo" | "morphOne" | "morphMany" | "morphToMany" | "morphedByMany";
|
|
9
15
|
export type ModelInstanceAttributeKeys<T> = Extract<Exclude<keyof T, BaseModelInstanceKey>, string>;
|
|
10
16
|
export type ModelAttributes<T> = T extends {
|
|
@@ -13,6 +19,13 @@ export type ModelAttributes<T> = T extends {
|
|
|
13
19
|
export type ModelColumn<T> = LiteralUnion<Extract<keyof ModelAttributes<T>, string>>;
|
|
14
20
|
export type ModelColumnValue<T, K> = K extends keyof ModelAttributes<T> ? ModelAttributes<T>[K] : any;
|
|
15
21
|
export type ModelAttributeInput<T> = Partial<ModelAttributes<T>> & Record<string, any>;
|
|
22
|
+
export interface BulkModelOptions {
|
|
23
|
+
chunkSize?: number;
|
|
24
|
+
events?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface SaveOptions {
|
|
27
|
+
events?: boolean;
|
|
28
|
+
}
|
|
16
29
|
export type ModelRelationValue = Relation<any> | MorphTo<any> | MorphOne<any> | MorphMany<any> | MorphToMany<any> | BelongsToMany<any>;
|
|
17
30
|
export type ModelRelationName<T> = LiteralUnion<Extract<{
|
|
18
31
|
[K in keyof T]-?: T[K] extends (...args: any[]) => ModelRelationValue ? K : never;
|
|
@@ -126,8 +139,19 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
126
139
|
static applyGlobalScopes(builder: Builder<any>): void;
|
|
127
140
|
static getQualifiedDeletedAtColumn(): string;
|
|
128
141
|
static shouldAutoGeneratePrimaryKey(): Promise<boolean>;
|
|
142
|
+
static prepareBulkRecords<M extends ModelConstructor>(this: M, records: ModelAttributeInput<InstanceType<M>>[]): Promise<Record<string, any>[]>;
|
|
143
|
+
static prepareBulkRecord<M extends ModelConstructor>(this: M, record: ModelAttributeInput<InstanceType<M>>, options?: {
|
|
144
|
+
touchCreatedAt?: boolean;
|
|
145
|
+
touchUpdatedAt?: boolean;
|
|
146
|
+
generatePrimaryKey?: boolean;
|
|
147
|
+
}): Promise<Record<string, any>>;
|
|
129
148
|
static hydrate<M extends ModelConstructor>(this: M, row: Record<string, any>, connection?: Connection): InstanceType<M>;
|
|
130
149
|
static create<M extends ModelConstructor>(this: M, attributes: ModelAttributeInput<InstanceType<M>>): Promise<InstanceType<M>>;
|
|
150
|
+
static insert<M extends ModelConstructor>(this: M, records: ModelAttributeInput<InstanceType<M>> | ModelAttributeInput<InstanceType<M>>[], options?: Omit<BulkModelOptions, "events">): Promise<any>;
|
|
151
|
+
static upsert<M extends ModelConstructor>(this: M, records: ModelAttributeInput<InstanceType<M>> | ModelAttributeInput<InstanceType<M>>[], uniqueBy: ModelColumn<InstanceType<M>> | ModelColumn<InstanceType<M>>[], updateColumns?: ModelColumn<InstanceType<M>>[], options?: Omit<BulkModelOptions, "events">): Promise<any>;
|
|
152
|
+
static updateOrInsert<M extends ModelConstructor>(this: M, attributes: ModelAttributeInput<InstanceType<M>>, values?: ModelAttributeInput<InstanceType<M>>): Promise<boolean>;
|
|
153
|
+
static createMany<M extends ModelConstructor>(this: M, records: ModelAttributeInput<InstanceType<M>>[], options?: BulkModelOptions): Promise<InstanceType<M>[]>;
|
|
154
|
+
static saveMany<M extends ModelConstructor>(this: M, models: InstanceType<M>[], options?: BulkModelOptions): Promise<InstanceType<M>[]>;
|
|
131
155
|
static find<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M> | null>;
|
|
132
156
|
static findOrFail<M extends ModelConstructor>(this: M, id: any): Promise<InstanceType<M>>;
|
|
133
157
|
static first<M extends ModelConstructor>(this: M): Promise<InstanceType<M> | null>;
|
|
@@ -168,7 +192,7 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
168
192
|
static inRandomOrder<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
169
193
|
static lockForUpdate<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
170
194
|
static sharedLock<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
171
|
-
static with<M extends ModelConstructor>(this: M, ...relations: ModelRelationName<InstanceType<M>>[]): Builder<InstanceType<M>>;
|
|
195
|
+
static with<M extends ModelConstructor>(this: M, ...relations: (ModelRelationName<InstanceType<M>> | EagerLoadInput | EagerLoadInput[])[]): Builder<InstanceType<M>>;
|
|
172
196
|
static withTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
173
197
|
static onlyTrashed<M extends ModelConstructor>(this: M): Builder<InstanceType<M>>;
|
|
174
198
|
static withoutGlobalScope<M extends ModelConstructor>(this: M, scope: string): Builder<InstanceType<M>>;
|
|
@@ -188,8 +212,9 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
188
212
|
static each<M extends ModelConstructor>(this: M, count: number, callback: (item: InstanceType<M>) => void | Promise<void>): Promise<void>;
|
|
189
213
|
static cursor<M extends ModelConstructor>(this: M): AsyncGenerator<InstanceType<M>>;
|
|
190
214
|
static lazy<M extends ModelConstructor>(this: M, count?: number): AsyncGenerator<InstanceType<M>>;
|
|
191
|
-
static
|
|
192
|
-
static
|
|
215
|
+
static normalizeEagerLoads(relations: (EagerLoadInput | EagerLoadInput[])[]): EagerLoadDefinition[];
|
|
216
|
+
static eagerLoadRelations(models: Model[], relations: (string | EagerLoadDefinition)[]): Promise<void>;
|
|
217
|
+
static eagerLoadRelation(models: Model[], relationName: string, constraint?: EagerLoadConstraint): Promise<void>;
|
|
193
218
|
fill(attributes: Partial<T> | ModelAttributeInput<this>): this;
|
|
194
219
|
setConnection(connection: Connection): this;
|
|
195
220
|
getConnection(): Connection;
|
|
@@ -205,12 +230,12 @@ export declare class Model<T extends Record<string, any> = Record<string, any>>
|
|
|
205
230
|
protected resolveCustomCast(cast: CastDefinition): CastsAttributes | null;
|
|
206
231
|
getDirty(): Partial<T>;
|
|
207
232
|
isDirty(): boolean;
|
|
208
|
-
save(): Promise<this>;
|
|
233
|
+
save(options?: SaveOptions): Promise<this>;
|
|
209
234
|
updateTimestamps(): void;
|
|
210
235
|
touch(): Promise<boolean>;
|
|
211
236
|
increment<K extends ModelColumn<this>>(column: K, amount?: number, extra?: ModelAttributeInput<this>): Promise<this>;
|
|
212
237
|
decrement<K extends ModelColumn<this>>(column: K, amount?: number, extra?: ModelAttributeInput<this>): Promise<this>;
|
|
213
|
-
load(...relations: string[]): Promise<this>;
|
|
238
|
+
load(...relations: (string | EagerLoadInput | EagerLoadInput[])[]): Promise<this>;
|
|
214
239
|
delete(): Promise<boolean>;
|
|
215
240
|
restore(): Promise<boolean>;
|
|
216
241
|
forceDelete(): Promise<boolean>;
|
package/dist/src/model/Model.js
CHANGED
|
@@ -433,6 +433,33 @@ export class Model {
|
|
|
433
433
|
const numericTypes = new Set(["integer", "int", "bigint", "smallint", "tinyint", "real", "float", "double", "decimal", "numeric"]);
|
|
434
434
|
return !numericTypes.has(type);
|
|
435
435
|
}
|
|
436
|
+
static async prepareBulkRecords(records) {
|
|
437
|
+
const prepared = [];
|
|
438
|
+
for (const record of records) {
|
|
439
|
+
prepared.push(await this.prepareBulkRecord(record));
|
|
440
|
+
}
|
|
441
|
+
return prepared;
|
|
442
|
+
}
|
|
443
|
+
static async prepareBulkRecord(record, options = {}) {
|
|
444
|
+
const instance = new this();
|
|
445
|
+
instance.fill(record);
|
|
446
|
+
const attributes = { ...instance.$attributes };
|
|
447
|
+
if (this.timestamps) {
|
|
448
|
+
const now = instance.freshTimestamp();
|
|
449
|
+
if (options.touchCreatedAt !== false && attributes.created_at === undefined)
|
|
450
|
+
attributes.created_at = now;
|
|
451
|
+
if (options.touchUpdatedAt !== false && attributes.updated_at === undefined)
|
|
452
|
+
attributes.updated_at = now;
|
|
453
|
+
}
|
|
454
|
+
if (options.generatePrimaryKey !== false) {
|
|
455
|
+
const primaryKey = this.primaryKey;
|
|
456
|
+
const primaryKeyValue = attributes[primaryKey];
|
|
457
|
+
if ((primaryKeyValue === null || primaryKeyValue === undefined || primaryKeyValue === "") && await this.shouldAutoGeneratePrimaryKey()) {
|
|
458
|
+
attributes[primaryKey] = crypto.randomUUID();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return attributes;
|
|
462
|
+
}
|
|
436
463
|
static hydrate(row, connection) {
|
|
437
464
|
const instance = new this();
|
|
438
465
|
instance.$attributes = { ...instance.$attributes, ...row };
|
|
@@ -450,6 +477,100 @@ export class Model {
|
|
|
450
477
|
await instance.save();
|
|
451
478
|
return instance;
|
|
452
479
|
}
|
|
480
|
+
static async insert(records, options = {}) {
|
|
481
|
+
const prepared = await this.prepareBulkRecords(Array.isArray(records) ? records : [records]);
|
|
482
|
+
const chunkSize = options.chunkSize || prepared.length || 1;
|
|
483
|
+
let result;
|
|
484
|
+
for (let i = 0; i < prepared.length; i += chunkSize) {
|
|
485
|
+
result = await this.query().insert(prepared.slice(i, i + chunkSize));
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
static async upsert(records, uniqueBy, updateColumns, options = {}) {
|
|
490
|
+
const prepared = await this.prepareBulkRecords(Array.isArray(records) ? records : [records]);
|
|
491
|
+
const chunkSize = options.chunkSize || prepared.length || 1;
|
|
492
|
+
let columns = updateColumns;
|
|
493
|
+
if (!columns && this.timestamps) {
|
|
494
|
+
const uniqueColumns = new Set(Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy]);
|
|
495
|
+
columns = Object.keys(prepared[0] || {}).filter((column) => column !== "created_at" && !uniqueColumns.has(column));
|
|
496
|
+
}
|
|
497
|
+
let result;
|
|
498
|
+
for (let i = 0; i < prepared.length; i += chunkSize) {
|
|
499
|
+
result = await this.query().upsert(prepared.slice(i, i + chunkSize), uniqueBy, columns);
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
static async updateOrInsert(attributes, values = {}) {
|
|
504
|
+
const existing = await this.where(attributes).first();
|
|
505
|
+
if (existing) {
|
|
506
|
+
const update = await this.prepareBulkRecord(values, { touchUpdatedAt: true, touchCreatedAt: false, generatePrimaryKey: false });
|
|
507
|
+
await this.where(attributes).update(update);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
await this.insert({ ...attributes, ...values });
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
static async createMany(records, options = {}) {
|
|
514
|
+
const models = records.map((attributes) => new this(attributes));
|
|
515
|
+
await this.saveMany(models, options);
|
|
516
|
+
return models;
|
|
517
|
+
}
|
|
518
|
+
static async saveMany(models, options = {}) {
|
|
519
|
+
const chunkSize = options.chunkSize || models.length || 1;
|
|
520
|
+
const events = options.events !== false;
|
|
521
|
+
if (events) {
|
|
522
|
+
for (const model of models) {
|
|
523
|
+
await model.save();
|
|
524
|
+
}
|
|
525
|
+
return models;
|
|
526
|
+
}
|
|
527
|
+
for (let i = 0; i < models.length; i += chunkSize) {
|
|
528
|
+
const chunk = models.slice(i, i + chunkSize);
|
|
529
|
+
const newModels = chunk.filter((model) => !model.$exists);
|
|
530
|
+
const existingModels = chunk.filter((model) => model.$exists);
|
|
531
|
+
if (newModels.length > 0) {
|
|
532
|
+
const shouldGeneratePrimaryKey = await this.shouldAutoGeneratePrimaryKey();
|
|
533
|
+
const bulkModels = [];
|
|
534
|
+
for (const model of newModels) {
|
|
535
|
+
const pk = model.getAttribute(this.primaryKey);
|
|
536
|
+
if (!shouldGeneratePrimaryKey && (pk === null || pk === undefined || pk === "")) {
|
|
537
|
+
const record = await this.prepareBulkRecord(model.$attributes);
|
|
538
|
+
const id = await this.query().insertGetId(record);
|
|
539
|
+
if (id)
|
|
540
|
+
record[this.primaryKey] = id;
|
|
541
|
+
model.$attributes = record;
|
|
542
|
+
model.$original = { ...record };
|
|
543
|
+
model.$exists = true;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
bulkModels.push(model);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (bulkModels.length > 0) {
|
|
550
|
+
const records = await this.prepareBulkRecords(bulkModels.map((model) => model.$attributes));
|
|
551
|
+
await this.query().insert(records);
|
|
552
|
+
for (let index = 0; index < bulkModels.length; index++) {
|
|
553
|
+
bulkModels[index].$attributes = records[index];
|
|
554
|
+
bulkModels[index].$original = { ...records[index] };
|
|
555
|
+
bulkModels[index].$exists = true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
for (const model of existingModels) {
|
|
560
|
+
let dirty = model.getDirty();
|
|
561
|
+
if (Object.keys(dirty).length > 0 && this.timestamps) {
|
|
562
|
+
model.$attributes.updated_at = model.freshTimestamp();
|
|
563
|
+
delete model.$castCache.updated_at;
|
|
564
|
+
dirty = model.getDirty();
|
|
565
|
+
}
|
|
566
|
+
if (Object.keys(dirty).length === 0)
|
|
567
|
+
continue;
|
|
568
|
+
await this.query().where(this.primaryKey, model.getAttribute(this.primaryKey)).update(dirty);
|
|
569
|
+
model.$original = { ...model.$attributes };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return models;
|
|
573
|
+
}
|
|
453
574
|
static async find(id) {
|
|
454
575
|
return this.query().find(id, this.primaryKey);
|
|
455
576
|
}
|
|
@@ -629,34 +750,65 @@ export class Model {
|
|
|
629
750
|
static lazy(count) {
|
|
630
751
|
return this.query().lazy(count);
|
|
631
752
|
}
|
|
632
|
-
static
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (Array.isArray(related))
|
|
641
|
-
nestedModels.push(...related);
|
|
642
|
-
else if (related)
|
|
643
|
-
nestedModels.push(related);
|
|
644
|
-
}
|
|
645
|
-
if (nestedModels.length > 0) {
|
|
646
|
-
await this.eagerLoadRelations(nestedModels, [rest.join(".")]);
|
|
647
|
-
}
|
|
753
|
+
static normalizeEagerLoads(relations) {
|
|
754
|
+
const normalized = [];
|
|
755
|
+
for (const relation of relations.flat()) {
|
|
756
|
+
if (typeof relation === "string") {
|
|
757
|
+
normalized.push({ name: relation });
|
|
758
|
+
}
|
|
759
|
+
else if ("name" in relation && typeof relation.name === "string") {
|
|
760
|
+
normalized.push(relation);
|
|
648
761
|
}
|
|
649
762
|
else {
|
|
650
|
-
|
|
763
|
+
for (const [name, constraint] of Object.entries(relation)) {
|
|
764
|
+
normalized.push({ name, constraint });
|
|
765
|
+
}
|
|
651
766
|
}
|
|
652
767
|
}
|
|
768
|
+
return normalized;
|
|
653
769
|
}
|
|
654
|
-
static async
|
|
770
|
+
static async eagerLoadRelations(models, relations) {
|
|
771
|
+
const normalized = this.normalizeEagerLoads(relations);
|
|
772
|
+
const groups = new Map();
|
|
773
|
+
for (const definition of normalized) {
|
|
774
|
+
const [first] = definition.name.split(".");
|
|
775
|
+
const group = groups.get(first) || [];
|
|
776
|
+
group.push(definition);
|
|
777
|
+
groups.set(first, group);
|
|
778
|
+
}
|
|
779
|
+
for (const [relationName, definitions] of groups) {
|
|
780
|
+
const direct = definitions.find((definition) => definition.name === relationName);
|
|
781
|
+
await this.eagerLoadRelation(models, relationName, direct?.constraint);
|
|
782
|
+
const nestedDefinitions = definitions
|
|
783
|
+
.filter((definition) => definition.name.includes("."))
|
|
784
|
+
.map((definition) => ({
|
|
785
|
+
name: definition.name.split(".").slice(1).join("."),
|
|
786
|
+
constraint: definition.constraint,
|
|
787
|
+
}));
|
|
788
|
+
if (nestedDefinitions.length === 0)
|
|
789
|
+
continue;
|
|
790
|
+
const nestedModels = [];
|
|
791
|
+
for (const model of models) {
|
|
792
|
+
const related = model.getRelation(relationName);
|
|
793
|
+
if (Array.isArray(related))
|
|
794
|
+
nestedModels.push(...related);
|
|
795
|
+
else if (related)
|
|
796
|
+
nestedModels.push(related);
|
|
797
|
+
}
|
|
798
|
+
if (nestedModels.length > 0) {
|
|
799
|
+
await this.eagerLoadRelations(nestedModels, nestedDefinitions);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
static async eagerLoadRelation(models, relationName, constraint) {
|
|
655
804
|
if (models.length === 0)
|
|
656
805
|
return;
|
|
657
806
|
const firstModel = models[0];
|
|
658
807
|
const relation = firstModel[relationName]();
|
|
659
808
|
relation.addEagerConstraints(models);
|
|
809
|
+
if (constraint) {
|
|
810
|
+
constraint(relation.getQuery());
|
|
811
|
+
}
|
|
660
812
|
const results = await relation.getEager();
|
|
661
813
|
relation.match(models, results, relationName);
|
|
662
814
|
}
|
|
@@ -805,10 +957,12 @@ export class Model {
|
|
|
805
957
|
isDirty() {
|
|
806
958
|
return Object.keys(this.getDirty()).length > 0;
|
|
807
959
|
}
|
|
808
|
-
async save() {
|
|
960
|
+
async save(options = {}) {
|
|
809
961
|
const constructor = this.constructor;
|
|
962
|
+
const events = options.events !== false;
|
|
810
963
|
if (this.$exists) {
|
|
811
|
-
|
|
964
|
+
if (events)
|
|
965
|
+
await ObserverRegistry.dispatch("saving", this);
|
|
812
966
|
let dirty = this.getDirty();
|
|
813
967
|
if (Object.keys(dirty).length > 0 && constructor.timestamps) {
|
|
814
968
|
this.$attributes["updated_at"] = this.freshTimestamp();
|
|
@@ -816,20 +970,25 @@ export class Model {
|
|
|
816
970
|
dirty = this.getDirty();
|
|
817
971
|
}
|
|
818
972
|
if (Object.keys(dirty).length > 0) {
|
|
819
|
-
|
|
973
|
+
if (events)
|
|
974
|
+
await ObserverRegistry.dispatch("updating", this);
|
|
820
975
|
const pk = this.getAttribute(constructor.primaryKey);
|
|
821
976
|
const connection = this.getConnection();
|
|
822
977
|
await new Builder(connection, connection.qualifyTable(constructor.getTable()))
|
|
823
978
|
.where(constructor.primaryKey, pk)
|
|
824
979
|
.update(dirty);
|
|
825
|
-
|
|
980
|
+
if (events)
|
|
981
|
+
await ObserverRegistry.dispatch("updated", this);
|
|
826
982
|
}
|
|
827
983
|
this.$original = { ...this.$attributes };
|
|
828
|
-
|
|
984
|
+
if (events)
|
|
985
|
+
await ObserverRegistry.dispatch("saved", this);
|
|
829
986
|
}
|
|
830
987
|
else {
|
|
831
|
-
|
|
832
|
-
|
|
988
|
+
if (events)
|
|
989
|
+
await ObserverRegistry.dispatch("creating", this);
|
|
990
|
+
if (events)
|
|
991
|
+
await ObserverRegistry.dispatch("saving", this);
|
|
833
992
|
if (constructor.timestamps) {
|
|
834
993
|
const now = this.freshTimestamp();
|
|
835
994
|
this.$attributes["created_at"] = now;
|
|
@@ -858,8 +1017,10 @@ export class Model {
|
|
|
858
1017
|
}
|
|
859
1018
|
this.$exists = true;
|
|
860
1019
|
this.$original = { ...this.$attributes };
|
|
861
|
-
|
|
862
|
-
|
|
1020
|
+
if (events)
|
|
1021
|
+
await ObserverRegistry.dispatch("created", this);
|
|
1022
|
+
if (events)
|
|
1023
|
+
await ObserverRegistry.dispatch("saved", this);
|
|
863
1024
|
}
|
|
864
1025
|
const identityMap = IdentityMap.current();
|
|
865
1026
|
if (identityMap) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Connection } from "../connection/Connection.js";
|
|
2
2
|
import type { WhereClause, OrderClause, HavingClause, UnionClause } from "../types/index.js";
|
|
3
|
-
import type { ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor
|
|
3
|
+
import type { EagerLoadDefinition, EagerLoadInput, ModelAttributeInput, ModelColumn, ModelColumnValue, ModelConstructor } from "../model/Model.js";
|
|
4
4
|
type RelationConstraint = (query: Builder<any>) => void | Builder<any>;
|
|
5
5
|
export interface Paginator<T> {
|
|
6
6
|
data: T[];
|
|
@@ -24,7 +24,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
24
24
|
joins: string[];
|
|
25
25
|
distinctFlag: boolean;
|
|
26
26
|
model?: ModelConstructor;
|
|
27
|
-
eagerLoads:
|
|
27
|
+
eagerLoads: EagerLoadDefinition[];
|
|
28
28
|
randomOrderFlag: boolean;
|
|
29
29
|
lockMode?: string;
|
|
30
30
|
unions: UnionClause[];
|
|
@@ -36,6 +36,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
36
36
|
constructor(connection: Connection, table: string);
|
|
37
37
|
private get grammar();
|
|
38
38
|
private invalidateSqlCache;
|
|
39
|
+
private normalizeEagerLoads;
|
|
39
40
|
setModel(model: ModelConstructor): this;
|
|
40
41
|
table(table: string): this;
|
|
41
42
|
select(...columns: ModelColumn<T>[]): this;
|
|
@@ -106,7 +107,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
106
107
|
crossJoin(table: string): this;
|
|
107
108
|
union(query: Builder<T> | string, all?: boolean): this;
|
|
108
109
|
unionAll(query: Builder<T> | string): this;
|
|
109
|
-
with(...relations:
|
|
110
|
+
with(...relations: (EagerLoadInput | EagerLoadInput[])[]): this;
|
|
110
111
|
withoutGlobalScope(scope: string): this;
|
|
111
112
|
withoutGlobalScopes(): this;
|
|
112
113
|
withTrashed(): this;
|
|
@@ -32,6 +32,23 @@ export class Builder {
|
|
|
32
32
|
invalidateSqlCache() {
|
|
33
33
|
this.sqlCache = undefined;
|
|
34
34
|
}
|
|
35
|
+
normalizeEagerLoads(relations) {
|
|
36
|
+
const normalized = [];
|
|
37
|
+
for (const relation of relations.flat()) {
|
|
38
|
+
if (typeof relation === "string") {
|
|
39
|
+
normalized.push({ name: relation });
|
|
40
|
+
}
|
|
41
|
+
else if ("name" in relation && typeof relation.name === "string") {
|
|
42
|
+
normalized.push(relation);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
for (const [name, constraint] of Object.entries(relation)) {
|
|
46
|
+
normalized.push({ name, constraint });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
35
52
|
setModel(model) {
|
|
36
53
|
this.model = model;
|
|
37
54
|
return this;
|
|
@@ -332,7 +349,7 @@ export class Builder {
|
|
|
332
349
|
return this.union(query, true);
|
|
333
350
|
}
|
|
334
351
|
with(...relations) {
|
|
335
|
-
this.eagerLoads.push(...relations);
|
|
352
|
+
this.eagerLoads.push(...this.normalizeEagerLoads(relations));
|
|
336
353
|
return this;
|
|
337
354
|
}
|
|
338
355
|
withoutGlobalScope(scope) {
|
package/package.json
CHANGED