ilana-orm 1.0.14 → 1.0.16
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 +178 -115
- package/database/connection.js +21 -10
- package/database/schema-builder.js +18 -12
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/orm/Factory.js +20 -18
- package/orm/Model.d.ts +26 -3
- package/orm/Model.js +159 -68
- package/orm/QueryBuilder.d.ts +24 -2
- package/orm/QueryBuilder.js +437 -75
- package/orm/Relation.d.ts +15 -3
- package/orm/Relation.js +74 -11
- package/orm/Relation.mjs +1 -1
- package/package.json +10 -9
package/orm/Factory.js
CHANGED
|
@@ -7,14 +7,14 @@ class Factory {
|
|
|
7
7
|
this.definition = definition;
|
|
8
8
|
this.faker = faker;
|
|
9
9
|
this.states = new Map();
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
10
|
+
this._afterCreatingCallbacks = [];
|
|
11
|
+
this._afterMakingCallbacks = [];
|
|
12
|
+
this._beforeCreatingCallbacks = [];
|
|
13
|
+
this._beforeMakingCallbacks = [];
|
|
14
14
|
this.count = 1;
|
|
15
15
|
this.currentStates = [];
|
|
16
16
|
this.relationships = new Map();
|
|
17
|
-
this.
|
|
17
|
+
this._sequenceCount = 0;
|
|
18
18
|
this.sequences = new Map();
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -28,22 +28,22 @@ class Factory {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
afterCreating(callback) {
|
|
31
|
-
this.
|
|
31
|
+
this._afterCreatingCallbacks.push(callback);
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
afterMaking(callback) {
|
|
36
|
-
this.
|
|
36
|
+
this._afterMakingCallbacks.push(callback);
|
|
37
37
|
return this;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
beforeCreating(callback) {
|
|
41
|
-
this.
|
|
41
|
+
this._beforeCreatingCallbacks.push(callback);
|
|
42
42
|
return this;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
beforeMaking(callback) {
|
|
46
|
-
this.
|
|
46
|
+
this._beforeMakingCallbacks.push(callback);
|
|
47
47
|
return this;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -75,7 +75,7 @@ class Factory {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
sequence() {
|
|
78
|
-
return ++this.
|
|
78
|
+
return ++this._sequenceCount;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Enhanced sequence methods
|
|
@@ -92,7 +92,7 @@ class Factory {
|
|
|
92
92
|
this.sequences?.set(key, 0);
|
|
93
93
|
} else {
|
|
94
94
|
this.sequences?.clear();
|
|
95
|
-
this.
|
|
95
|
+
this._sequenceCount = 0;
|
|
96
96
|
}
|
|
97
97
|
return this;
|
|
98
98
|
}
|
|
@@ -138,8 +138,6 @@ class Factory {
|
|
|
138
138
|
|
|
139
139
|
if (typeof this.definition === 'function') {
|
|
140
140
|
modelAttributes = this.definition(faker);
|
|
141
|
-
} else if (this.definition === undefined && typeof this.definition === 'function') {
|
|
142
|
-
modelAttributes = this.definition();
|
|
143
141
|
} else {
|
|
144
142
|
throw new Error('Factory must have a definition function');
|
|
145
143
|
}
|
|
@@ -153,7 +151,7 @@ class Factory {
|
|
|
153
151
|
|
|
154
152
|
modelAttributes = { ...modelAttributes, ...attributes };
|
|
155
153
|
|
|
156
|
-
for (const callback of this.
|
|
154
|
+
for (const callback of this._beforeMakingCallbacks) {
|
|
157
155
|
modelAttributes = callback(modelAttributes) || modelAttributes;
|
|
158
156
|
}
|
|
159
157
|
|
|
@@ -167,7 +165,7 @@ class Factory {
|
|
|
167
165
|
model.fill(modelAttributes);
|
|
168
166
|
|
|
169
167
|
// Run afterMaking callbacks
|
|
170
|
-
for (const callback of this.
|
|
168
|
+
for (const callback of this._afterMakingCallbacks) {
|
|
171
169
|
await callback(model);
|
|
172
170
|
}
|
|
173
171
|
|
|
@@ -178,14 +176,14 @@ class Factory {
|
|
|
178
176
|
const model = await this.makeOne(attributes);
|
|
179
177
|
|
|
180
178
|
// Run beforeCreating callbacks
|
|
181
|
-
for (const callback of this.
|
|
179
|
+
for (const callback of this._beforeCreatingCallbacks) {
|
|
182
180
|
await callback(model);
|
|
183
181
|
}
|
|
184
|
-
|
|
182
|
+
|
|
185
183
|
await model.save();
|
|
186
184
|
|
|
187
185
|
// Run afterCreating callbacks
|
|
188
|
-
for (const callback of this.
|
|
186
|
+
for (const callback of this._afterCreatingCallbacks) {
|
|
189
187
|
await callback(model);
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -243,6 +241,8 @@ class Factory {
|
|
|
243
241
|
const batchFactory = new Factory(this.model, this.definition);
|
|
244
242
|
batchFactory.count = currentBatchSize;
|
|
245
243
|
batchFactory.currentStates = [...this.currentStates];
|
|
244
|
+
batchFactory._afterCreatingCallbacks = [...this._afterCreatingCallbacks];
|
|
245
|
+
batchFactory._beforeCreatingCallbacks = [...this._beforeCreatingCallbacks];
|
|
246
246
|
|
|
247
247
|
const batch = await batchFactory.create(attributes);
|
|
248
248
|
results.push(...(Array.isArray(batch) ? batch : [batch]));
|
|
@@ -352,6 +352,8 @@ if (typeof Model !== 'undefined') {
|
|
|
352
352
|
// Return a new instance to avoid state pollution
|
|
353
353
|
const newFactory = new Factory(this, existingFactory.definition);
|
|
354
354
|
newFactory.states = new Map(existingFactory.states);
|
|
355
|
+
newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
|
|
356
|
+
newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
|
|
355
357
|
return newFactory;
|
|
356
358
|
}
|
|
357
359
|
throw new Error(`No factory defined for model: ${this.name}`);
|
package/orm/Model.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import QueryBuilder from './QueryBuilder';
|
|
2
|
-
import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany } from './Relation';
|
|
2
|
+
import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphOne, MorphMany } from './Relation';
|
|
3
3
|
|
|
4
4
|
export interface ModelAttributes {
|
|
5
5
|
[key: string]: any;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface CastInstance {
|
|
9
|
+
get(value: any): any;
|
|
10
|
+
set(value: any): any;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface ModelCasts {
|
|
9
|
-
[key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float';
|
|
14
|
+
[key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float' | CastInstance;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export interface ModelEvents {
|
|
@@ -26,7 +31,7 @@ export interface Observer {
|
|
|
26
31
|
restored?(model: any): Promise<void> | void;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
export default class Model {
|
|
34
|
+
export default class Model<TAttributes extends ModelAttributes = ModelAttributes> {
|
|
30
35
|
// Static properties
|
|
31
36
|
protected static table: string;
|
|
32
37
|
protected static connection?: string;
|
|
@@ -34,7 +39,10 @@ export default class Model {
|
|
|
34
39
|
protected static keyType: 'number' | 'string';
|
|
35
40
|
protected static incrementing: boolean;
|
|
36
41
|
protected static timestamps: boolean;
|
|
42
|
+
protected static createdAt: string;
|
|
43
|
+
protected static updatedAt: string;
|
|
37
44
|
protected static softDeletes: boolean;
|
|
45
|
+
protected static deletedAt: string;
|
|
38
46
|
protected static fillable: string[];
|
|
39
47
|
protected static guarded: string[];
|
|
40
48
|
protected static casts: ModelCasts;
|
|
@@ -64,6 +72,7 @@ export default class Model {
|
|
|
64
72
|
static resolveRelatedModel(related: string | typeof Model): typeof Model;
|
|
65
73
|
static query(): QueryBuilder;
|
|
66
74
|
static with(...relations: string[]): QueryBuilder;
|
|
75
|
+
static withCount(...relations: string[]): QueryBuilder;
|
|
67
76
|
static on(connectionOrTrx: string | any): QueryBuilder;
|
|
68
77
|
static all(): Promise<Model[]>;
|
|
69
78
|
static find(id: any): Promise<Model | null>;
|
|
@@ -72,6 +81,10 @@ export default class Model {
|
|
|
72
81
|
static firstOrFail(): Promise<Model>;
|
|
73
82
|
static latest(column?: string): QueryBuilder;
|
|
74
83
|
static oldest(column?: string): QueryBuilder;
|
|
84
|
+
static withTrashed(): QueryBuilder;
|
|
85
|
+
static onlyTrashed(): QueryBuilder;
|
|
86
|
+
static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
87
|
+
static withoutGlobalScopes(): QueryBuilder;
|
|
75
88
|
static make(attributes?: ModelAttributes): Model;
|
|
76
89
|
static create(attributes?: ModelAttributes): Promise<Model>;
|
|
77
90
|
static generateUuid(): string;
|
|
@@ -111,10 +124,19 @@ export default class Model {
|
|
|
111
124
|
// Instance methods
|
|
112
125
|
getKey(): any;
|
|
113
126
|
fill(attributes: ModelAttributes): this;
|
|
127
|
+
load(...relations: string[]): Promise<this>;
|
|
128
|
+
loadMissing(...relations: string[]): Promise<this>;
|
|
129
|
+
getRelation(key: string): any;
|
|
130
|
+
relationLoaded(key: string): boolean;
|
|
131
|
+
makeHidden(keys: string | string[]): this;
|
|
132
|
+
makeVisible(keys: string | string[]): this;
|
|
133
|
+
append(keys: string | string[]): this;
|
|
114
134
|
isFillable(key: string): boolean;
|
|
115
135
|
getAttribute(key: string): any;
|
|
116
136
|
setAttribute(key: string, value: any): this;
|
|
117
137
|
syncOriginal(): void;
|
|
138
|
+
getOriginal(key: string): any;
|
|
139
|
+
getOriginal(): ModelAttributes;
|
|
118
140
|
save(): Promise<boolean>;
|
|
119
141
|
update(attributes?: ModelAttributes): Promise<boolean>;
|
|
120
142
|
isDirty(key?: string): boolean;
|
|
@@ -148,6 +170,7 @@ export default class Model {
|
|
|
148
170
|
secondLocalKey?: string
|
|
149
171
|
): HasManyThrough;
|
|
150
172
|
morphTo(typeColumn?: string, idColumn?: string): MorphTo;
|
|
173
|
+
morphOne(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphOne;
|
|
151
174
|
morphMany(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphMany;
|
|
152
175
|
|
|
153
176
|
// Protected methods
|
package/orm/Model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Model.js
|
|
2
2
|
const QueryBuilder = require('./QueryBuilder');
|
|
3
|
-
const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany } = require('./Relation');
|
|
3
|
+
const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany, MorphOne } = require('./Relation');
|
|
4
4
|
const ModelRegistry = require('./ModelRegistry');
|
|
5
5
|
const Database = require('../database/connection');
|
|
6
6
|
|
|
@@ -86,7 +86,8 @@ class Model {
|
|
|
86
86
|
const allKeys = new Set([
|
|
87
87
|
...Object.keys(this.attributes || {}),
|
|
88
88
|
...this.fillable,
|
|
89
|
-
...(this._deferred ? Object.keys(this._deferred) : [])
|
|
89
|
+
...(this._deferred ? Object.keys(this._deferred) : []),
|
|
90
|
+
...(this.appends || [])
|
|
90
91
|
]);
|
|
91
92
|
|
|
92
93
|
for (const key of allKeys) {
|
|
@@ -105,6 +106,16 @@ class Model {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
_toPascalCase(key) {
|
|
110
|
+
return key.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_selfFk() {
|
|
114
|
+
return this.constructor.name
|
|
115
|
+
.replace(/([A-Z])/g, (m, l, i) => i === 0 ? l.toLowerCase() : '_' + l.toLowerCase())
|
|
116
|
+
+ '_id';
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
// --- Static registry & resolving ---
|
|
109
120
|
static register() {
|
|
110
121
|
ModelRegistry.register(this.name, this);
|
|
@@ -150,6 +161,7 @@ class Model {
|
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
static with(...rels) { return this.query().with(...rels); }
|
|
164
|
+
static withCount(...rels) { return this.query().withCount(...rels); }
|
|
153
165
|
static on(connectionOrTrx) {
|
|
154
166
|
if (connectionOrTrx && typeof connectionOrTrx.raw === 'function') {
|
|
155
167
|
// It's a transaction object
|
|
@@ -166,8 +178,14 @@ class Model {
|
|
|
166
178
|
static async findBy(column, value) { return this.query().where(column, value).first(); }
|
|
167
179
|
static async first() { return this.query().first(); }
|
|
168
180
|
static async firstOrFail() { return this.query().firstOrFail(); }
|
|
169
|
-
static latest(col) { return this.query().latest(col || 'created_at'); }
|
|
170
|
-
static oldest(col) { return this.query().oldest(col || 'created_at'); }
|
|
181
|
+
static latest(col) { return this.query().latest(col || this.createdAt || 'created_at'); }
|
|
182
|
+
static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
|
|
183
|
+
static withTrashed() { return this.query().withTrashed(); }
|
|
184
|
+
static onlyTrashed() { return this.query().onlyTrashed(); }
|
|
185
|
+
static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
|
|
186
|
+
static withoutGlobalScopes() {
|
|
187
|
+
return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
188
|
+
}
|
|
171
189
|
|
|
172
190
|
static make(attrs = {}) {
|
|
173
191
|
const inst = new this(attrs);
|
|
@@ -192,7 +210,16 @@ class Model {
|
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
static async insert(data) { return this.query().insert(data); }
|
|
195
|
-
static async destroy(ids) {
|
|
213
|
+
static async destroy(ids) {
|
|
214
|
+
const idList = Array.isArray(ids) ? ids : [ids];
|
|
215
|
+
if (this.softDeletes) {
|
|
216
|
+
const models = await this.query().whereIn(this.primaryKey, idList).get();
|
|
217
|
+
let count = 0;
|
|
218
|
+
for (const model of models) { await model.delete(); count++; }
|
|
219
|
+
return count;
|
|
220
|
+
}
|
|
221
|
+
return this.query().whereIn(this.primaryKey, idList).delete();
|
|
222
|
+
}
|
|
196
223
|
static async firstOrCreate(a, v) { return (await this.query().where(a).first()) || this.create({ ...a, ...v }); }
|
|
197
224
|
static async firstOrNew(a, v) {
|
|
198
225
|
const existing = await this.query().where(a).first();
|
|
@@ -211,8 +238,13 @@ class Model {
|
|
|
211
238
|
}
|
|
212
239
|
|
|
213
240
|
// scopes
|
|
214
|
-
static addGlobalScope(n, s) {
|
|
215
|
-
|
|
241
|
+
static addGlobalScope(n, s) {
|
|
242
|
+
if (!Object.hasOwn(this, 'globalScopes')) this.globalScopes = new Map();
|
|
243
|
+
this.globalScopes.set(n, s);
|
|
244
|
+
}
|
|
245
|
+
static removeGlobalScope(n) {
|
|
246
|
+
if (Object.hasOwn(this, 'globalScopes')) this.globalScopes.delete(n);
|
|
247
|
+
}
|
|
216
248
|
static withoutGlobalScope(n) {
|
|
217
249
|
const qb = new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
218
250
|
const scopes = new Map(this.globalScopes);
|
|
@@ -220,10 +252,17 @@ class Model {
|
|
|
220
252
|
scopes.forEach(s => s(qb));
|
|
221
253
|
return qb;
|
|
222
254
|
}
|
|
223
|
-
static applyGlobalScopes(qb) {
|
|
255
|
+
static applyGlobalScopes(qb) {
|
|
256
|
+
const scopes = Object.hasOwn(this, 'globalScopes') ? this.globalScopes : new Map();
|
|
257
|
+
scopes.forEach(s => s(qb));
|
|
258
|
+
}
|
|
224
259
|
|
|
225
260
|
// events
|
|
226
|
-
static _addEventHandler(evt, fn) {
|
|
261
|
+
static _addEventHandler(evt, fn) {
|
|
262
|
+
if (!Object.hasOwn(this, 'events')) this.events = {};
|
|
263
|
+
this.events[evt] = this.events[evt] || [];
|
|
264
|
+
this.events[evt].push(fn);
|
|
265
|
+
}
|
|
227
266
|
static creating(fn) { this._addEventHandler('creating', fn); }
|
|
228
267
|
static created(fn) { this._addEventHandler('created', fn); }
|
|
229
268
|
static updating(fn) { this._addEventHandler('updating', fn); }
|
|
@@ -254,7 +293,8 @@ class Model {
|
|
|
254
293
|
}
|
|
255
294
|
// static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
|
|
256
295
|
static async fireEvent(evt, mdl) {
|
|
257
|
-
const
|
|
296
|
+
const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
|
|
297
|
+
const handlers = ownEvents[evt] || [];
|
|
258
298
|
for (const handler of handlers) {
|
|
259
299
|
if (await handler(mdl) === false) return false;
|
|
260
300
|
}
|
|
@@ -278,6 +318,46 @@ class Model {
|
|
|
278
318
|
return this;
|
|
279
319
|
}
|
|
280
320
|
|
|
321
|
+
async load(...relations) {
|
|
322
|
+
const qb = new QueryBuilder(this.constructor.getTableName(), this.constructor, this.constructor.getConnectionName());
|
|
323
|
+
qb.eagerLoad = relations.flat();
|
|
324
|
+
await qb.loadRelations([this]);
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async loadMissing(...relations) {
|
|
329
|
+
const toLoad = relations.flat().filter(r => !(r.split('.')[0] in this.relations));
|
|
330
|
+
if (toLoad.length) await this.load(...toLoad);
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getRelation(key) {
|
|
335
|
+
return this.relations[key];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
relationLoaded(key) {
|
|
339
|
+
return Object.prototype.hasOwnProperty.call(this.relations, key);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
makeHidden(keys) {
|
|
343
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
344
|
+
this.hidden = [...(this.hidden || []), ...list];
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
makeVisible(keys) {
|
|
349
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
350
|
+
this.hidden = (this.hidden || []).filter(k => !list.includes(k));
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
append(keys) {
|
|
355
|
+
const list = Array.isArray(keys) ? keys : [keys];
|
|
356
|
+
this.appends = [...(this.appends || []), ...list];
|
|
357
|
+
this._createAttributeGetters();
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
281
361
|
isFillable(k) {
|
|
282
362
|
if (Array.isArray(this.fillable) && this.fillable.length) return this.fillable.includes(k);
|
|
283
363
|
if (this.guarded.includes('*')) return false;
|
|
@@ -285,36 +365,36 @@ class Model {
|
|
|
285
365
|
}
|
|
286
366
|
|
|
287
367
|
getAttribute(k) {
|
|
368
|
+
const accessor = `get${this._toPascalCase(k)}Attribute`;
|
|
369
|
+
if (typeof this[accessor] === 'function') {
|
|
370
|
+
return this[accessor]();
|
|
371
|
+
}
|
|
288
372
|
const val = this.attributes[k];
|
|
289
373
|
const cast = this.casts[k];
|
|
374
|
+
if (cast && typeof cast === 'object' && typeof cast.get === 'function') {
|
|
375
|
+
return cast.get(val);
|
|
376
|
+
}
|
|
290
377
|
if (cast === 'json' || cast === 'array') {
|
|
291
378
|
try { return JSON.parse(val); } catch { return val; }
|
|
292
379
|
}
|
|
293
|
-
if (cast === 'date' && val != null)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
// Try moment-timezone first
|
|
299
|
-
const moment = require('moment-timezone');
|
|
300
|
-
// Parse the stored value as if it's in the configured timezone
|
|
301
|
-
return moment.tz(val, timezone).format('YYYY-MM-DD HH:mm:ss');
|
|
302
|
-
} catch (e) {
|
|
303
|
-
// Fallback: return the stored value as-is since it's already in the correct timezone
|
|
304
|
-
return val;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
380
|
+
if (cast === 'date' && val != null) return val;
|
|
381
|
+
if (cast === 'boolean') return val == null ? val : Boolean(val);
|
|
382
|
+
if (cast === 'number' || cast === 'float') return val == null ? val : Number(val);
|
|
307
383
|
return val;
|
|
308
384
|
}
|
|
309
385
|
|
|
310
386
|
setAttribute(k, v) {
|
|
387
|
+
const mutator = `set${this._toPascalCase(k)}Attribute`;
|
|
388
|
+
if (typeof this[mutator] === 'function') {
|
|
389
|
+
v = this[mutator](v);
|
|
390
|
+
}
|
|
311
391
|
const cast = this.casts[k];
|
|
312
392
|
let val = v;
|
|
313
|
-
if (cast === '
|
|
393
|
+
if (cast && typeof cast === 'object' && typeof cast.set === 'function') {
|
|
394
|
+
val = cast.set(v);
|
|
395
|
+
} else if (cast === 'json' || cast === 'array') {
|
|
314
396
|
val = typeof v === 'string' ? v : JSON.stringify(v);
|
|
315
|
-
}
|
|
316
|
-
if (cast === 'date' && v instanceof Date) {
|
|
317
|
-
// Format date for database storage (YYYY-MM-DD HH:mm:ss)
|
|
397
|
+
} else if (cast === 'date' && v instanceof Date) {
|
|
318
398
|
const year = v.getFullYear();
|
|
319
399
|
const month = String(v.getMonth() + 1).padStart(2, '0');
|
|
320
400
|
const day = String(v.getDate()).padStart(2, '0');
|
|
@@ -324,7 +404,6 @@ class Model {
|
|
|
324
404
|
val = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
325
405
|
}
|
|
326
406
|
this.attributes[k] = val;
|
|
327
|
-
// Only mark as dirty if not during initialization and model exists
|
|
328
407
|
if (!this._deferred && this.exists) {
|
|
329
408
|
this._dirty.add(k);
|
|
330
409
|
}
|
|
@@ -336,39 +415,34 @@ class Model {
|
|
|
336
415
|
this._dirty.clear();
|
|
337
416
|
}
|
|
338
417
|
|
|
418
|
+
getOriginal(key) {
|
|
419
|
+
return key !== undefined ? this.original[key] : { ...this.original };
|
|
420
|
+
}
|
|
421
|
+
|
|
339
422
|
_getCurrentTimestamp() {
|
|
423
|
+
const now = new Date();
|
|
340
424
|
const config = this._getConfig();
|
|
341
425
|
const timezone = this.constructor.timezone || config?.timezone || 'UTC';
|
|
342
|
-
|
|
426
|
+
|
|
427
|
+
if (timezone === 'UTC') return now;
|
|
428
|
+
|
|
343
429
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return targetTime;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
_getTimezoneOffset(timezone, date) {
|
|
366
|
-
try {
|
|
367
|
-
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
|
368
|
-
const targetDate = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
|
|
369
|
-
return (targetDate.getTime() - utcDate.getTime()) / 60000;
|
|
370
|
-
} catch (e) {
|
|
371
|
-
return 0; // Default to UTC
|
|
430
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
431
|
+
timeZone: timezone,
|
|
432
|
+
year: 'numeric',
|
|
433
|
+
month: '2-digit',
|
|
434
|
+
day: '2-digit',
|
|
435
|
+
hour: '2-digit',
|
|
436
|
+
minute: '2-digit',
|
|
437
|
+
second: '2-digit',
|
|
438
|
+
hour12: false
|
|
439
|
+
});
|
|
440
|
+
const parts = Object.fromEntries(
|
|
441
|
+
formatter.formatToParts(now).map(p => [p.type, p.value])
|
|
442
|
+
);
|
|
443
|
+
return new Date(`${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}`);
|
|
444
|
+
} catch {
|
|
445
|
+
return now;
|
|
372
446
|
}
|
|
373
447
|
}
|
|
374
448
|
|
|
@@ -401,9 +475,12 @@ class Model {
|
|
|
401
475
|
if (await this.constructor.fireEvent('creating', this) === false) return false;
|
|
402
476
|
if (await this.constructor.fireEvent('saving', this) === false) return false;
|
|
403
477
|
|
|
478
|
+
const createdAtCol = this.constructor.createdAt || 'created_at';
|
|
479
|
+
const updatedAtCol = this.constructor.updatedAt || 'updated_at';
|
|
480
|
+
|
|
404
481
|
if (this.constructor.timestamps) {
|
|
405
482
|
const now = this._getCurrentTimestamp();
|
|
406
|
-
this.setAttribute(
|
|
483
|
+
this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
|
|
407
484
|
}
|
|
408
485
|
|
|
409
486
|
// Generate UUID if needed
|
|
@@ -425,16 +502,19 @@ class Model {
|
|
|
425
502
|
await this.constructor.fireEvent('saved', this);
|
|
426
503
|
this.syncOriginal();
|
|
427
504
|
} else if (this.isDirty()) {
|
|
505
|
+
const createdAtCol = this.constructor.createdAt || 'created_at';
|
|
506
|
+
const updatedAtCol = this.constructor.updatedAt || 'updated_at';
|
|
507
|
+
|
|
428
508
|
// Updating existing record
|
|
429
509
|
if (await this.constructor.fireEvent('updating', this) === false) return false;
|
|
430
510
|
if (await this.constructor.fireEvent('saving', this) === false) return false;
|
|
431
511
|
|
|
432
512
|
if (this.constructor.timestamps) {
|
|
433
|
-
this.setAttribute(
|
|
513
|
+
this.setAttribute(updatedAtCol, this._getCurrentTimestamp());
|
|
434
514
|
}
|
|
435
515
|
|
|
436
516
|
const updateData = this.getDirty();
|
|
437
|
-
delete updateData
|
|
517
|
+
delete updateData[createdAtCol]; // Never update created_at
|
|
438
518
|
|
|
439
519
|
if (Object.keys(updateData).length > 0) {
|
|
440
520
|
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).update(updateData);
|
|
@@ -470,8 +550,10 @@ class Model {
|
|
|
470
550
|
|
|
471
551
|
await this.constructor.fireEvent('deleting', this);
|
|
472
552
|
|
|
553
|
+
const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
|
|
554
|
+
|
|
473
555
|
if (this.constructor.softDeletes) {
|
|
474
|
-
this.setAttribute(
|
|
556
|
+
this.setAttribute(deletedAtCol, new Date());
|
|
475
557
|
await this.save();
|
|
476
558
|
} else {
|
|
477
559
|
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).delete();
|
|
@@ -483,13 +565,14 @@ class Model {
|
|
|
483
565
|
}
|
|
484
566
|
|
|
485
567
|
async restore() {
|
|
486
|
-
|
|
568
|
+
const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
|
|
569
|
+
if (!this.constructor.softDeletes || !this.getAttribute(deletedAtCol)) {
|
|
487
570
|
return false;
|
|
488
571
|
}
|
|
489
572
|
|
|
490
573
|
await this.constructor.fireEvent('restoring', this);
|
|
491
574
|
|
|
492
|
-
this.setAttribute(
|
|
575
|
+
this.setAttribute(deletedAtCol, null);
|
|
493
576
|
await this.save();
|
|
494
577
|
|
|
495
578
|
await this.constructor.fireEvent('restored', this);
|
|
@@ -497,7 +580,8 @@ class Model {
|
|
|
497
580
|
}
|
|
498
581
|
|
|
499
582
|
trashed() {
|
|
500
|
-
|
|
583
|
+
const deletedAtCol = this.constructor.deletedAt || 'deleted_at';
|
|
584
|
+
return this.constructor.softDeletes && this.getAttribute(deletedAtCol) !== null;
|
|
501
585
|
}
|
|
502
586
|
|
|
503
587
|
only(keys) {
|
|
@@ -578,15 +662,18 @@ class Model {
|
|
|
578
662
|
// relations - convert classes to strings to avoid circular dependencies
|
|
579
663
|
hasOne(related, fk, lk) {
|
|
580
664
|
const relatedName = this._resolveRelatedName(related);
|
|
581
|
-
return new HasOne(this, relatedName, fk ||
|
|
665
|
+
return new HasOne(this, relatedName, fk || this._selfFk(), lk || this.constructor.primaryKey);
|
|
582
666
|
}
|
|
583
667
|
hasMany(related, fk, lk) {
|
|
584
668
|
const relatedName = this._resolveRelatedName(related);
|
|
585
|
-
return new HasMany(this, relatedName, fk ||
|
|
669
|
+
return new HasMany(this, relatedName, fk || this._selfFk(), lk || this.constructor.primaryKey);
|
|
586
670
|
}
|
|
587
671
|
belongsTo(related, fk, ok) {
|
|
588
672
|
const relatedName = this._resolveRelatedName(related);
|
|
589
|
-
|
|
673
|
+
const defaultFk = typeof relatedName === 'string'
|
|
674
|
+
? relatedName.replace(/([A-Z])/g, (m, l, i) => i === 0 ? l.toLowerCase() : '_' + l.toLowerCase()) + '_id'
|
|
675
|
+
: undefined;
|
|
676
|
+
return new BelongsTo(this, relatedName, fk || defaultFk, ok || this.constructor.primaryKey);
|
|
590
677
|
}
|
|
591
678
|
belongsToMany(related, pivot, fp, rp, pk, rk) {
|
|
592
679
|
const relatedName = this._resolveRelatedName(related);
|
|
@@ -612,6 +699,10 @@ class Model {
|
|
|
612
699
|
return new HasManyThrough(this, relatedName, throughName, fk, sk, lk, slk);
|
|
613
700
|
}
|
|
614
701
|
morphTo(type, id) { return new MorphTo(this, type, id); }
|
|
702
|
+
morphOne(related, type, id) {
|
|
703
|
+
const relatedName = typeof related === 'function' && related.name ? related.name : related;
|
|
704
|
+
return new MorphOne(this, relatedName, type, id, this.constructor.name);
|
|
705
|
+
}
|
|
615
706
|
morphMany(related, type, id) {
|
|
616
707
|
const relatedName = typeof related === 'function' && related.name ? related.name : related;
|
|
617
708
|
return new MorphMany(this, relatedName, type, id, this.constructor.name);
|
package/orm/QueryBuilder.d.ts
CHANGED
|
@@ -56,27 +56,44 @@ export default class QueryBuilder {
|
|
|
56
56
|
whereDate(column: string, operator: string, value: string): this;
|
|
57
57
|
whereMonth(column: string, month: number): this;
|
|
58
58
|
whereYear(column: string, year: number): this;
|
|
59
|
-
|
|
59
|
+
whereNotBetween(column: string, range: [any, any]): this;
|
|
60
|
+
orWhereNull(column: string): this;
|
|
61
|
+
orWhereNotNull(column: string): this;
|
|
62
|
+
orWhereIn(column: string, values: any[]): this;
|
|
63
|
+
orWhereNotIn(column: string, values: any[]): this;
|
|
64
|
+
orWhereRaw(sql: string, bindings?: any[]): this;
|
|
65
|
+
whereDay(column: string, operatorOrValue: any, value?: any): this;
|
|
66
|
+
whereTime(column: string, operatorOrValue: any, value?: any): this;
|
|
60
67
|
when<T>(condition: T, callback: (query: this, condition: T) => void, otherwise?: (query: this) => void): this;
|
|
68
|
+
unless<T>(condition: T, callback: (query: this) => void, otherwise?: (query: this, condition: T) => void): this;
|
|
61
69
|
|
|
62
70
|
// Joins
|
|
63
71
|
join(table: string, first: string, operator: string, second: string): this;
|
|
64
72
|
leftJoin(table: string, first: string, operator: string, second: string): this;
|
|
65
73
|
rightJoin(table: string, first: string, operator: string, second: string): this;
|
|
74
|
+
innerJoin(table: string, first: string, operator: string, second: string): this;
|
|
75
|
+
crossJoin(table: string): this;
|
|
66
76
|
|
|
67
77
|
// Ordering and limits
|
|
68
78
|
orderBy(column: string, direction?: 'asc' | 'desc'): this;
|
|
79
|
+
orderByRaw(sql: string): this;
|
|
69
80
|
latest(column?: string): this;
|
|
70
81
|
oldest(column?: string): this;
|
|
82
|
+
inRandomOrder(): this;
|
|
71
83
|
limit(count: number): this;
|
|
72
84
|
offset(count: number): this;
|
|
73
85
|
take(count: number): this;
|
|
74
86
|
skip(count: number): this;
|
|
87
|
+
from(table: string): this;
|
|
88
|
+
forPage(page: number, perPage?: number): this;
|
|
75
89
|
|
|
76
90
|
// Grouping
|
|
77
91
|
groupBy(...columns: string[]): this;
|
|
78
92
|
having(column: string, operator: string, value: any): this;
|
|
79
93
|
having(rawSql: string): this;
|
|
94
|
+
havingRaw(sql: string, bindings?: any[]): this;
|
|
95
|
+
whereExists(callback: (query: QueryBuilder) => void): this;
|
|
96
|
+
whereNotExists(callback: (query: QueryBuilder) => void): this;
|
|
80
97
|
|
|
81
98
|
// Locking
|
|
82
99
|
lockForUpdate(): this;
|
|
@@ -85,7 +102,8 @@ export default class QueryBuilder {
|
|
|
85
102
|
noWait(): this;
|
|
86
103
|
|
|
87
104
|
// Selection
|
|
88
|
-
select(...columns:
|
|
105
|
+
select(...columns: any[]): this;
|
|
106
|
+
addSelect(...columns: any[]): this;
|
|
89
107
|
distinct(): this;
|
|
90
108
|
|
|
91
109
|
// Raw queries
|
|
@@ -105,14 +123,18 @@ export default class QueryBuilder {
|
|
|
105
123
|
withConstraints(relations: { [key: string]: (query: QueryBuilder) => void }): this;
|
|
106
124
|
withCount(...relations: string[]): this;
|
|
107
125
|
whereHas(relation: string, callback?: (query: QueryBuilder) => void): this;
|
|
126
|
+
doesntHave(relation: string): this;
|
|
127
|
+
whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): this;
|
|
108
128
|
|
|
109
129
|
// Execution methods
|
|
110
130
|
get(): Promise<Collection<Model>>;
|
|
111
131
|
first(): Promise<Model | null>;
|
|
132
|
+
firstOrFail(): Promise<Model>;
|
|
112
133
|
find(id: any): Promise<Model | null>;
|
|
113
134
|
findOrFail(id: any): Promise<Model>;
|
|
114
135
|
pluck(column: string): Promise<any[]>;
|
|
115
136
|
exists(): Promise<boolean>;
|
|
137
|
+
doesntExist(): Promise<boolean>;
|
|
116
138
|
|
|
117
139
|
// Pagination
|
|
118
140
|
paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
|