arkormx 0.1.7 → 0.1.8
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/dist/index.cjs +4681 -21
- package/dist/index.mjs +4592 -21
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,32 +1,1802 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let fs = require("fs");
|
|
30
|
+
let path = require("path");
|
|
31
|
+
path = __toESM(path);
|
|
32
|
+
let module$1 = require("module");
|
|
33
|
+
let node_fs = require("node:fs");
|
|
34
|
+
let node_path = require("node:path");
|
|
35
|
+
let node_child_process = require("node:child_process");
|
|
36
|
+
let _h3ravel_support = require("@h3ravel/support");
|
|
37
|
+
let url = require("url");
|
|
38
|
+
let _h3ravel_shared = require("@h3ravel/shared");
|
|
39
|
+
let _h3ravel_musket = require("@h3ravel/musket");
|
|
40
|
+
let node_url = require("node:url");
|
|
41
|
+
let _h3ravel_collect_js = require("@h3ravel/collect.js");
|
|
42
|
+
|
|
43
|
+
//#region src/casts.ts
|
|
44
|
+
const builtinCasts = {
|
|
45
|
+
string: {
|
|
46
|
+
get: (value) => value == null ? value : String(value),
|
|
47
|
+
set: (value) => value == null ? value : String(value)
|
|
48
|
+
},
|
|
49
|
+
number: {
|
|
50
|
+
get: (value) => value == null ? value : Number(value),
|
|
51
|
+
set: (value) => value == null ? value : Number(value)
|
|
52
|
+
},
|
|
53
|
+
boolean: {
|
|
54
|
+
get: (value) => value == null ? value : Boolean(value),
|
|
55
|
+
set: (value) => value == null ? value : Boolean(value)
|
|
56
|
+
},
|
|
57
|
+
date: {
|
|
58
|
+
get: (value) => {
|
|
59
|
+
if (value == null || value instanceof Date) return value;
|
|
60
|
+
return new Date(String(value));
|
|
61
|
+
},
|
|
62
|
+
set: (value) => {
|
|
63
|
+
if (value == null || value instanceof Date) return value;
|
|
64
|
+
return new Date(String(value));
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
json: {
|
|
68
|
+
get: (value) => {
|
|
69
|
+
if (value == null || typeof value !== "string") return value;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(value);
|
|
72
|
+
} catch {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
set: (value) => {
|
|
77
|
+
if (value == null || typeof value === "string") return value;
|
|
78
|
+
return JSON.stringify(value);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
array: {
|
|
82
|
+
get: (value) => {
|
|
83
|
+
if (Array.isArray(value)) return value;
|
|
84
|
+
if (typeof value === "string") try {
|
|
85
|
+
const parsed = JSON.parse(value);
|
|
86
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
87
|
+
} catch {
|
|
88
|
+
return [value];
|
|
89
|
+
}
|
|
90
|
+
if (value == null) return value;
|
|
91
|
+
return [value];
|
|
92
|
+
},
|
|
93
|
+
set: (value) => {
|
|
94
|
+
if (value == null) return value;
|
|
95
|
+
return Array.isArray(value) ? value : [value];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function resolveCast(definition) {
|
|
100
|
+
if (typeof definition === "string") return builtinCasts[definition];
|
|
101
|
+
return definition;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/Exceptions/ArkormException.ts
|
|
106
|
+
/**
|
|
107
|
+
* The ArkormException class is a custom error type for handling
|
|
108
|
+
* exceptions specific to the Arkormˣ.
|
|
109
|
+
*
|
|
110
|
+
* @author Legacy (3m1n3nc3)
|
|
111
|
+
* @since 0.1.0
|
|
112
|
+
*/
|
|
113
|
+
var ArkormException = class extends Error {
|
|
114
|
+
constructor(message) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "ArkormException";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/database/Migration.ts
|
|
122
|
+
/**
|
|
123
|
+
* The Migration class serves as a base for defining database migrations, requiring
|
|
124
|
+
* the implementation of `up` and `down` methods to specify the changes to be
|
|
125
|
+
* applied or reverted in the database schema.
|
|
126
|
+
*
|
|
127
|
+
* @author Legacy (3m1n3nc3)
|
|
128
|
+
* @since 0.1.0
|
|
129
|
+
*/
|
|
130
|
+
var Migration = class {};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/database/TableBuilder.ts
|
|
134
|
+
/**
|
|
135
|
+
* The TableBuilder class provides a fluent interface for defining
|
|
136
|
+
* the structure of a database table in a migration, including columns to add or drop.
|
|
137
|
+
*
|
|
138
|
+
* @author Legacy (3m1n3nc3)
|
|
139
|
+
* @since 0.1.0
|
|
140
|
+
*/
|
|
141
|
+
var TableBuilder = class {
|
|
142
|
+
columns = [];
|
|
143
|
+
dropColumnNames = [];
|
|
144
|
+
indexes = [];
|
|
145
|
+
latestColumnName;
|
|
146
|
+
/**
|
|
147
|
+
* Defines a primary key column in the table.
|
|
148
|
+
*
|
|
149
|
+
* @param columnNameOrOptions
|
|
150
|
+
* @param options
|
|
151
|
+
* @returns
|
|
152
|
+
*/
|
|
153
|
+
primary(columnNameOrOptions, options) {
|
|
154
|
+
const config = typeof columnNameOrOptions === "string" ? {
|
|
155
|
+
columnName: columnNameOrOptions,
|
|
156
|
+
...options ?? {}
|
|
157
|
+
} : columnNameOrOptions ?? {};
|
|
158
|
+
const column = this.resolveColumn(config.columnName);
|
|
159
|
+
column.primary = true;
|
|
160
|
+
if (typeof config.autoIncrement === "boolean") column.autoIncrement = config.autoIncrement;
|
|
161
|
+
if (Object.prototype.hasOwnProperty.call(config, "default")) column.default = config.default;
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Defines an auto-incrementing primary key column.
|
|
166
|
+
*
|
|
167
|
+
* @param name The name of the primary key column.
|
|
168
|
+
* @default 'id'
|
|
169
|
+
* @returns The current TableBuilder instance for chaining.
|
|
170
|
+
*/
|
|
171
|
+
id(name = "id", type = "id") {
|
|
172
|
+
return this.column(name, type, { primary: true });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Defines a UUID column in the table.
|
|
176
|
+
*
|
|
177
|
+
* @param name The name of the UUID column.
|
|
178
|
+
* @param options Additional options for the UUID column.
|
|
179
|
+
* @returns The current TableBuilder instance for chaining.
|
|
180
|
+
*/
|
|
181
|
+
uuid(name, options = {}) {
|
|
182
|
+
return this.column(name, "uuid", options);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Defines a string column in the table.
|
|
186
|
+
*
|
|
187
|
+
* @param name The name of the string column.
|
|
188
|
+
* @param options Additional options for the string column.
|
|
189
|
+
* @returns The current TableBuilder instance for chaining.
|
|
190
|
+
*/
|
|
191
|
+
string(name, options = {}) {
|
|
192
|
+
return this.column(name, "string", options);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Defines a text column in the table.
|
|
196
|
+
*
|
|
197
|
+
* @param name The name of the text column.
|
|
198
|
+
* @param options Additional options for the text column.
|
|
199
|
+
* @returns The current TableBuilder instance for chaining.
|
|
200
|
+
*/
|
|
201
|
+
text(name, options = {}) {
|
|
202
|
+
return this.column(name, "text", options);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Defines an integer column in the table.
|
|
206
|
+
*
|
|
207
|
+
* @param name The name of the integer column.
|
|
208
|
+
* @param options Additional options for the integer column.
|
|
209
|
+
* @returns
|
|
210
|
+
*/
|
|
211
|
+
integer(name, options = {}) {
|
|
212
|
+
return this.column(name, "integer", options);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Defines a big integer column in the table.
|
|
216
|
+
*
|
|
217
|
+
* @param name The name of the big integer column.
|
|
218
|
+
* @param options Additional options for the big integer column.
|
|
219
|
+
* @returns
|
|
220
|
+
*/
|
|
221
|
+
bigInteger(name, options = {}) {
|
|
222
|
+
return this.column(name, "bigInteger", options);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Defines a float column in the table.
|
|
226
|
+
*
|
|
227
|
+
* @param name The name of the float column.
|
|
228
|
+
* @param options Additional options for the float column.
|
|
229
|
+
* @returns
|
|
230
|
+
*/
|
|
231
|
+
float(name, options = {}) {
|
|
232
|
+
return this.column(name, "float", options);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Defines a boolean column in the table.
|
|
236
|
+
*
|
|
237
|
+
* @param name The name of the boolean column.
|
|
238
|
+
* @param options Additional options for the boolean column.
|
|
239
|
+
* @returns
|
|
240
|
+
*/
|
|
241
|
+
boolean(name, options = {}) {
|
|
242
|
+
return this.column(name, "boolean", options);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Defines a JSON column in the table.
|
|
246
|
+
*
|
|
247
|
+
* @param name The name of the JSON column.
|
|
248
|
+
* @param options Additional options for the JSON column.
|
|
249
|
+
* @returns
|
|
250
|
+
*/
|
|
251
|
+
json(name, options = {}) {
|
|
252
|
+
return this.column(name, "json", options);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Defines a date column in the table.
|
|
256
|
+
*
|
|
257
|
+
* @param name The name of the date column.
|
|
258
|
+
* @param options Additional options for the date column.
|
|
259
|
+
* @returns
|
|
260
|
+
*/
|
|
261
|
+
date(name, options = {}) {
|
|
262
|
+
return this.column(name, "date", options);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Defines colonns for a polymorphic relationship in the table.
|
|
266
|
+
*
|
|
267
|
+
* @param name The base name for the polymorphic relationship columns.
|
|
268
|
+
* @returns
|
|
269
|
+
*/
|
|
270
|
+
morphs(name, nullable = false) {
|
|
271
|
+
this.string(`${name}Type`, { nullable });
|
|
272
|
+
this.integer(`${name}Id`, { nullable });
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Defines nullable columns for a polymorphic relationship in the table.
|
|
277
|
+
*
|
|
278
|
+
* @param name The base name for the polymorphic relationship columns.
|
|
279
|
+
* @returns
|
|
280
|
+
*/
|
|
281
|
+
nullableMorphs(name) {
|
|
282
|
+
return this.morphs(name, true);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Defines a timestamp column in the table.
|
|
286
|
+
*
|
|
287
|
+
* @param name The name of the timestamp column.
|
|
288
|
+
* @param options Additional options for the timestamp column.
|
|
289
|
+
* @returns
|
|
290
|
+
*/
|
|
291
|
+
timestamp(name, options = {}) {
|
|
292
|
+
return this.column(name, "timestamp", options);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Defines both createdAt and updatedAt timestamp columns in the table.
|
|
296
|
+
*
|
|
297
|
+
* @returns
|
|
298
|
+
*/
|
|
299
|
+
timestamps() {
|
|
300
|
+
this.timestamp("createdAt", { nullable: false });
|
|
301
|
+
this.timestamp("updatedAt", { nullable: false });
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Defines a soft delete timestamp column in the table.
|
|
306
|
+
*
|
|
307
|
+
* @param column The name of the soft delete column.
|
|
308
|
+
* @returns
|
|
309
|
+
*/
|
|
310
|
+
softDeletes(column = "deletedAt") {
|
|
311
|
+
this.timestamp(column, { nullable: true });
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Defines a column to be dropped from the table in an alterTable operation.
|
|
316
|
+
*
|
|
317
|
+
* @param name The name of the column to drop.
|
|
318
|
+
* @returns
|
|
319
|
+
*/
|
|
320
|
+
dropColumn(name) {
|
|
321
|
+
this.dropColumnNames.push(name);
|
|
322
|
+
return this;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Marks a column as nullable.
|
|
326
|
+
*
|
|
327
|
+
* @param columnName Optional explicit column name. When omitted, applies to the latest defined column.
|
|
328
|
+
* @returns The current TableBuilder instance for chaining.
|
|
329
|
+
*/
|
|
330
|
+
nullable(columnName) {
|
|
331
|
+
const column = this.resolveColumn(columnName);
|
|
332
|
+
column.nullable = true;
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Sets the column position to appear after another column when possible.
|
|
337
|
+
*
|
|
338
|
+
* @param referenceColumn The column that the target column should be placed after.
|
|
339
|
+
* @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
|
|
340
|
+
* @returns The current TableBuilder instance for chaining.
|
|
341
|
+
*/
|
|
342
|
+
after(referenceColumn, columnName) {
|
|
343
|
+
const column = this.resolveColumn(columnName);
|
|
344
|
+
column.after = referenceColumn;
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Maps the column to a custom database column name.
|
|
349
|
+
*
|
|
350
|
+
* @param name The custom database column name.
|
|
351
|
+
* @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
|
|
352
|
+
* @returns The current TableBuilder instance for chaining.
|
|
353
|
+
*/
|
|
354
|
+
map(name, columnName) {
|
|
355
|
+
const column = this.resolveColumn(columnName);
|
|
356
|
+
column.map = name;
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Defines an index on one or more columns.
|
|
361
|
+
*
|
|
362
|
+
* @param columns Optional target columns. When omitted, applies to the latest defined column.
|
|
363
|
+
* @param name Optional index name.
|
|
364
|
+
* @returns The current TableBuilder instance for chaining.
|
|
365
|
+
*/
|
|
366
|
+
index(columns, name) {
|
|
367
|
+
const columnList = Array.isArray(columns) ? columns : typeof columns === "string" ? [columns] : [this.resolveColumn().name];
|
|
368
|
+
this.indexes.push({
|
|
369
|
+
columns: [...columnList],
|
|
370
|
+
name
|
|
371
|
+
});
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Returns a deep copy of the defined columns for the table.
|
|
376
|
+
*
|
|
377
|
+
* @returns
|
|
378
|
+
*/
|
|
379
|
+
getColumns() {
|
|
380
|
+
return this.columns.map((column) => ({ ...column }));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Returns a copy of the defined column names to be dropped from the table.
|
|
384
|
+
*
|
|
385
|
+
* @returns
|
|
386
|
+
*/
|
|
387
|
+
getDropColumns() {
|
|
388
|
+
return [...this.dropColumnNames];
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Returns a deep copy of the defined indexes for the table.
|
|
392
|
+
*
|
|
393
|
+
* @returns
|
|
394
|
+
*/
|
|
395
|
+
getIndexes() {
|
|
396
|
+
return this.indexes.map((index) => ({
|
|
397
|
+
...index,
|
|
398
|
+
columns: [...index.columns]
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Defines a column in the table with the given name.
|
|
403
|
+
*
|
|
404
|
+
* @param name The name of the column.
|
|
405
|
+
* @param type The type of the column.
|
|
406
|
+
* @param options Additional options for the column.
|
|
407
|
+
* @returns
|
|
408
|
+
*/
|
|
409
|
+
column(name, type, options) {
|
|
410
|
+
this.columns.push({
|
|
411
|
+
name,
|
|
412
|
+
type,
|
|
413
|
+
map: options.map,
|
|
414
|
+
nullable: options.nullable,
|
|
415
|
+
unique: options.unique,
|
|
416
|
+
primary: options.primary,
|
|
417
|
+
autoIncrement: options.autoIncrement,
|
|
418
|
+
after: options.after,
|
|
419
|
+
default: options.default
|
|
420
|
+
});
|
|
421
|
+
this.latestColumnName = name;
|
|
422
|
+
return this;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve a target column by name or fallback to the latest defined column.
|
|
426
|
+
*
|
|
427
|
+
* @param columnName
|
|
428
|
+
* @returns
|
|
429
|
+
*/
|
|
430
|
+
resolveColumn(columnName) {
|
|
431
|
+
const targetName = columnName ?? this.latestColumnName;
|
|
432
|
+
if (!targetName) throw new Error("No column available for this operation.");
|
|
433
|
+
const column = this.columns.find((item) => item.name === targetName);
|
|
434
|
+
if (!column) throw new Error(`Column [${targetName}] was not found in the table definition.`);
|
|
435
|
+
return column;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/database/SchemaBuilder.ts
|
|
441
|
+
/**
|
|
442
|
+
* The SchemaBuilder class provides methods for defining the operations to be
|
|
443
|
+
* performed in a migration, such as creating, altering, or dropping tables.
|
|
444
|
+
*
|
|
445
|
+
* @author Legacy (3m1n3nc3)
|
|
446
|
+
* @since 0.1.0
|
|
447
|
+
*/
|
|
448
|
+
var SchemaBuilder = class {
|
|
449
|
+
operations = [];
|
|
450
|
+
/**
|
|
451
|
+
* Defines a new table to be created in the migration.
|
|
452
|
+
*
|
|
453
|
+
* @param table The name of the table to create.
|
|
454
|
+
* @param callback A callback function to define the table's columns and structure.
|
|
455
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
456
|
+
*/
|
|
457
|
+
createTable(table, callback) {
|
|
458
|
+
const builder = new TableBuilder();
|
|
459
|
+
callback(builder);
|
|
460
|
+
this.operations.push({
|
|
461
|
+
type: "createTable",
|
|
462
|
+
table,
|
|
463
|
+
columns: builder.getColumns(),
|
|
464
|
+
indexes: builder.getIndexes()
|
|
465
|
+
});
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Defines alterations to an existing table in the migration.
|
|
470
|
+
*
|
|
471
|
+
* @param table The name of the table to alter.
|
|
472
|
+
* @param callback A callback function to define the alterations to the table's columns and structure.
|
|
473
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
474
|
+
*/
|
|
475
|
+
alterTable(table, callback) {
|
|
476
|
+
const builder = new TableBuilder();
|
|
477
|
+
callback(builder);
|
|
478
|
+
this.operations.push({
|
|
479
|
+
type: "alterTable",
|
|
480
|
+
table,
|
|
481
|
+
addColumns: builder.getColumns(),
|
|
482
|
+
dropColumns: builder.getDropColumns(),
|
|
483
|
+
addIndexes: builder.getIndexes()
|
|
484
|
+
});
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Defines a table to be dropped in the migration.
|
|
489
|
+
*
|
|
490
|
+
* @param table The name of the table to drop.
|
|
491
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
492
|
+
*/
|
|
493
|
+
dropTable(table) {
|
|
494
|
+
this.operations.push({
|
|
495
|
+
type: "dropTable",
|
|
496
|
+
table
|
|
497
|
+
});
|
|
498
|
+
return this;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Returns a deep copy of the defined schema operations for the migration/
|
|
502
|
+
*
|
|
503
|
+
* @returns An array of schema operations for the migration.
|
|
504
|
+
*/
|
|
505
|
+
getOperations() {
|
|
506
|
+
return this.operations.map((operation) => {
|
|
507
|
+
if (operation.type === "createTable") return {
|
|
508
|
+
...operation,
|
|
509
|
+
columns: operation.columns.map((column) => ({ ...column })),
|
|
510
|
+
indexes: operation.indexes.map((index) => ({
|
|
511
|
+
...index,
|
|
512
|
+
columns: [...index.columns]
|
|
513
|
+
}))
|
|
514
|
+
};
|
|
515
|
+
if (operation.type === "alterTable") return {
|
|
516
|
+
...operation,
|
|
517
|
+
addColumns: operation.addColumns.map((column) => ({ ...column })),
|
|
518
|
+
dropColumns: [...operation.dropColumns],
|
|
519
|
+
addIndexes: operation.addIndexes.map((index) => ({
|
|
520
|
+
...index,
|
|
521
|
+
columns: [...index.columns]
|
|
522
|
+
}))
|
|
523
|
+
};
|
|
524
|
+
return { ...operation };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/helpers/migrations.ts
|
|
531
|
+
const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
|
|
532
|
+
/**
|
|
533
|
+
* Convert a table name to a PascalCase model name, with basic singularization.
|
|
534
|
+
*
|
|
535
|
+
* @param tableName The name of the table to convert.
|
|
536
|
+
* @returns The corresponding PascalCase model name.
|
|
537
|
+
*/
|
|
538
|
+
const toModelName = (tableName) => {
|
|
539
|
+
const normalized = tableName.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
540
|
+
const parts = (normalized.endsWith("s") && normalized.length > 1 ? normalized.slice(0, -1) : normalized).split(/\s+/g).filter(Boolean);
|
|
541
|
+
if (parts.length === 0) return "GeneratedModel";
|
|
542
|
+
return parts.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Escape special characters in a string for use in a regular expression.
|
|
546
|
+
*
|
|
547
|
+
* @param value
|
|
548
|
+
* @returns
|
|
549
|
+
*/
|
|
550
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
551
|
+
/**
|
|
552
|
+
* Convert a SchemaColumn definition to a Prisma field type string, including modifiers.
|
|
553
|
+
*
|
|
554
|
+
* @param column
|
|
555
|
+
* @returns
|
|
556
|
+
*/
|
|
557
|
+
const resolvePrismaType = (column) => {
|
|
558
|
+
if (column.type === "id") return "Int";
|
|
559
|
+
if (column.type === "uuid") return "String";
|
|
560
|
+
if (column.type === "string" || column.type === "text") return "String";
|
|
561
|
+
if (column.type === "integer") return "Int";
|
|
562
|
+
if (column.type === "bigInteger") return "BigInt";
|
|
563
|
+
if (column.type === "float") return "Float";
|
|
564
|
+
if (column.type === "boolean") return "Boolean";
|
|
565
|
+
if (column.type === "json") return "Json";
|
|
566
|
+
return "DateTime";
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Format a default value for inclusion in a Prisma schema field definition, based on its type.
|
|
570
|
+
*
|
|
571
|
+
* @param value
|
|
572
|
+
* @returns
|
|
573
|
+
*/
|
|
574
|
+
const formatDefaultValue = (value) => {
|
|
575
|
+
if (value == null) return void 0;
|
|
576
|
+
if (typeof value === "string") return `@default("${value.replace(/"/g, "\\\"")}")`;
|
|
577
|
+
if (typeof value === "number" || typeof value === "bigint") return `@default(${value})`;
|
|
578
|
+
if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
|
|
582
|
+
*
|
|
583
|
+
* @param column
|
|
584
|
+
* @returns
|
|
585
|
+
*/
|
|
586
|
+
const buildFieldLine = (column) => {
|
|
587
|
+
if (column.type === "id") {
|
|
588
|
+
const primary = column.primary === false ? "" : " @id";
|
|
589
|
+
const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
|
|
590
|
+
const configuredDefault = formatDefaultValue(column.default);
|
|
591
|
+
const shouldAutoIncrement = column.autoIncrement ?? column.primary !== false;
|
|
592
|
+
const defaultSuffix = configuredDefault ? ` ${configuredDefault}` : shouldAutoIncrement && primary ? " @default(autoincrement())" : "";
|
|
593
|
+
return ` ${column.name} Int${primary}${defaultSuffix}${mapped}`;
|
|
594
|
+
}
|
|
595
|
+
const scalar = resolvePrismaType(column);
|
|
596
|
+
const nullable = column.nullable ? "?" : "";
|
|
597
|
+
const unique = column.unique ? " @unique" : "";
|
|
598
|
+
const primary = column.primary ? " @id" : "";
|
|
599
|
+
const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
|
|
600
|
+
const defaultValue = formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
|
|
601
|
+
const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
|
|
602
|
+
return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${mapped}`;
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Build a Prisma model-level @@index definition line.
|
|
606
|
+
*
|
|
607
|
+
* @param index
|
|
608
|
+
* @returns
|
|
609
|
+
*/
|
|
610
|
+
const buildIndexLine = (index) => {
|
|
611
|
+
return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* Build a Prisma model block string based on a SchemaTableCreateOperation, including
|
|
615
|
+
* all fields and any necessary mapping.
|
|
616
|
+
*
|
|
617
|
+
* @param operation The schema table create operation to convert.
|
|
618
|
+
* @returns The corresponding Prisma model block string.
|
|
619
|
+
*/
|
|
620
|
+
const buildModelBlock = (operation) => {
|
|
621
|
+
const modelName = toModelName(operation.table);
|
|
622
|
+
const mapped = operation.table !== modelName.toLowerCase();
|
|
623
|
+
const fields = operation.columns.map(buildFieldLine);
|
|
624
|
+
const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${(0, _h3ravel_support.str)(operation.table).snake()}")`] : []];
|
|
625
|
+
return `model ${modelName} {\n${(metadata.length > 0 ? [
|
|
626
|
+
...fields,
|
|
627
|
+
"",
|
|
628
|
+
...metadata
|
|
629
|
+
] : fields).join("\n")}\n}`;
|
|
630
|
+
};
|
|
631
|
+
/**
|
|
632
|
+
* Find the Prisma model block in a schema string that corresponds to a given
|
|
633
|
+
* table name, using both explicit mapping and naming conventions.
|
|
634
|
+
*
|
|
635
|
+
* @param schema
|
|
636
|
+
* @param table
|
|
637
|
+
* @returns
|
|
638
|
+
*/
|
|
639
|
+
const findModelBlock = (schema, table) => {
|
|
640
|
+
const candidates = [...schema.matchAll(PRISMA_MODEL_REGEX)];
|
|
641
|
+
const explicitMapRegex = new RegExp(`@@map\\("${escapeRegex(table)}"\\)`);
|
|
642
|
+
for (const match of candidates) {
|
|
643
|
+
const block = match[0];
|
|
644
|
+
const modelName = match[1];
|
|
645
|
+
const start = match.index ?? 0;
|
|
646
|
+
const end = start + block.length;
|
|
647
|
+
if (explicitMapRegex.test(block)) return {
|
|
648
|
+
modelName,
|
|
649
|
+
block,
|
|
650
|
+
start,
|
|
651
|
+
end
|
|
652
|
+
};
|
|
653
|
+
if (modelName.toLowerCase() === table.toLowerCase()) return {
|
|
654
|
+
modelName,
|
|
655
|
+
block,
|
|
656
|
+
start,
|
|
657
|
+
end
|
|
658
|
+
};
|
|
659
|
+
if (modelName.toLowerCase() === toModelName(table).toLowerCase()) return {
|
|
660
|
+
modelName,
|
|
661
|
+
block,
|
|
662
|
+
start,
|
|
663
|
+
end
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
};
|
|
668
|
+
/**
|
|
669
|
+
* Apply a create table operation to a Prisma schema string, adding a new model
|
|
670
|
+
* block for the specified table and fields.
|
|
671
|
+
*
|
|
672
|
+
* @param schema The current Prisma schema string.
|
|
673
|
+
* @param operation The schema table create operation to apply.
|
|
674
|
+
* @returns The updated Prisma schema string with the new model block.
|
|
675
|
+
*/
|
|
676
|
+
const applyCreateTableOperation = (schema, operation) => {
|
|
677
|
+
if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
|
|
678
|
+
const block = buildModelBlock(operation);
|
|
679
|
+
return `${schema.trimEnd()}\n\n${block}\n`;
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* Apply an alter table operation to a Prisma schema string, modifying the model
|
|
683
|
+
* block for the specified table by adding and removing fields as needed.
|
|
684
|
+
*
|
|
685
|
+
* @param schema The current Prisma schema string.
|
|
686
|
+
* @param operation The schema table alter operation to apply.
|
|
687
|
+
* @returns The updated Prisma schema string with the modified model block.
|
|
688
|
+
*/
|
|
689
|
+
const applyAlterTableOperation = (schema, operation) => {
|
|
690
|
+
const model = findModelBlock(schema, operation.table);
|
|
691
|
+
if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
|
|
692
|
+
let block = model.block;
|
|
693
|
+
const bodyLines = block.split("\n");
|
|
694
|
+
operation.dropColumns.forEach((column) => {
|
|
695
|
+
const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
|
|
696
|
+
for (let index = 0; index < bodyLines.length; index += 1) if (columnRegex.test(bodyLines[index])) {
|
|
697
|
+
bodyLines.splice(index, 1);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
operation.addColumns.forEach((column) => {
|
|
702
|
+
const fieldLine = buildFieldLine(column);
|
|
703
|
+
const columnRegex = new RegExp(`^\\s*${escapeRegex(column.name)}\\s+`);
|
|
704
|
+
if (bodyLines.some((line) => columnRegex.test(line))) return;
|
|
705
|
+
const defaultInsertIndex = Math.max(1, bodyLines.length - 1);
|
|
706
|
+
const afterInsertIndex = typeof column.after === "string" && column.after.length > 0 ? bodyLines.findIndex((line) => new RegExp(`^\\s*${escapeRegex(column.after)}\\s+`).test(line)) : -1;
|
|
707
|
+
const insertIndex = afterInsertIndex > 0 ? Math.min(afterInsertIndex + 1, defaultInsertIndex) : defaultInsertIndex;
|
|
708
|
+
bodyLines.splice(insertIndex, 0, fieldLine);
|
|
709
|
+
});
|
|
710
|
+
(operation.addIndexes ?? []).forEach((index) => {
|
|
711
|
+
const indexLine = buildIndexLine(index);
|
|
712
|
+
if (bodyLines.some((line) => line.trim() === indexLine.trim())) return;
|
|
713
|
+
const insertIndex = Math.max(1, bodyLines.length - 1);
|
|
714
|
+
bodyLines.splice(insertIndex, 0, indexLine);
|
|
715
|
+
});
|
|
716
|
+
block = bodyLines.join("\n");
|
|
717
|
+
return `${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`;
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* Apply a drop table operation to a Prisma schema string, removing the model block
|
|
721
|
+
* for the specified table.
|
|
722
|
+
*/
|
|
723
|
+
const applyDropTableOperation = (schema, operation) => {
|
|
724
|
+
const model = findModelBlock(schema, operation.table);
|
|
725
|
+
if (!model) return schema;
|
|
726
|
+
const before = schema.slice(0, model.start).trimEnd();
|
|
727
|
+
const after = schema.slice(model.end).trimStart();
|
|
728
|
+
return `${before}${before && after ? "\n\n" : ""}${after}`;
|
|
729
|
+
};
|
|
730
|
+
/**
|
|
731
|
+
* The SchemaBuilder class provides a fluent interface for defining
|
|
732
|
+
* database schema operations in a migration, such as creating, altering, and
|
|
733
|
+
* dropping tables.
|
|
734
|
+
*
|
|
735
|
+
* @param schema The current Prisma schema string.
|
|
736
|
+
* @param operations The list of schema operations to apply.
|
|
737
|
+
* @returns The updated Prisma schema string after applying all operations.
|
|
738
|
+
*/
|
|
739
|
+
const applyOperationsToPrismaSchema = (schema, operations) => {
|
|
740
|
+
return operations.reduce((current, operation) => {
|
|
741
|
+
if (operation.type === "createTable") return applyCreateTableOperation(current, operation);
|
|
742
|
+
if (operation.type === "alterTable") return applyAlterTableOperation(current, operation);
|
|
743
|
+
return applyDropTableOperation(current, operation);
|
|
744
|
+
}, schema);
|
|
745
|
+
};
|
|
746
|
+
/**
|
|
747
|
+
* Run a Prisma CLI command using npx, capturing and throwing any errors that occur.
|
|
748
|
+
*
|
|
749
|
+
* @param args The arguments to pass to the Prisma CLI command.
|
|
750
|
+
* @param cwd The current working directory to run the command in.
|
|
751
|
+
* @returns void
|
|
752
|
+
*/
|
|
753
|
+
const runPrismaCommand = (args, cwd) => {
|
|
754
|
+
const command = (0, node_child_process.spawnSync)("npx", ["prisma", ...args], {
|
|
755
|
+
cwd,
|
|
756
|
+
encoding: "utf-8"
|
|
757
|
+
});
|
|
758
|
+
if (command.status === 0) return;
|
|
759
|
+
const errorOutput = [command.stdout, command.stderr].filter(Boolean).join("\n").trim();
|
|
760
|
+
throw new ArkormException(errorOutput ? `Prisma command failed: prisma ${args.join(" ")}\n${errorOutput}` : `Prisma command failed: prisma ${args.join(" ")}`);
|
|
761
|
+
};
|
|
762
|
+
/**
|
|
763
|
+
* Generate a new migration file with a given name and options, including
|
|
764
|
+
* writing the file to disk if specified.
|
|
765
|
+
*
|
|
766
|
+
* @param name
|
|
767
|
+
* @returns
|
|
768
|
+
*/
|
|
769
|
+
const resolveMigrationClassName = (name) => {
|
|
770
|
+
const cleaned = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
771
|
+
if (!cleaned) return "GeneratedMigration";
|
|
772
|
+
return `${cleaned.split(/\s+/g).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}Migration`;
|
|
773
|
+
};
|
|
774
|
+
/**
|
|
775
|
+
* Pad a number with leading zeros to ensure it is at least two digits, for
|
|
776
|
+
* use in migration timestamps.
|
|
777
|
+
*
|
|
778
|
+
* @param value
|
|
779
|
+
* @returns
|
|
780
|
+
*/
|
|
781
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
782
|
+
/**
|
|
783
|
+
* Create a timestamp string in the format YYYYMMDDHHMMSS for use in migration
|
|
784
|
+
* file names, based on the current date and time or a provided date.
|
|
785
|
+
*
|
|
786
|
+
* @param date
|
|
787
|
+
* @returns
|
|
788
|
+
*/
|
|
789
|
+
const createMigrationTimestamp = (date = /* @__PURE__ */ new Date()) => {
|
|
790
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* Convert a migration name to a slug suitable for use in a file name, by
|
|
794
|
+
* lowercasing and replacing non-alphanumeric characters with underscores.
|
|
795
|
+
*
|
|
796
|
+
* @param name
|
|
797
|
+
* @returns
|
|
798
|
+
*/
|
|
799
|
+
const toMigrationFileSlug = (name) => {
|
|
800
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration";
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Build the source code for a new migration file based on a given class
|
|
804
|
+
* name, using a template with empty up and down methods.
|
|
805
|
+
*
|
|
806
|
+
* @param className
|
|
807
|
+
* @returns
|
|
808
|
+
*/
|
|
809
|
+
const buildMigrationSource = (className, extension = "ts") => {
|
|
810
|
+
if (extension === "js") return [
|
|
811
|
+
"import { Migration } from 'arkormx'",
|
|
812
|
+
"",
|
|
813
|
+
`export default class ${className} extends Migration {`,
|
|
814
|
+
" /**",
|
|
815
|
+
" * @param {import('arkormx').SchemaBuilder} schema",
|
|
816
|
+
" * @returns {Promise<void>}",
|
|
817
|
+
" */",
|
|
818
|
+
" async up (schema) {",
|
|
819
|
+
" }",
|
|
820
|
+
"",
|
|
821
|
+
" /**",
|
|
822
|
+
" * @param {import('arkormx').SchemaBuilder} schema",
|
|
823
|
+
" * @returns {Promise<void>}",
|
|
824
|
+
" */",
|
|
825
|
+
" async down (schema) {",
|
|
826
|
+
" }",
|
|
827
|
+
"}",
|
|
828
|
+
""
|
|
829
|
+
].join("\n");
|
|
830
|
+
return [
|
|
831
|
+
"import { Migration, SchemaBuilder } from 'arkormx'",
|
|
832
|
+
"",
|
|
833
|
+
`export default class ${className} extends Migration {`,
|
|
834
|
+
" public async up (schema: SchemaBuilder): Promise<void> {",
|
|
835
|
+
" }",
|
|
836
|
+
"",
|
|
837
|
+
" public async down (schema: SchemaBuilder): Promise<void> {",
|
|
838
|
+
" }",
|
|
839
|
+
"}",
|
|
840
|
+
""
|
|
841
|
+
].join("\n");
|
|
842
|
+
};
|
|
843
|
+
/**
|
|
844
|
+
* Generate a new migration file with a given name and options, including
|
|
845
|
+
* writing the file to disk if specified, and return the details of the generated file.
|
|
846
|
+
*
|
|
847
|
+
* @param name
|
|
848
|
+
* @param options
|
|
849
|
+
* @returns
|
|
850
|
+
*/
|
|
851
|
+
const generateMigrationFile = (name, options = {}) => {
|
|
852
|
+
const timestamp = createMigrationTimestamp(/* @__PURE__ */ new Date());
|
|
853
|
+
const fileSlug = toMigrationFileSlug(name);
|
|
854
|
+
const className = resolveMigrationClassName(name);
|
|
855
|
+
const extension = options.extension ?? "ts";
|
|
856
|
+
const directory = options.directory ?? (0, node_path.join)(process.cwd(), "database", "migrations");
|
|
857
|
+
const fileName = `${timestamp}_${fileSlug}.${extension}`;
|
|
858
|
+
const filePath = (0, node_path.join)(directory, fileName);
|
|
859
|
+
const content = buildMigrationSource(className, extension);
|
|
860
|
+
if (options.write ?? true) {
|
|
861
|
+
if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
|
|
862
|
+
if ((0, node_fs.existsSync)(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
|
|
863
|
+
(0, node_fs.writeFileSync)(filePath, content);
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
fileName,
|
|
867
|
+
filePath,
|
|
868
|
+
className,
|
|
869
|
+
content
|
|
870
|
+
};
|
|
871
|
+
};
|
|
872
|
+
/**
|
|
873
|
+
* Get the list of schema operations that would be performed by a given migration class when run in a specified direction (up or down), without actually applying them.
|
|
874
|
+
*
|
|
875
|
+
* @param migration The migration class or instance to analyze.
|
|
876
|
+
* @param direction The direction of the migration to plan for ('up' or 'down').
|
|
877
|
+
* @returns A promise that resolves to an array of schema operations that would be performed.
|
|
878
|
+
*/
|
|
879
|
+
const getMigrationPlan = async (migration, direction = "up") => {
|
|
880
|
+
const instance = migration instanceof Migration ? migration : new migration();
|
|
881
|
+
const schema = new SchemaBuilder();
|
|
882
|
+
if (direction === "up") await instance.up(schema);
|
|
883
|
+
else await instance.down(schema);
|
|
884
|
+
return schema.getOperations();
|
|
885
|
+
};
|
|
886
|
+
/**
|
|
887
|
+
* Apply the schema operations defined in a migration to a Prisma schema
|
|
888
|
+
* file, updating the file on disk if specified, and return the updated
|
|
889
|
+
* schema and list of operations applied.
|
|
890
|
+
*
|
|
891
|
+
* @param migration The migration class or instance to apply.
|
|
892
|
+
* @param options Options for applying the migration, including schema path and write flag.
|
|
893
|
+
* @returns A promise that resolves to an object containing the updated schema, schema path, and list of operations applied.
|
|
894
|
+
*/
|
|
895
|
+
const applyMigrationToPrismaSchema = async (migration, options = {}) => {
|
|
896
|
+
const schemaPath = options.schemaPath ?? (0, node_path.join)(process.cwd(), "prisma", "schema.prisma");
|
|
897
|
+
if (!(0, node_fs.existsSync)(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
|
|
898
|
+
const source = (0, node_fs.readFileSync)(schemaPath, "utf-8");
|
|
899
|
+
const operations = await getMigrationPlan(migration, "up");
|
|
900
|
+
const schema = applyOperationsToPrismaSchema(source, operations);
|
|
901
|
+
if (options.write ?? true) (0, node_fs.writeFileSync)(schemaPath, schema);
|
|
902
|
+
return {
|
|
903
|
+
schema,
|
|
904
|
+
schemaPath,
|
|
905
|
+
operations
|
|
906
|
+
};
|
|
907
|
+
};
|
|
908
|
+
/**
|
|
909
|
+
* Run a migration by applying its schema operations to a Prisma schema
|
|
910
|
+
* file, optionally generating Prisma client code and running migrations after
|
|
911
|
+
* applying the schema changes.
|
|
912
|
+
*
|
|
913
|
+
* @param migration The migration class or instance to run.
|
|
914
|
+
* @param options Options for running the migration, including schema path, write flag, and Prisma commands.
|
|
915
|
+
* @returns A promise that resolves to an object containing the schema path and list of operations applied.
|
|
916
|
+
*/
|
|
917
|
+
const runMigrationWithPrisma = async (migration, options = {}) => {
|
|
918
|
+
const cwd = options.cwd ?? process.cwd();
|
|
919
|
+
const applied = await applyMigrationToPrismaSchema(migration, {
|
|
920
|
+
schemaPath: options.schemaPath ?? (0, node_path.join)(cwd, "prisma", "schema.prisma"),
|
|
921
|
+
write: options.write
|
|
922
|
+
});
|
|
923
|
+
const shouldGenerate = options.runGenerate ?? true;
|
|
924
|
+
const shouldMigrate = options.runMigrate ?? true;
|
|
925
|
+
const mode = options.migrateMode ?? "dev";
|
|
926
|
+
if (shouldGenerate) runPrismaCommand(["generate"], cwd);
|
|
927
|
+
if (shouldMigrate) if (mode === "deploy") runPrismaCommand(["migrate", "deploy"], cwd);
|
|
928
|
+
else runPrismaCommand([
|
|
929
|
+
"migrate",
|
|
930
|
+
"dev",
|
|
931
|
+
"--name",
|
|
932
|
+
options.migrationName ?? `arkorm_${createMigrationTimestamp()}`
|
|
933
|
+
], cwd);
|
|
934
|
+
return {
|
|
935
|
+
schemaPath: applied.schemaPath,
|
|
936
|
+
operations: applied.operations
|
|
937
|
+
};
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/helpers/runtime-config.ts
|
|
942
|
+
const resolveDefaultStubsPath = () => {
|
|
943
|
+
let current = path.default.dirname((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
944
|
+
while (true) {
|
|
945
|
+
const packageJsonPath = path.default.join(current, "package.json");
|
|
946
|
+
const stubsPath = path.default.join(current, "stubs");
|
|
947
|
+
if ((0, fs.existsSync)(packageJsonPath) && (0, fs.existsSync)(stubsPath)) return stubsPath;
|
|
948
|
+
const parent = path.default.dirname(current);
|
|
949
|
+
if (parent === current) break;
|
|
950
|
+
current = parent;
|
|
951
|
+
}
|
|
952
|
+
return path.default.join(process.cwd(), "stubs");
|
|
953
|
+
};
|
|
954
|
+
const baseConfig = {
|
|
955
|
+
paths: {
|
|
956
|
+
stubs: resolveDefaultStubsPath(),
|
|
957
|
+
seeders: path.default.join(process.cwd(), "database", "seeders"),
|
|
958
|
+
models: path.default.join(process.cwd(), "src", "models"),
|
|
959
|
+
migrations: path.default.join(process.cwd(), "database", "migrations"),
|
|
960
|
+
factories: path.default.join(process.cwd(), "database", "factories"),
|
|
961
|
+
buildOutput: path.default.join(process.cwd(), "dist")
|
|
962
|
+
},
|
|
963
|
+
outputExt: "ts"
|
|
964
|
+
};
|
|
965
|
+
const userConfig = {
|
|
966
|
+
...baseConfig,
|
|
967
|
+
paths: { ...baseConfig.paths ?? {} }
|
|
968
|
+
};
|
|
969
|
+
let runtimeConfigLoaded = false;
|
|
970
|
+
let runtimeConfigLoadingPromise;
|
|
971
|
+
let runtimeClientResolver;
|
|
972
|
+
let runtimePaginationURLDriverFactory;
|
|
973
|
+
const mergePathConfig = (paths) => {
|
|
974
|
+
const defaults = baseConfig.paths ?? {};
|
|
975
|
+
const current = userConfig.paths ?? {};
|
|
976
|
+
const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
|
|
977
|
+
if (typeof value === "string" && value.trim().length > 0) all[key] = value;
|
|
978
|
+
return all;
|
|
979
|
+
}, {});
|
|
980
|
+
return {
|
|
981
|
+
...defaults,
|
|
982
|
+
...current,
|
|
983
|
+
...incoming
|
|
984
|
+
};
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* Define the ArkORM runtime configuration. This function can be used to provide.
|
|
988
|
+
*
|
|
989
|
+
* @param config The ArkORM configuration object.
|
|
990
|
+
* @returns The same configuration object.
|
|
991
|
+
*/
|
|
992
|
+
const defineConfig = (config) => {
|
|
993
|
+
return config;
|
|
994
|
+
};
|
|
995
|
+
/**
|
|
996
|
+
* Get the user-provided ArkORM configuration.
|
|
997
|
+
*
|
|
998
|
+
* @returns The user-provided ArkORM configuration object.
|
|
999
|
+
*/
|
|
1000
|
+
const getUserConfig = (key) => {
|
|
1001
|
+
if (key) return userConfig[key];
|
|
1002
|
+
return userConfig;
|
|
1003
|
+
};
|
|
1004
|
+
/**
|
|
1005
|
+
* Configure the ArkORM runtime with the provided Prisma client resolver and
|
|
1006
|
+
* delegate mapping resolver.
|
|
1007
|
+
*
|
|
1008
|
+
* @param prisma
|
|
1009
|
+
* @param mapping
|
|
1010
|
+
*/
|
|
1011
|
+
const configureArkormRuntime = (prisma, options = {}) => {
|
|
1012
|
+
const nextConfig = {
|
|
1013
|
+
...userConfig,
|
|
1014
|
+
prisma,
|
|
1015
|
+
paths: mergePathConfig(options.paths)
|
|
1016
|
+
};
|
|
1017
|
+
if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
|
|
1018
|
+
if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
|
|
1019
|
+
Object.assign(userConfig, { ...nextConfig });
|
|
1020
|
+
runtimeClientResolver = prisma;
|
|
1021
|
+
runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
|
|
1022
|
+
};
|
|
1023
|
+
/**
|
|
1024
|
+
* Reset the ArkORM runtime configuration.
|
|
1025
|
+
* This is primarily intended for testing purposes.
|
|
1026
|
+
*/
|
|
1027
|
+
const resetArkormRuntimeForTests = () => {
|
|
1028
|
+
Object.assign(userConfig, {
|
|
1029
|
+
...baseConfig,
|
|
1030
|
+
paths: { ...baseConfig.paths ?? {} }
|
|
1031
|
+
});
|
|
1032
|
+
runtimeConfigLoaded = false;
|
|
1033
|
+
runtimeConfigLoadingPromise = void 0;
|
|
1034
|
+
runtimeClientResolver = void 0;
|
|
1035
|
+
runtimePaginationURLDriverFactory = void 0;
|
|
1036
|
+
};
|
|
1037
|
+
/**
|
|
1038
|
+
* Resolve a Prisma client instance from the provided resolver, which can be either
|
|
1039
|
+
* a direct client instance or a function that returns a client instance.
|
|
1040
|
+
*
|
|
1041
|
+
* @param resolver
|
|
1042
|
+
* @returns
|
|
1043
|
+
*/
|
|
1044
|
+
const resolveClient = (resolver) => {
|
|
1045
|
+
if (!resolver) return void 0;
|
|
1046
|
+
const client = typeof resolver === "function" ? resolver() : resolver;
|
|
1047
|
+
if (!client || typeof client !== "object") return void 0;
|
|
1048
|
+
return client;
|
|
1049
|
+
};
|
|
1050
|
+
/**
|
|
1051
|
+
* Resolve and apply the ArkORM configuration from an imported module.
|
|
1052
|
+
* This function checks for a default export and falls back to the module itself, then validates
|
|
1053
|
+
* the configuration object and applies it to the runtime if valid.
|
|
1054
|
+
*
|
|
1055
|
+
* @param imported
|
|
1056
|
+
* @returns
|
|
1057
|
+
*/
|
|
1058
|
+
const resolveAndApplyConfig = (imported) => {
|
|
1059
|
+
const config = imported?.default ?? imported;
|
|
1060
|
+
if (!config || typeof config !== "object" || !config.prisma) return;
|
|
1061
|
+
configureArkormRuntime(config.prisma, {
|
|
1062
|
+
pagination: config.pagination,
|
|
1063
|
+
paths: config.paths,
|
|
1064
|
+
outputExt: config.outputExt
|
|
1065
|
+
});
|
|
1066
|
+
runtimeConfigLoaded = true;
|
|
1067
|
+
};
|
|
1068
|
+
/**
|
|
1069
|
+
* Dynamically import a configuration file.
|
|
1070
|
+
* A cache-busting query parameter is appended to ensure the latest version is loaded.
|
|
1071
|
+
*
|
|
1072
|
+
* @param configPath
|
|
1073
|
+
* @returns A promise that resolves to the imported configuration module.
|
|
1074
|
+
*/
|
|
1075
|
+
const importConfigFile = (configPath) => {
|
|
1076
|
+
return import(`${(0, url.pathToFileURL)(configPath).href}?arkorm_runtime=${Date.now()}`);
|
|
1077
|
+
};
|
|
1078
|
+
const loadRuntimeConfigSync = () => {
|
|
1079
|
+
const require = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
1080
|
+
const syncConfigPaths = [path.default.join(process.cwd(), "arkormx.config.cjs")];
|
|
1081
|
+
for (const configPath of syncConfigPaths) {
|
|
1082
|
+
if (!(0, fs.existsSync)(configPath)) continue;
|
|
1083
|
+
try {
|
|
1084
|
+
resolveAndApplyConfig(require(configPath));
|
|
1085
|
+
return true;
|
|
1086
|
+
} catch {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return false;
|
|
1091
|
+
};
|
|
1092
|
+
/**
|
|
1093
|
+
* Load the ArkORM configuration by searching for configuration files in the
|
|
1094
|
+
* current working directory.
|
|
1095
|
+
* @returns
|
|
1096
|
+
*/
|
|
1097
|
+
const loadArkormConfig = async () => {
|
|
1098
|
+
if (runtimeConfigLoaded) return;
|
|
1099
|
+
if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
|
|
1100
|
+
if (loadRuntimeConfigSync()) return;
|
|
1101
|
+
runtimeConfigLoadingPromise = (async () => {
|
|
1102
|
+
const configPaths = [path.default.join(process.cwd(), "arkormx.config.js"), path.default.join(process.cwd(), "arkormx.config.ts")];
|
|
1103
|
+
for (const configPath of configPaths) {
|
|
1104
|
+
if (!(0, fs.existsSync)(configPath)) continue;
|
|
1105
|
+
try {
|
|
1106
|
+
resolveAndApplyConfig(await importConfigFile(configPath));
|
|
1107
|
+
return;
|
|
1108
|
+
} catch {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
runtimeConfigLoaded = true;
|
|
1113
|
+
})();
|
|
1114
|
+
await runtimeConfigLoadingPromise;
|
|
1115
|
+
};
|
|
1116
|
+
/**
|
|
1117
|
+
* Ensure that the ArkORM configuration is loaded.
|
|
1118
|
+
* This function can be called to trigger the loading process if it hasn't already been initiated.
|
|
1119
|
+
* If the configuration is already loaded, it will return immediately.
|
|
1120
|
+
*
|
|
1121
|
+
* @returns
|
|
1122
|
+
*/
|
|
1123
|
+
const ensureArkormConfigLoading = () => {
|
|
1124
|
+
if (runtimeConfigLoaded) return;
|
|
1125
|
+
if (!runtimeConfigLoadingPromise) loadArkormConfig();
|
|
1126
|
+
};
|
|
1127
|
+
const getDefaultStubsPath = () => {
|
|
1128
|
+
return resolveDefaultStubsPath();
|
|
1129
|
+
};
|
|
1130
|
+
/**
|
|
1131
|
+
* Get the runtime Prisma client.
|
|
1132
|
+
* This function will trigger the loading of the ArkORM configuration if
|
|
1133
|
+
* it hasn't already been loaded.
|
|
1134
|
+
*
|
|
1135
|
+
* @returns
|
|
1136
|
+
*/
|
|
1137
|
+
const getRuntimePrismaClient = () => {
|
|
1138
|
+
if (!runtimeConfigLoaded) loadRuntimeConfigSync();
|
|
1139
|
+
return resolveClient(runtimeClientResolver);
|
|
1140
|
+
};
|
|
1141
|
+
/**
|
|
1142
|
+
* Get the configured pagination URL driver factory from runtime config.
|
|
1143
|
+
*
|
|
1144
|
+
* @returns
|
|
1145
|
+
*/
|
|
1146
|
+
const getRuntimePaginationURLDriverFactory = () => {
|
|
1147
|
+
if (!runtimeConfigLoaded) loadRuntimeConfigSync();
|
|
1148
|
+
return runtimePaginationURLDriverFactory;
|
|
1149
|
+
};
|
|
1150
|
+
/**
|
|
1151
|
+
* Check if a given value is a Prisma delegate-like object
|
|
1152
|
+
* by verifying the presence of common delegate methods.
|
|
1153
|
+
*
|
|
1154
|
+
* @param value The value to check.
|
|
1155
|
+
* @returns True if the value is a Prisma delegate-like object, false otherwise.
|
|
1156
|
+
*/
|
|
1157
|
+
const isDelegateLike = (value) => {
|
|
1158
|
+
if (!value || typeof value !== "object") return false;
|
|
1159
|
+
const candidate = value;
|
|
1160
|
+
return [
|
|
1161
|
+
"findMany",
|
|
1162
|
+
"findFirst",
|
|
1163
|
+
"create",
|
|
1164
|
+
"update",
|
|
1165
|
+
"delete",
|
|
1166
|
+
"count"
|
|
1167
|
+
].every((method) => typeof candidate[method] === "function");
|
|
1168
|
+
};
|
|
1169
|
+
loadArkormConfig();
|
|
1170
|
+
|
|
1171
|
+
//#endregion
|
|
1172
|
+
//#region src/cli/CliApp.ts
|
|
1173
|
+
/**
|
|
1174
|
+
* Main application class for the Arkormˣ CLI.
|
|
1175
|
+
*
|
|
1176
|
+
* @author Legacy (3m1n3nc3)
|
|
1177
|
+
* @since 0.1.0
|
|
1178
|
+
*/
|
|
1179
|
+
var CliApp = class {
|
|
1180
|
+
command;
|
|
1181
|
+
config = {};
|
|
1182
|
+
constructor() {
|
|
1183
|
+
this.config = getUserConfig();
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Get the current configuration object or a specific configuration value.
|
|
1187
|
+
*
|
|
1188
|
+
* @param key Optional specific configuration key to retrieve
|
|
1189
|
+
* @returns The entire configuration object or the value of the specified key
|
|
1190
|
+
*/
|
|
1191
|
+
getConfig = getUserConfig;
|
|
1192
|
+
/**
|
|
1193
|
+
* Utility to ensure directory exists
|
|
1194
|
+
*
|
|
1195
|
+
* @param filePath
|
|
1196
|
+
*/
|
|
1197
|
+
ensureDirectory(filePath) {
|
|
1198
|
+
const dir = (0, path.dirname)(filePath);
|
|
1199
|
+
if (!(0, fs.existsSync)(dir)) (0, fs.mkdirSync)(dir, { recursive: true });
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Convert absolute paths under current working directory into relative display paths.
|
|
1203
|
+
*
|
|
1204
|
+
* @param filePath
|
|
1205
|
+
* @returns
|
|
1206
|
+
*/
|
|
1207
|
+
formatPathForLog(filePath) {
|
|
1208
|
+
const relPath = (0, path.relative)(process.cwd(), filePath);
|
|
1209
|
+
if (!relPath) return ".";
|
|
1210
|
+
if (relPath.startsWith("..")) return filePath;
|
|
1211
|
+
return relPath;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Utility to format a value for logging, converting absolute paths under current
|
|
1215
|
+
* working directory into relative display paths.
|
|
1216
|
+
*
|
|
1217
|
+
* @param name
|
|
1218
|
+
* @param value
|
|
1219
|
+
* @returns
|
|
1220
|
+
*/
|
|
1221
|
+
splitLogger(name, value) {
|
|
1222
|
+
value = value.includes(process.cwd()) ? this.formatPathForLog(value) : value;
|
|
1223
|
+
return _h3ravel_shared.Logger.twoColumnDetail(name + " ", " " + value, false).join("");
|
|
1224
|
+
}
|
|
1225
|
+
hasTypeScriptInstalled() {
|
|
1226
|
+
try {
|
|
1227
|
+
(0, module$1.createRequire)(require("url").pathToFileURL(__filename).href).resolve("typescript", { paths: [process.cwd()] });
|
|
1228
|
+
return true;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
resolveOutputExt() {
|
|
1234
|
+
const preferred = this.getConfig("outputExt") === "js" ? "js" : "ts";
|
|
1235
|
+
if (preferred === "ts" && !this.hasTypeScriptInstalled()) return "js";
|
|
1236
|
+
return preferred;
|
|
1237
|
+
}
|
|
1238
|
+
stripKnownSourceExtension(value) {
|
|
1239
|
+
return value.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "");
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Resolve a directory path to runtime output when the source path is unavailable.
|
|
1243
|
+
*
|
|
1244
|
+
* @param directoryPath
|
|
1245
|
+
* @returns
|
|
1246
|
+
*/
|
|
1247
|
+
resolveRuntimeDirectoryPath(directoryPath) {
|
|
1248
|
+
if ((0, fs.existsSync)(directoryPath)) return directoryPath;
|
|
1249
|
+
const { buildOutput } = this.getConfig("paths") || {};
|
|
1250
|
+
if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directoryPath;
|
|
1251
|
+
const relativeSource = (0, path.relative)(process.cwd(), directoryPath);
|
|
1252
|
+
if (!relativeSource || relativeSource.startsWith("..")) return directoryPath;
|
|
1253
|
+
const mappedDirectory = (0, path.join)(buildOutput, relativeSource);
|
|
1254
|
+
return (0, fs.existsSync)(mappedDirectory) ? mappedDirectory : directoryPath;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Resolve a script file path for runtime execution.
|
|
1258
|
+
* If a .ts file is provided, tries equivalent .js/.cjs/.mjs files first.
|
|
1259
|
+
* Also attempts mapped paths inside paths.buildOutput preserving structure.
|
|
1260
|
+
*
|
|
1261
|
+
* @param filePath
|
|
1262
|
+
* @returns
|
|
1263
|
+
*/
|
|
1264
|
+
resolveRuntimeScriptPath(filePath) {
|
|
1265
|
+
const extension = (0, path.extname)(filePath).toLowerCase();
|
|
1266
|
+
const isTsFile = extension === ".ts" || extension === ".mts" || extension === ".cts";
|
|
1267
|
+
const candidates = [];
|
|
1268
|
+
if (isTsFile) {
|
|
1269
|
+
const base = filePath.slice(0, -extension.length);
|
|
1270
|
+
candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
|
|
1271
|
+
}
|
|
1272
|
+
const { buildOutput } = this.getConfig("paths") ?? {};
|
|
1273
|
+
if (typeof buildOutput === "string" && buildOutput.trim().length > 0) {
|
|
1274
|
+
const relativeSource = (0, path.relative)(process.cwd(), filePath);
|
|
1275
|
+
if (relativeSource && !relativeSource.startsWith("..")) {
|
|
1276
|
+
const mappedFile = (0, path.join)(buildOutput, relativeSource);
|
|
1277
|
+
const mappedExtension = (0, path.extname)(mappedFile).toLowerCase();
|
|
1278
|
+
if (mappedExtension === ".ts" || mappedExtension === ".mts" || mappedExtension === ".cts") {
|
|
1279
|
+
const mappedBase = mappedFile.slice(0, -mappedExtension.length);
|
|
1280
|
+
candidates.push(`${mappedBase}.js`, `${mappedBase}.cjs`, `${mappedBase}.mjs`);
|
|
1281
|
+
} else candidates.push(mappedFile);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const runtimeMatch = candidates.find((path$2) => (0, fs.existsSync)(path$2));
|
|
1285
|
+
if (runtimeMatch) return runtimeMatch;
|
|
1286
|
+
return filePath;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Utility to generate file from stub
|
|
1290
|
+
*
|
|
1291
|
+
* @param stubPath
|
|
1292
|
+
* @param outputPath
|
|
1293
|
+
* @param replacements
|
|
1294
|
+
*/
|
|
1295
|
+
generateFile(stubPath, outputPath, replacements, options) {
|
|
1296
|
+
if ((0, fs.existsSync)(outputPath) && !options?.force) {
|
|
1297
|
+
this.command.error(`Error: ${this.formatPathForLog(outputPath)} already exists.`);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
} else if ((0, fs.existsSync)(outputPath) && options?.force) (0, fs.rmSync)(outputPath);
|
|
1300
|
+
let content = (0, fs.readFileSync)(stubPath, "utf-8");
|
|
1301
|
+
for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
1302
|
+
this.ensureDirectory(outputPath);
|
|
1303
|
+
(0, fs.writeFileSync)(outputPath, content);
|
|
1304
|
+
return outputPath;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolve a configuration path with a fallback default
|
|
1308
|
+
*
|
|
1309
|
+
* @param key The configuration key to resolve
|
|
1310
|
+
* @param fallback The fallback value if the configuration key is not set
|
|
1311
|
+
* @returns The resolved configuration path
|
|
1312
|
+
*/
|
|
1313
|
+
resolveConfigPath(key, fallback) {
|
|
1314
|
+
const { [key]: configured } = this.getConfig("paths") ?? {};
|
|
1315
|
+
if (typeof configured === "string" && configured.trim().length > 0) return configured;
|
|
1316
|
+
return fallback;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Resolve the path to a stub file based on configuration
|
|
1320
|
+
*
|
|
1321
|
+
* @param stubName
|
|
1322
|
+
* @returns
|
|
1323
|
+
*/
|
|
1324
|
+
resolveStubPath(stubName) {
|
|
1325
|
+
return (0, path.join)(this.resolveConfigPath("stubs", getDefaultStubsPath()), stubName);
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Generate a factory file for a given model name.
|
|
1329
|
+
*
|
|
1330
|
+
* @param name
|
|
1331
|
+
* @param options
|
|
1332
|
+
* @returns
|
|
1333
|
+
*/
|
|
1334
|
+
makeFactory(name, options = {}) {
|
|
1335
|
+
const baseName = (0, _h3ravel_support.str)(name.replace(/Factory$/, "")).pascal();
|
|
1336
|
+
const factoryName = `${baseName}Factory`;
|
|
1337
|
+
const modelName = options.modelName ? (0, _h3ravel_support.str)(options.modelName).pascal() : baseName;
|
|
1338
|
+
const outputExt = this.resolveOutputExt();
|
|
1339
|
+
const outputPath = (0, path.join)(this.resolveConfigPath("factories", (0, path.join)(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
|
|
1340
|
+
const modelPath = (0, path.join)(this.resolveConfigPath("models", (0, path.join)(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
|
|
1341
|
+
const relativeImport = options.modelImportPath ?? `./${this.stripKnownSourceExtension((0, path.relative)((0, path.dirname)(outputPath), modelPath).replace(/\\/g, "/"))}${outputExt === "js" ? ".js" : ""}`;
|
|
1342
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "factory.js.stub" : "factory.stub");
|
|
1343
|
+
return {
|
|
1344
|
+
name: factoryName,
|
|
1345
|
+
path: this.generateFile(stubPath, outputPath, {
|
|
1346
|
+
FactoryName: factoryName,
|
|
1347
|
+
ModelName: modelName.toString(),
|
|
1348
|
+
ModelImportPath: relativeImport.startsWith(".") ? relativeImport : `./${relativeImport}`
|
|
1349
|
+
}, options)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Generate a seeder file for a given name.
|
|
1354
|
+
*
|
|
1355
|
+
* @param name
|
|
1356
|
+
* @param options
|
|
1357
|
+
* @returns
|
|
1358
|
+
*/
|
|
1359
|
+
makeSeeder(name, options = {}) {
|
|
1360
|
+
const seederName = `${(0, _h3ravel_support.str)(name.replace(/Seeder$/, "")).pascal()}Seeder`;
|
|
1361
|
+
const outputExt = this.resolveOutputExt();
|
|
1362
|
+
const outputPath = (0, path.join)(this.resolveConfigPath("seeders", (0, path.join)(process.cwd(), "database", "seeders")), `${seederName}.${outputExt}`);
|
|
1363
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "seeder.js.stub" : "seeder.stub");
|
|
1364
|
+
return {
|
|
1365
|
+
name: seederName,
|
|
1366
|
+
path: this.generateFile(stubPath, outputPath, { SeederName: seederName }, options)
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Generate a migration file for a given name.
|
|
1371
|
+
*
|
|
1372
|
+
* @param name The name of the migration.
|
|
1373
|
+
* @returns An object containing the name and path of the generated migration file.
|
|
1374
|
+
*/
|
|
1375
|
+
makeMigration(name) {
|
|
1376
|
+
const generated = generateMigrationFile(name, {
|
|
1377
|
+
directory: this.resolveConfigPath("migrations", (0, path.join)(process.cwd(), "database", "migrations")),
|
|
1378
|
+
extension: this.resolveOutputExt()
|
|
1379
|
+
});
|
|
1380
|
+
return {
|
|
1381
|
+
name: generated.className,
|
|
1382
|
+
path: generated.filePath
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Generate a model file along with optional factory, seeder, and migration files.
|
|
1387
|
+
*
|
|
1388
|
+
* @param name
|
|
1389
|
+
* @param options
|
|
1390
|
+
* @returns
|
|
1391
|
+
*/
|
|
1392
|
+
makeModel(name, options = {}) {
|
|
1393
|
+
const baseName = (0, _h3ravel_support.str)(name.replace(/Model$/, "")).pascal().toString();
|
|
1394
|
+
const modelName = `${baseName}`;
|
|
1395
|
+
const delegateName = (0, _h3ravel_support.str)(baseName).camel().plural().toString();
|
|
1396
|
+
const outputExt = this.resolveOutputExt();
|
|
1397
|
+
const outputPath = (0, path.join)(this.resolveConfigPath("models", (0, path.join)(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
|
|
1398
|
+
const shouldBuildFactory = options.all || options.factory;
|
|
1399
|
+
const shouldBuildSeeder = options.all || options.seeder;
|
|
1400
|
+
const shouldBuildMigration = options.all || options.migration;
|
|
1401
|
+
const factoryName = `${baseName}Factory`;
|
|
1402
|
+
const factoryPath = (0, path.join)(this.resolveConfigPath("factories", (0, path.join)(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
|
|
1403
|
+
const factoryImportPath = `./${(0, path.relative)((0, path.dirname)(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
|
|
1404
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "model.js.stub" : "model.stub");
|
|
1405
|
+
const modelPath = this.generateFile(stubPath, outputPath, {
|
|
1406
|
+
ModelName: modelName,
|
|
1407
|
+
DelegateName: delegateName,
|
|
1408
|
+
FactoryImport: shouldBuildFactory ? `import { ${factoryName} } from '${factoryImportPath}'\n` : "",
|
|
1409
|
+
FactoryLink: shouldBuildFactory ? outputExt === "js" ? `\n static factoryClass = ${factoryName}` : `\n protected static override factoryClass = ${factoryName}` : ""
|
|
1410
|
+
}, options);
|
|
1411
|
+
const prisma = this.ensurePrismaModelEntry(modelName, delegateName);
|
|
1412
|
+
const created = {
|
|
1413
|
+
model: {
|
|
1414
|
+
name: modelName,
|
|
1415
|
+
path: modelPath
|
|
1416
|
+
},
|
|
1417
|
+
prisma,
|
|
1418
|
+
factory: void 0,
|
|
1419
|
+
seeder: void 0,
|
|
1420
|
+
migration: void 0
|
|
1421
|
+
};
|
|
1422
|
+
if (shouldBuildFactory) created.factory = this.makeFactory(baseName, {
|
|
1423
|
+
force: options.force,
|
|
1424
|
+
modelName,
|
|
1425
|
+
modelImportPath: `./${(0, path.relative)((0, path.dirname)(factoryPath), outputPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`
|
|
1426
|
+
});
|
|
1427
|
+
if (shouldBuildSeeder) created.seeder = this.makeSeeder(baseName, { force: options.force });
|
|
1428
|
+
if (shouldBuildMigration) created.migration = this.makeMigration(`create ${delegateName} table`);
|
|
1429
|
+
return created;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Ensure that the Prisma schema has a model entry for the given model
|
|
1433
|
+
* and delegate names.
|
|
1434
|
+
* If the entry does not exist, it will be created with a default `id` field.
|
|
1435
|
+
*
|
|
1436
|
+
* @param modelName The name of the model to ensure in the Prisma schema.
|
|
1437
|
+
* @param delegateName The name of the delegate (table) to ensure in the Prisma schema.
|
|
1438
|
+
*/
|
|
1439
|
+
ensurePrismaModelEntry(modelName, delegateName) {
|
|
1440
|
+
const schemaPath = (0, path.join)(process.cwd(), "prisma", "schema.prisma");
|
|
1441
|
+
if (!(0, fs.existsSync)(schemaPath)) return {
|
|
1442
|
+
path: schemaPath,
|
|
1443
|
+
updated: false
|
|
1444
|
+
};
|
|
1445
|
+
const source = (0, fs.readFileSync)(schemaPath, "utf-8");
|
|
1446
|
+
const existingByTable = findModelBlock(source, delegateName);
|
|
1447
|
+
const existingByName = new RegExp(`model\\s+${modelName}\\s*\\{`, "m").test(source);
|
|
1448
|
+
if (existingByTable || existingByName) return {
|
|
1449
|
+
path: schemaPath,
|
|
1450
|
+
updated: false
|
|
1451
|
+
};
|
|
1452
|
+
(0, fs.writeFileSync)(schemaPath, applyCreateTableOperation(source, {
|
|
1453
|
+
type: "createTable",
|
|
1454
|
+
table: delegateName,
|
|
1455
|
+
columns: [{
|
|
1456
|
+
name: "id",
|
|
1457
|
+
type: "id",
|
|
1458
|
+
primary: true
|
|
1459
|
+
}]
|
|
1460
|
+
}));
|
|
1461
|
+
return {
|
|
1462
|
+
path: schemaPath,
|
|
1463
|
+
updated: true
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Convert a Prisma scalar type to its corresponding TypeScript type.
|
|
1468
|
+
*
|
|
1469
|
+
* @param value The Prisma scalar type.
|
|
1470
|
+
* @returns The corresponding TypeScript type.
|
|
1471
|
+
*/
|
|
1472
|
+
prismaTypeToTs(value) {
|
|
1473
|
+
if (value === "Int" || value === "Float" || value === "Decimal") return "number";
|
|
1474
|
+
if (value === "BigInt") return "bigint";
|
|
1475
|
+
if (value === "String") return "string";
|
|
1476
|
+
if (value === "Boolean") return "boolean";
|
|
1477
|
+
if (value === "DateTime") return "Date";
|
|
1478
|
+
if (value === "Json") return "Record<string, unknown>";
|
|
1479
|
+
if (value === "Bytes") return "Buffer";
|
|
1480
|
+
return "unknown";
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Parse the Prisma schema to extract model definitions and their fields, focusing
|
|
1484
|
+
* on scalar types.
|
|
1485
|
+
*
|
|
1486
|
+
* @param schema The Prisma schema as a string.
|
|
1487
|
+
* @returns An array of model definitions with their fields.
|
|
1488
|
+
*/
|
|
1489
|
+
parsePrismaModels(schema) {
|
|
1490
|
+
const models = [];
|
|
1491
|
+
const modelRegex = /model\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
1492
|
+
const scalarTypes = new Set([
|
|
1493
|
+
"Int",
|
|
1494
|
+
"Float",
|
|
1495
|
+
"Decimal",
|
|
1496
|
+
"BigInt",
|
|
1497
|
+
"String",
|
|
1498
|
+
"Boolean",
|
|
1499
|
+
"DateTime",
|
|
1500
|
+
"Json",
|
|
1501
|
+
"Bytes"
|
|
1502
|
+
]);
|
|
1503
|
+
for (const match of schema.matchAll(modelRegex)) {
|
|
1504
|
+
const name = match[1];
|
|
1505
|
+
const body = match[2];
|
|
1506
|
+
const table = body.match(/@@map\("([^"]+)"\)/)?.[1] ?? `${name.charAt(0).toLowerCase()}${name.slice(1)}s`;
|
|
1507
|
+
const fields = [];
|
|
1508
|
+
body.split("\n").forEach((rawLine) => {
|
|
1509
|
+
const line = rawLine.trim();
|
|
1510
|
+
if (!line || line.startsWith("@@") || line.startsWith("//")) return;
|
|
1511
|
+
const fieldMatch = line.match(/^(\w+)\s+([A-Za-z]+)(\?)?\b/);
|
|
1512
|
+
if (!fieldMatch) return;
|
|
1513
|
+
const fieldType = fieldMatch[2];
|
|
1514
|
+
if (!scalarTypes.has(fieldType)) return;
|
|
1515
|
+
fields.push({
|
|
1516
|
+
name: fieldMatch[1],
|
|
1517
|
+
type: this.prismaTypeToTs(fieldType),
|
|
1518
|
+
optional: Boolean(fieldMatch[3])
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
models.push({
|
|
1522
|
+
name,
|
|
1523
|
+
table,
|
|
1524
|
+
fields
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
return models;
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Sync model attribute declarations in a model file based on the
|
|
1531
|
+
* provided declarations.
|
|
1532
|
+
* This method takes the source code of a model file and a list of
|
|
1533
|
+
* attribute declarations,
|
|
1534
|
+
*
|
|
1535
|
+
* @param modelSource The source code of the model file.
|
|
1536
|
+
* @param declarations A list of attribute declarations to sync.
|
|
1537
|
+
* @returns An object containing the updated content and a flag indicating if it was updated.
|
|
1538
|
+
*/
|
|
1539
|
+
syncModelDeclarations(modelSource, declarations) {
|
|
1540
|
+
const lines = modelSource.split("\n");
|
|
1541
|
+
const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
|
|
1542
|
+
if (classIndex < 0) return {
|
|
1543
|
+
content: modelSource,
|
|
1544
|
+
updated: false
|
|
1545
|
+
};
|
|
1546
|
+
let classEndIndex = -1;
|
|
1547
|
+
let depth = 0;
|
|
1548
|
+
for (let index = classIndex; index < lines.length; index += 1) {
|
|
1549
|
+
const line = lines[index];
|
|
1550
|
+
depth += (line.match(/\{/g) || []).length;
|
|
1551
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
1552
|
+
if (depth === 0) {
|
|
1553
|
+
classEndIndex = index;
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (classEndIndex < 0) return {
|
|
1558
|
+
content: modelSource,
|
|
1559
|
+
updated: false
|
|
1560
|
+
};
|
|
1561
|
+
const withoutDeclares = lines.slice(classIndex + 1, classEndIndex).filter((line) => !/^\s*declare\s+\w+\??:\s*[^\n]+$/.test(line));
|
|
1562
|
+
const rebuiltClass = [...declarations.map((declaration) => ` ${declaration}`), ...withoutDeclares];
|
|
1563
|
+
const content = [
|
|
1564
|
+
...lines.slice(0, classIndex + 1),
|
|
1565
|
+
...rebuiltClass,
|
|
1566
|
+
...lines.slice(classEndIndex)
|
|
1567
|
+
].join("\n");
|
|
1568
|
+
return {
|
|
1569
|
+
content,
|
|
1570
|
+
updated: content !== modelSource
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Sync model attribute declarations in model files based on the Prisma schema.
|
|
1575
|
+
* This method reads the Prisma schema to extract model definitions and their
|
|
1576
|
+
* scalar fields, then updates the corresponding model files to include `declare`
|
|
1577
|
+
* statements for these fields. It returns an object containing the paths of the
|
|
1578
|
+
* schema and models, the total number of model files processed, and lists of
|
|
1579
|
+
* updated and skipped files.
|
|
1580
|
+
*
|
|
1581
|
+
* @param options Optional parameters to specify custom paths for the Prisma schema and models directory.
|
|
1582
|
+
* @returns An object with details about the synchronization process, including updated and skipped files.
|
|
1583
|
+
*/
|
|
1584
|
+
syncModelsFromPrisma(options = {}) {
|
|
1585
|
+
const schemaPath = options.schemaPath ?? (0, path.join)(process.cwd(), "prisma", "schema.prisma");
|
|
1586
|
+
const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", (0, path.join)(process.cwd(), "src", "models"));
|
|
1587
|
+
if (!(0, fs.existsSync)(schemaPath)) throw new Error(`Prisma schema file not found: ${schemaPath}`);
|
|
1588
|
+
if (!(0, fs.existsSync)(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
|
|
1589
|
+
const schema = (0, fs.readFileSync)(schemaPath, "utf-8");
|
|
1590
|
+
const prismaModels = this.parsePrismaModels(schema);
|
|
1591
|
+
const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts"));
|
|
1592
|
+
const updated = [];
|
|
1593
|
+
const skipped = [];
|
|
1594
|
+
modelFiles.forEach((file) => {
|
|
1595
|
+
const filePath = (0, path.join)(modelsDir, file);
|
|
1596
|
+
const source = (0, fs.readFileSync)(filePath, "utf-8");
|
|
1597
|
+
const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
|
|
1598
|
+
if (!classMatch) {
|
|
1599
|
+
skipped.push(filePath);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const className = classMatch[1];
|
|
1603
|
+
const delegate = classMatch[2];
|
|
1604
|
+
const prismaModel = prismaModels.find((model) => model.table === delegate) ?? prismaModels.find((model) => model.name === className);
|
|
1605
|
+
if (!prismaModel || prismaModel.fields.length === 0) {
|
|
1606
|
+
skipped.push(filePath);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const declarations = prismaModel.fields.map((field) => `declare ${field.name}${field.optional ? "?" : ""}: ${field.type}`);
|
|
1610
|
+
const synced = this.syncModelDeclarations(source, declarations);
|
|
1611
|
+
if (!synced.updated) {
|
|
1612
|
+
skipped.push(filePath);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
(0, fs.writeFileSync)(filePath, synced.content);
|
|
1616
|
+
updated.push(filePath);
|
|
1617
|
+
});
|
|
1618
|
+
return {
|
|
1619
|
+
schemaPath,
|
|
1620
|
+
modelsDir,
|
|
1621
|
+
total: modelFiles.length,
|
|
1622
|
+
updated,
|
|
1623
|
+
skipped
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region src/cli/commands/InitCommand.ts
|
|
1630
|
+
/**
|
|
1631
|
+
* The InitCommand class implements the CLI command for initializing Arkormˣ by creating
|
|
1632
|
+
* a default config file in the current directory.
|
|
1633
|
+
*
|
|
1634
|
+
* @author Legacy (3m1n3nc3)
|
|
1635
|
+
* @since 0.1.0
|
|
1636
|
+
*/
|
|
1637
|
+
var InitCommand = class extends _h3ravel_musket.Command {
|
|
1638
|
+
signature = `init
|
|
13
1639
|
{--force : Force overwrite if config file already exists (existing file will be backed up) }
|
|
14
|
-
`;
|
|
1640
|
+
`;
|
|
1641
|
+
description = "Initialize Arkormˣ by creating a default config file in the current directory";
|
|
1642
|
+
/**
|
|
1643
|
+
* Command handler for the init command.
|
|
1644
|
+
*/
|
|
1645
|
+
async handle() {
|
|
1646
|
+
this.app.command = this;
|
|
1647
|
+
const outputDir = (0, node_path.join)(process.cwd(), "arkormx.config.js");
|
|
1648
|
+
const { stubs } = getUserConfig("paths") ?? {};
|
|
1649
|
+
const stubsDir = typeof stubs === "string" && stubs.trim().length > 0 ? stubs : getDefaultStubsPath();
|
|
1650
|
+
const preferredStubPath = (0, node_path.join)(stubsDir, "arkormx.config.stub");
|
|
1651
|
+
const legacyStubPath = (0, node_path.join)(stubsDir, "arkorm.config.stub");
|
|
1652
|
+
const stubPath = (0, fs.existsSync)(preferredStubPath) ? preferredStubPath : legacyStubPath;
|
|
1653
|
+
if ((0, fs.existsSync)(outputDir) && !this.option("force")) {
|
|
1654
|
+
this.error("Error: Arkormˣ has already been initialized. Use --force to reinitialize.");
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
this.app.ensureDirectory(outputDir);
|
|
1658
|
+
if ((0, fs.existsSync)(outputDir) && this.option("force")) (0, fs.copyFileSync)(outputDir, outputDir.replace(/\.js$/, `.backup.${Date.now()}.js`));
|
|
1659
|
+
if (!(0, fs.existsSync)(stubPath)) {
|
|
1660
|
+
this.error(`Error: Missing config stub at ${preferredStubPath} (or ${legacyStubPath})`);
|
|
1661
|
+
process.exit(1);
|
|
1662
|
+
}
|
|
1663
|
+
(0, fs.writeFileSync)(outputDir, (0, fs.readFileSync)(stubPath, "utf-8"));
|
|
1664
|
+
this.success("Arkormˣ initialized successfully!");
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
//#endregion
|
|
1669
|
+
//#region src/cli/commands/MakeFactoryCommand.ts
|
|
1670
|
+
/**
|
|
1671
|
+
* The MakeFactoryCommand class implements the CLI command for creating new factory classes.
|
|
1672
|
+
*
|
|
1673
|
+
* @author Legacy (3m1n3nc3)
|
|
1674
|
+
* @since 0.1.0
|
|
1675
|
+
*/
|
|
1676
|
+
var MakeFactoryCommand = class extends _h3ravel_musket.Command {
|
|
1677
|
+
signature = `make:factory
|
|
15
1678
|
{name : Name of the factory to create}
|
|
16
1679
|
{--f|force : Overwrite existing file}
|
|
17
|
-
`;
|
|
1680
|
+
`;
|
|
1681
|
+
description = "Create a new model factory class";
|
|
1682
|
+
/**
|
|
1683
|
+
* Command handler for the make:factory command.
|
|
1684
|
+
*
|
|
1685
|
+
* @returns
|
|
1686
|
+
*/
|
|
1687
|
+
async handle() {
|
|
1688
|
+
this.app.command = this;
|
|
1689
|
+
const name = this.argument("name");
|
|
1690
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1691
|
+
const created = this.app.makeFactory(name, { force: this.option("force") });
|
|
1692
|
+
this.success(`Created factory: ${this.app.formatPathForLog(created.path)}`);
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/cli/commands/MakeMigrationCommand.ts
|
|
1698
|
+
/**
|
|
1699
|
+
* The MakeMigrationCommand class implements the CLI command for creating new migration classes.
|
|
1700
|
+
*
|
|
1701
|
+
* @author Legacy (3m1n3nc3)
|
|
1702
|
+
* @since 0.1.0
|
|
1703
|
+
*/
|
|
1704
|
+
var MakeMigrationCommand = class extends _h3ravel_musket.Command {
|
|
1705
|
+
signature = `make:migration
|
|
18
1706
|
{name : Name of the migration to create}
|
|
19
|
-
`;
|
|
1707
|
+
`;
|
|
1708
|
+
description = "Create a new migration class file";
|
|
1709
|
+
/**
|
|
1710
|
+
* Command handler for the make:migration command.
|
|
1711
|
+
*
|
|
1712
|
+
* @returns
|
|
1713
|
+
*/
|
|
1714
|
+
async handle() {
|
|
1715
|
+
this.app.command = this;
|
|
1716
|
+
const name = this.argument("name");
|
|
1717
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1718
|
+
const created = this.app.makeMigration(name);
|
|
1719
|
+
this.success(`Created migration: ${this.app.formatPathForLog(created.path)}`);
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region src/cli/commands/MakeModelCommand.ts
|
|
1725
|
+
/**
|
|
1726
|
+
* The MakeModelCommand class implements the CLI command for creating new model
|
|
1727
|
+
* classes along with optional linked resources such as factories, seeders, and migrations.
|
|
1728
|
+
*
|
|
1729
|
+
* @author Legacy (3m1n3nc3)
|
|
1730
|
+
* @since 0.1.0
|
|
1731
|
+
*/
|
|
1732
|
+
var MakeModelCommand = class extends _h3ravel_musket.Command {
|
|
1733
|
+
signature = `make:model
|
|
20
1734
|
{name : Name of the model to create}
|
|
21
1735
|
{--f|force : Overwrite existing files}
|
|
22
1736
|
{--factory : Create and link a factory}
|
|
23
1737
|
{--seeder : Create a seeder}
|
|
24
1738
|
{--migration : Create a migration}
|
|
25
1739
|
{--all : Create and link factory, seeder, and migration}
|
|
26
|
-
`;
|
|
1740
|
+
`;
|
|
1741
|
+
description = "Create a new model and optional linked resources";
|
|
1742
|
+
/**
|
|
1743
|
+
* Command handler for the make:model command.
|
|
1744
|
+
*
|
|
1745
|
+
* @returns
|
|
1746
|
+
*/
|
|
1747
|
+
async handle() {
|
|
1748
|
+
this.app.command = this;
|
|
1749
|
+
const name = this.argument("name");
|
|
1750
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1751
|
+
const created = this.app.makeModel(name, this.options());
|
|
1752
|
+
this.success("Created files:");
|
|
1753
|
+
[
|
|
1754
|
+
["Model", created.model.path],
|
|
1755
|
+
[`Prisma schema ${created.prisma.updated ? "(updated)" : "(already up to date)"}`, created.prisma.path],
|
|
1756
|
+
created.factory ? ["Factory", created.factory.path] : "",
|
|
1757
|
+
created.seeder ? ["Seeder", created.seeder.path] : "",
|
|
1758
|
+
created.migration ? ["Migration", created.migration.path] : ""
|
|
1759
|
+
].filter(Boolean).map(([name, path]) => this.success(this.app.splitLogger(name, path)));
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
//#endregion
|
|
1764
|
+
//#region src/cli/commands/MakeSeederCommand.ts
|
|
1765
|
+
/**
|
|
1766
|
+
* The MakeSeederCommand class implements the CLI command for creating new seeder classes.
|
|
1767
|
+
*
|
|
1768
|
+
* @author Legacy (3m1n3nc3)
|
|
1769
|
+
* @since 0.1.0
|
|
1770
|
+
*/
|
|
1771
|
+
var MakeSeederCommand = class extends _h3ravel_musket.Command {
|
|
1772
|
+
signature = `make:seeder
|
|
27
1773
|
{name : Name of the seeder to create}
|
|
28
1774
|
{--f|force : Overwrite existing file}
|
|
29
|
-
`;
|
|
1775
|
+
`;
|
|
1776
|
+
description = "Create a new seeder class";
|
|
1777
|
+
/**
|
|
1778
|
+
* Command handler for the make:seeder command.
|
|
1779
|
+
*/
|
|
1780
|
+
async handle() {
|
|
1781
|
+
this.app.command = this;
|
|
1782
|
+
const name = this.argument("name");
|
|
1783
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1784
|
+
const created = this.app.makeSeeder(name, this.options());
|
|
1785
|
+
this.success(`Created seeder: ${this.app.formatPathForLog(created.path)}`);
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
//#endregion
|
|
1790
|
+
//#region src/cli/commands/MigrateCommand.ts
|
|
1791
|
+
/**
|
|
1792
|
+
* The MigrateCommand class implements the CLI command for applying migration
|
|
1793
|
+
* classes to the Prisma schema and running the Prisma workflow.
|
|
1794
|
+
*
|
|
1795
|
+
* @author Legacy (3m1n3nc3)
|
|
1796
|
+
* @since 0.1.0
|
|
1797
|
+
*/
|
|
1798
|
+
var MigrateCommand = class extends _h3ravel_musket.Command {
|
|
1799
|
+
signature = `migrate
|
|
30
1800
|
{name? : Migration class or file name}
|
|
31
1801
|
{--all : Run all migrations from the configured migrations directory}
|
|
32
1802
|
{--deploy : Use prisma migrate deploy instead of migrate dev}
|
|
@@ -34,17 +1804,2907 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.
|
|
|
34
1804
|
{--skip-migrate : Skip prisma migrate command}
|
|
35
1805
|
{--schema= : Explicit prisma schema path}
|
|
36
1806
|
{--migration-name= : Name for prisma migrate dev}
|
|
37
|
-
`;
|
|
1807
|
+
`;
|
|
1808
|
+
description = "Apply migration classes to schema.prisma and run Prisma workflow";
|
|
1809
|
+
/**
|
|
1810
|
+
* Command handler for the migrate command.
|
|
1811
|
+
* This method is responsible for orchestrating the migration
|
|
1812
|
+
* process, including loading migration classes, applying them to
|
|
1813
|
+
* the Prisma schema, and running the appropriate Prisma commands
|
|
1814
|
+
* based on the provided options.
|
|
1815
|
+
*
|
|
1816
|
+
* @returns
|
|
1817
|
+
*/
|
|
1818
|
+
async handle() {
|
|
1819
|
+
this.app.command = this;
|
|
1820
|
+
const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? (0, node_path.join)(process.cwd(), "database", "migrations");
|
|
1821
|
+
const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
|
|
1822
|
+
if (!(0, node_fs.existsSync)(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
|
|
1823
|
+
const schemaPath = this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : (0, node_path.join)(process.cwd(), "prisma", "schema.prisma");
|
|
1824
|
+
const classes = this.option("all") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
|
|
1825
|
+
if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
|
|
1826
|
+
for (const [MigrationClassItem] of classes) await applyMigrationToPrismaSchema(MigrationClassItem, {
|
|
1827
|
+
schemaPath,
|
|
1828
|
+
write: true
|
|
1829
|
+
});
|
|
1830
|
+
if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
|
|
1831
|
+
if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
|
|
1832
|
+
else runPrismaCommand([
|
|
1833
|
+
"migrate",
|
|
1834
|
+
"dev",
|
|
1835
|
+
"--name",
|
|
1836
|
+
this.option("migration-name") ? String(this.option("migration-name")) : `arkorm_cli_${Date.now()}`
|
|
1837
|
+
], process.cwd());
|
|
1838
|
+
this.success(`Applied ${classes.length} migration(s).`);
|
|
1839
|
+
classes.forEach(([_, file]) => this.success(this.app.splitLogger("Migrated", file)));
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Load all migration classes from the specified directory.
|
|
1843
|
+
*
|
|
1844
|
+
* @param migrationsDir The directory to load migration classes from.
|
|
1845
|
+
*/
|
|
1846
|
+
async loadAllMigrations(migrationsDir) {
|
|
1847
|
+
const files = (0, node_fs.readdirSync)(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath((0, node_path.join)(migrationsDir, file)));
|
|
1848
|
+
return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Load migration classes from a specific file or by class name.
|
|
1852
|
+
*
|
|
1853
|
+
* @param migrationsDir
|
|
1854
|
+
* @param name
|
|
1855
|
+
* @returns
|
|
1856
|
+
*/
|
|
1857
|
+
async loadNamedMigration(migrationsDir, name) {
|
|
1858
|
+
if (!name) return [[void 0, ""]];
|
|
1859
|
+
const base = name.replace(/Migration$/, "");
|
|
1860
|
+
const target = [
|
|
1861
|
+
`${name}.ts`,
|
|
1862
|
+
`${name}.js`,
|
|
1863
|
+
`${name}.mjs`,
|
|
1864
|
+
`${name}.cjs`,
|
|
1865
|
+
`${base}Migration.ts`,
|
|
1866
|
+
`${base}Migration.js`,
|
|
1867
|
+
`${base}Migration.mjs`,
|
|
1868
|
+
`${base}Migration.cjs`
|
|
1869
|
+
].map((file) => (0, node_path.join)(migrationsDir, file)).find((file) => (0, node_fs.existsSync)(file));
|
|
1870
|
+
if (!target) return [[void 0, name]];
|
|
1871
|
+
const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
|
|
1872
|
+
return (await this.loadMigrationClassesFromFile(runtimeTarget)).map((cls) => [cls, runtimeTarget]);
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Load migration classes from a given file path.
|
|
1876
|
+
*
|
|
1877
|
+
* @param filePath
|
|
1878
|
+
* @returns
|
|
1879
|
+
*/
|
|
1880
|
+
async loadMigrationClassesFromFile(filePath) {
|
|
1881
|
+
const imported = await import(`${(0, node_url.pathToFileURL)((0, node_path.resolve)(filePath)).href}?arkorm_migrate=${Date.now()}`);
|
|
1882
|
+
return Object.values(imported).filter((value) => {
|
|
1883
|
+
if (typeof value !== "function") return false;
|
|
1884
|
+
return value.prototype instanceof Migration;
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
//#endregion
|
|
1890
|
+
//#region src/cli/commands/ModelsSyncCommand.ts
|
|
1891
|
+
var ModelsSyncCommand = class extends _h3ravel_musket.Command {
|
|
1892
|
+
signature = `models:sync
|
|
38
1893
|
{--schema= : Path to prisma schema file}
|
|
39
1894
|
{--models= : Path to models directory}
|
|
40
|
-
`;
|
|
1895
|
+
`;
|
|
1896
|
+
description = "Sync model declare attributes from prisma schema for all model files";
|
|
1897
|
+
async handle() {
|
|
1898
|
+
this.app.command = this;
|
|
1899
|
+
const result = this.app.syncModelsFromPrisma({
|
|
1900
|
+
schemaPath: this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : void 0,
|
|
1901
|
+
modelsDir: this.option("models") ? (0, node_path.resolve)(String(this.option("models"))) : void 0
|
|
1902
|
+
});
|
|
1903
|
+
const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
|
|
1904
|
+
this.success("SUCCESS: Model sync completed with the following results:");
|
|
1905
|
+
[
|
|
1906
|
+
this.app.splitLogger("Schema", result.schemaPath),
|
|
1907
|
+
this.app.splitLogger("Models", result.modelsDir),
|
|
1908
|
+
this.app.splitLogger("Processed", String(result.total)),
|
|
1909
|
+
...updatedLines,
|
|
1910
|
+
this.app.splitLogger("Skipped", String(result.skipped.length))
|
|
1911
|
+
].map((line) => this.success(line));
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/database/Seeder.ts
|
|
1917
|
+
/**
|
|
1918
|
+
* The Seeder class serves as a base for defining database seeders, which are
|
|
1919
|
+
* used to populate the database with initial or test data.
|
|
1920
|
+
*
|
|
1921
|
+
* @author Legacy (3m1n3nc3)
|
|
1922
|
+
* @since 0.1.0
|
|
1923
|
+
*/
|
|
1924
|
+
var Seeder = class Seeder {
|
|
1925
|
+
/**
|
|
1926
|
+
* Runs one or more seeders.
|
|
1927
|
+
*
|
|
1928
|
+
* @param seeders The seeders to be run.
|
|
1929
|
+
*/
|
|
1930
|
+
async call(...seeders) {
|
|
1931
|
+
await Seeder.runSeeders(...seeders);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Converts a SeederInput into a Seeder instance.
|
|
1935
|
+
*
|
|
1936
|
+
* @param input The SeederInput to convert.
|
|
1937
|
+
* @returns A Seeder instance.
|
|
1938
|
+
*/
|
|
1939
|
+
static toSeederInstance(input) {
|
|
1940
|
+
if (input instanceof Seeder) return input;
|
|
1941
|
+
return new input();
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Runs the given seeders in sequence.
|
|
1945
|
+
*
|
|
1946
|
+
* @param seeders The seeders to be run.
|
|
1947
|
+
*/
|
|
1948
|
+
static async runSeeders(...seeders) {
|
|
1949
|
+
const queue = seeders.reduce((all, current) => {
|
|
1950
|
+
if (Array.isArray(current)) return [...all, ...current];
|
|
1951
|
+
all.push(current);
|
|
1952
|
+
return all;
|
|
1953
|
+
}, []);
|
|
1954
|
+
for (const seeder of queue) await this.toSeederInstance(seeder).run();
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
//#endregion
|
|
1959
|
+
//#region src/cli/commands/SeedCommand.ts
|
|
1960
|
+
/**
|
|
1961
|
+
* The SeedCommand class implements the CLI command for running seeder classes.
|
|
1962
|
+
*
|
|
1963
|
+
* @author Legacy (3m1n3nc3)
|
|
1964
|
+
* @since 0.1.0
|
|
1965
|
+
*/
|
|
1966
|
+
var SeedCommand = class extends _h3ravel_musket.Command {
|
|
1967
|
+
signature = `seed
|
|
41
1968
|
{name? : Seeder class or file name}
|
|
42
1969
|
{--all : Run all seeders in the configured seeders directory}
|
|
43
|
-
`;
|
|
1970
|
+
`;
|
|
1971
|
+
description = "Run one or more seeders";
|
|
1972
|
+
/**
|
|
1973
|
+
* Command handler for the seed command.
|
|
1974
|
+
*
|
|
1975
|
+
* @returns
|
|
1976
|
+
*/
|
|
1977
|
+
async handle() {
|
|
1978
|
+
this.app.command = this;
|
|
1979
|
+
const configuredSeedersDir = this.app.getConfig("paths")?.seeders ?? (0, node_path.join)(process.cwd(), "database", "seeders");
|
|
1980
|
+
const seedersDir = this.app.resolveRuntimeDirectoryPath(configuredSeedersDir);
|
|
1981
|
+
if (!(0, node_fs.existsSync)(seedersDir)) return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(configuredSeedersDir)}`);
|
|
1982
|
+
const classes = this.option("all") ? await this.loadAllSeeders(seedersDir) : await this.loadNamedSeeder(seedersDir, this.argument("name") ?? "DatabaseSeeder");
|
|
1983
|
+
if (classes.length === 0) return void this.error("ERROR: No seeder classes found to run.");
|
|
1984
|
+
for (const SeederClassItem of classes) await new SeederClassItem().run();
|
|
1985
|
+
this.success("Database seeding completed");
|
|
1986
|
+
classes.forEach((cls) => this.success(this.app.splitLogger("Seeded", cls.name)));
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Load all seeder classes from the specified directory.
|
|
1990
|
+
*
|
|
1991
|
+
* @param seedersDir
|
|
1992
|
+
* @returns
|
|
1993
|
+
*/
|
|
1994
|
+
async loadAllSeeders(seedersDir) {
|
|
1995
|
+
const files = (0, node_fs.readdirSync)(seedersDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).map((file) => this.app.resolveRuntimeScriptPath((0, node_path.join)(seedersDir, file)));
|
|
1996
|
+
return (await Promise.all(files.map(async (file) => await this.loadSeederClassesFromFile(file)))).flat();
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Load seeder classes from a specific file or by class name.
|
|
2000
|
+
*
|
|
2001
|
+
* @param seedersDir
|
|
2002
|
+
* @param name
|
|
2003
|
+
* @returns
|
|
2004
|
+
*/
|
|
2005
|
+
async loadNamedSeeder(seedersDir, name) {
|
|
2006
|
+
const base = name.replace(/Seeder$/, "");
|
|
2007
|
+
const target = [
|
|
2008
|
+
`${name}.ts`,
|
|
2009
|
+
`${name}.js`,
|
|
2010
|
+
`${name}.mjs`,
|
|
2011
|
+
`${name}.cjs`,
|
|
2012
|
+
`${base}Seeder.ts`,
|
|
2013
|
+
`${base}Seeder.js`,
|
|
2014
|
+
`${base}Seeder.mjs`,
|
|
2015
|
+
`${base}Seeder.cjs`
|
|
2016
|
+
].map((file) => (0, node_path.join)(seedersDir, file)).find((file) => (0, node_fs.existsSync)(file));
|
|
2017
|
+
if (!target) return [];
|
|
2018
|
+
const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
|
|
2019
|
+
return await this.loadSeederClassesFromFile(runtimeTarget);
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Load seeder classes from a given file path.
|
|
2023
|
+
*
|
|
2024
|
+
* @param filePath The path to the file containing seeder classes.
|
|
2025
|
+
* @returns An array of seeder classes.
|
|
2026
|
+
*/
|
|
2027
|
+
async loadSeederClassesFromFile(filePath) {
|
|
2028
|
+
const imported = await import(`${(0, node_url.pathToFileURL)((0, node_path.resolve)(filePath)).href}?arkorm_seed=${Date.now()}`);
|
|
2029
|
+
return Object.values(imported).filter((value) => {
|
|
2030
|
+
if (typeof value !== "function") return false;
|
|
2031
|
+
return value.prototype instanceof Seeder;
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
|
|
2036
|
+
//#endregion
|
|
2037
|
+
//#region src/cli/logo.ts
|
|
2038
|
+
var logo_default = String.raw`
|
|
44
2039
|
__/^^^^^^^^^^^^^^^^\__
|
|
45
2040
|
▄▄▄/ \▄▄
|
|
46
2041
|
▄██▀▀██▄ ▄▄
|
|
47
2042
|
███ ███ ████▄ ██ ▄█▀ ▄███▄ ████▄ ███▄███▄
|
|
48
2043
|
███▀▀███ ██ ▀▀ ████ ██ ██ ██ ▀▀ ██ ██ ██
|
|
49
2044
|
███ ███ ██ ██ ▀█▄ ▀███▀ ██ ██ ██ ██
|
|
50
|
-
`;var J=class extends te.Collection{},Ne=class{amount=1;sequence=0;states=[];count(e){return this.amount=Math.max(1,Math.floor(e)),this}state(e){return this.states.push(e),this}make(e={}){let t=this.buildAttributes(e);return new this.model(t)}makeMany(e=this.amount,t={}){let n=Math.max(1,Math.floor(e));return Array.from({length:n},()=>this.make(t))}async create(e={}){let t=this.make(e);if(typeof t.save!=`function`)throw Error(`Factory model does not support save().`);return await t.save()}async createMany(e=this.amount,t={}){let n=this.makeMany(e,t);return await Promise.all(n.map(async e=>{if(typeof e.save!=`function`)throw Error(`Factory model does not support save().`);return await e.save()}))}buildAttributes(e){let t=this.sequence;this.sequence+=1;let n=this.definition(t);for(let e of this.states)n=e(n,t);return{...n,...e}}},Pe=class extends Ne{model;constructor(e,t){super(),this.resolver=t,this.model=e}definition(e){return this.resolver(e)}};const Fe=(e,t)=>new Pe(e,t);var Y=class extends y{constructor(e=`No query results for the given model.`){super(e),this.name=`ModelNotFoundException`}};function Ie(e){return Object.entries(e).reduce((e,[t,n])=>(q(n)&&(e[t]=n),e),{})}function Le(e){return Ie(e)}function Re(e){return`${e.charAt(0).toLowerCase()}${e.slice(1)}s`}var X=class{constraint=null;constrain(e){if(!this.constraint)return this.constraint=e,this;let t=this.constraint;return this.constraint=n=>{let r=t(n)??n;return e(r)??r},this}where(e){return this.constrain(t=>t.where(e))}whereKey(e,t){return this.constrain(n=>n.whereKey(e,t))}whereIn(e,t){return this.constrain(n=>n.whereIn(e,t))}orderBy(e){return this.constrain(t=>t.orderBy(e))}include(e){return this.constrain(t=>t.include(e))}with(e){return this.constrain(t=>t.with(e))}select(e){return this.constrain(t=>t.select(e))}skip(e){return this.constrain(t=>t.skip(e))}take(e){return this.constrain(t=>t.take(e))}withTrashed(){return this.constrain(e=>e.withTrashed())}onlyTrashed(){return this.constrain(e=>e.onlyTrashed())}withoutTrashed(){return this.constrain(e=>e.withoutTrashed())}scope(e,...t){return this.constrain(n=>n.scope(e,...t))}applyConstraint(e){return this.constraint?this.constraint(e)??e:e}async get(){return this.getResults()}async first(){let e=await this.getResults();return e instanceof J?e.all()[0]??null:e}},ze=class extends X{constructor(e,t,n,r,i,a,o){super(),this.parent=e,this.related=t,this.throughDelegate=n,this.foreignPivotKey=r,this.relatedPivotKey=i,this.parentKey=a,this.relatedKey=o}async getResults(){let e=this.parent.getAttribute(this.parentKey),t=(await this.related.getDelegate(this.throughDelegate).findMany({where:{[this.foreignPivotKey]:e}})).map(e=>e[this.relatedPivotKey]);return this.applyConstraint(this.related.query().where({[this.relatedKey]:{in:t}})).get()}},Be=class extends X{constructor(e,t,n,r){super(),this.parent=e,this.related=t,this.foreignKey=n,this.ownerKey=r}async getResults(){let e=this.parent.getAttribute(this.foreignKey);return this.applyConstraint(this.related.query().where({[this.ownerKey]:e})).first()}},Ve=class extends X{constructor(e,t,n,r){super(),this.parent=e,this.related=t,this.foreignKey=n,this.localKey=r}async getResults(){let e=this.parent.getAttribute(this.localKey);return this.applyConstraint(this.related.query().where({[this.foreignKey]:e})).get()}},He=class extends X{constructor(e,t,n,r,i,a,o){super(),this.parent=e,this.related=t,this.throughDelegate=n,this.firstKey=r,this.secondKey=i,this.localKey=a,this.secondLocalKey=o}async getResults(){let e=this.parent.getAttribute(this.localKey),t=(await this.related.getDelegate(this.throughDelegate).findMany({where:{[this.firstKey]:e}})).map(e=>e[this.secondLocalKey]);return this.applyConstraint(this.related.query().where({[this.secondKey]:{in:t}})).get()}},Ue=class extends X{constructor(e,t,n,r){super(),this.parent=e,this.related=t,this.foreignKey=n,this.localKey=r}async getResults(){let e=this.parent.getAttribute(this.localKey);return this.applyConstraint(this.related.query().where({[this.foreignKey]:e})).first()}},We=class extends X{constructor(e,t,n,r,i,a,o){super(),this.parent=e,this.related=t,this.throughDelegate=n,this.firstKey=r,this.secondKey=i,this.localKey=a,this.secondLocalKey=o}async getResults(){let e=this.parent.getAttribute(this.localKey),t=await this.related.getDelegate(this.throughDelegate).findFirst({where:{[this.firstKey]:e}});return t?this.applyConstraint(this.related.query().where({[this.secondKey]:t[this.secondLocalKey]})).first():null}},Ge=class extends X{constructor(e,t,n,r){super(),this.parent=e,this.related=t,this.morphName=n,this.localKey=r}async getResults(){let e=this.parent.getAttribute(this.localKey),t=this.parent.constructor.name;return this.applyConstraint(this.related.query().where({[`${this.morphName}Id`]:e,[`${this.morphName}Type`]:t})).get()}},Ke=class extends X{constructor(e,t,n,r){super(),this.parent=e,this.related=t,this.morphName=n,this.localKey=r}async getResults(){let e=this.parent.getAttribute(this.localKey),t=this.parent.constructor.name;return this.applyConstraint(this.related.query().where({[`${this.morphName}Id`]:e,[`${this.morphName}Type`]:t})).first()}},qe=class extends X{constructor(e,t,n,r,i,a,o){super(),this.parent=e,this.related=t,this.throughDelegate=n,this.morphName=r,this.relatedPivotKey=i,this.parentKey=a,this.relatedKey=o}async getResults(){let e=this.parent.getAttribute(this.parentKey),t=this.parent.constructor.name,n=(await this.related.getDelegate(this.throughDelegate).findMany({where:{[`${this.morphName}Id`]:e,[`${this.morphName}Type`]:t}})).map(e=>e[this.relatedPivotKey]);return this.applyConstraint(this.related.query().where({[this.relatedKey]:{in:n}})).get()}},Z=class e{static DEFAULT_PAGE_NAME=`page`;path;query;fragment;pageName;constructor(t={}){this.path=t.path??`/`,this.query=t.query??{},this.fragment=t.fragment??``,this.pageName=t.pageName??e.DEFAULT_PAGE_NAME}getPageName(){return this.pageName}url(e){let t=Math.max(1,e),[n,r=``]=this.path.split(`?`),i=new URLSearchParams(r);Object.entries(this.query).forEach(([e,t])=>{if(t==null){i.delete(e);return}i.set(e,String(t))}),i.set(this.pageName,String(t));let a=i.toString(),o=this.fragment.replace(/^#/,``);return!a&&!o?n:o?a?`${n}?${a}#${o}`:`${n}#${o}`:`${n}?${a}`}},Q=class{data;meta;urlDriver;constructor(e,t,n,r,i={}){let a=Math.max(1,Math.ceil(t/n)),o=t===0?null:(r-1)*n+1,s=t===0?null:Math.min(r*n,t);this.data=e;let c=K();this.urlDriver=c?c(i):new Z(i),this.meta={total:t,perPage:n,currentPage:r,lastPage:a,from:o,to:s}}getPageName(){return this.urlDriver.getPageName()}url(e){return this.urlDriver.url(e)}nextPageUrl(){return this.meta.currentPage>=this.meta.lastPage?null:this.url(this.meta.currentPage+1)}previousPageUrl(){return this.meta.currentPage<=1?null:this.url(this.meta.currentPage-1)}firstPageUrl(){return this.url(1)}lastPageUrl(){return this.url(this.meta.lastPage)}toJSON(){return{data:this.data,meta:this.meta,links:{first:this.firstPageUrl(),last:this.lastPageUrl(),prev:this.previousPageUrl(),next:this.nextPageUrl()}}}},$=class{data;meta;urlDriver;constructor(e,t,n,r,i={}){let a=e.all().length,o=a===0?null:(n-1)*t+1,s=a===0?null:(o??1)+a-1;this.data=e;let c=K();this.urlDriver=c?c(i):new Z(i),this.meta={perPage:t,currentPage:n,from:o,to:s,hasMorePages:r}}getPageName(){return this.urlDriver.getPageName()}url(e){return this.urlDriver.url(e)}nextPageUrl(){return this.meta.hasMorePages?this.url(this.meta.currentPage+1):null}previousPageUrl(){return this.meta.currentPage<=1?null:this.url(this.meta.currentPage-1)}toJSON(){return{data:this.data,meta:this.meta,links:{prev:this.previousPageUrl(),next:this.nextPageUrl()}}}},Je=class e{args={};eagerLoads={};includeTrashed=!1;onlyTrashedRecords=!1;randomOrderEnabled=!1;relationFilters=[];relationAggregates=[];constructor(e,t){this.delegate=e,this.model=t}where(e){return this.addLogicalWhere(`AND`,e)}orWhere(e){return this.addLogicalWhere(`OR`,e)}whereNot(e){return this.where({NOT:e})}orWhereNot(e){return this.orWhere({NOT:e})}whereNull(e){return this.where({[e]:null})}whereNotNull(e){return this.where({[e]:{not:null}})}whereBetween(e,t){let[n,r]=t;return this.where({[e]:{gte:n,lte:r}})}whereDate(e,t){let n=this.coerceDate(t),r=new Date(Date.UTC(n.getUTCFullYear(),n.getUTCMonth(),n.getUTCDate())),i=new Date(r);return i.setUTCDate(i.getUTCDate()+1),this.where({[e]:{gte:r,lt:i}})}whereMonth(e,t,n=new Date().getUTCFullYear()){let r=Math.min(12,Math.max(1,t)),i=new Date(Date.UTC(n,r-1,1)),a=new Date(Date.UTC(n,r,1));return this.where({[e]:{gte:i,lt:a}})}whereYear(e,t){let n=new Date(Date.UTC(t,0,1)),r=new Date(Date.UTC(t+1,0,1));return this.where({[e]:{gte:n,lt:r}})}whereKeyNot(e,t){return this.where({[e]:{not:t}})}orWhereIn(e,t){return this.orWhere({[e]:{in:t}})}whereNotIn(e,t){return this.where({[e]:{notIn:t}})}orWhereNotIn(e,t){return this.orWhere({[e]:{notIn:t}})}async firstWhere(e,t,n){let r=n!==void 0,i=r?t:`=`,a=r?n:t;return this.clone().where(this.buildComparisonWhere(e,i,a)).first()}addLogicalWhere(e,t){return this.args.where?(this.args.where={[e]:[this.args.where,t]},this):(this.args.where=t,this)}buildComparisonWhere(e,t,n){return t===`=`?{[e]:n}:t===`!=`?{[e]:{not:n}}:t===`>`?{[e]:{gt:n}}:t===`>=`?{[e]:{gte:n}}:t===`<`?{[e]:{lt:n}}:{[e]:{lte:n}}}coerceDate(e){let t=e instanceof Date?new Date(e.getTime()):new Date(e);if(Number.isNaN(t.getTime()))throw new y(`Invalid date value for date-based query helper.`);return t}whereKey(e,t){return this.where({[e]:t})}whereIn(e,t){return this.where({[e]:{in:t}})}orderBy(e){return this.randomOrderEnabled=!1,this.args.orderBy=e,this}inRandomOrder(){return this.randomOrderEnabled=!0,this}reorder(e,t=`asc`){return this.args.orderBy=void 0,this.randomOrderEnabled=!1,e?this.orderBy({[e]:t}):this}latest(e=`createdAt`){return this.orderBy({[e]:`desc`})}oldest(e=`createdAt`){return this.orderBy({[e]:`asc`})}include(e){return this.args.include=e,this}with(e){let t=this.normalizeWith(e),n=Object.keys(t);return this.args.include={...this.args.include||{},...n.reduce((e,t)=>(e[t]=!0,e),{})},Object.entries(t).forEach(([e,t])=>{this.eagerLoads[e]=t}),this}has(e,t=`>=`,n=1,r){return this.relationFilters.push({relation:e,callback:r,operator:t,count:n,boolean:`AND`}),this}orHas(e,t=`>=`,n=1){return this.relationFilters.push({relation:e,operator:t,count:n,boolean:`OR`}),this}doesntHave(e,t){return this.has(e,`<`,1,t)}orDoesntHave(e){return this.orHas(e,`<`,1)}whereHas(e,t,n=`>=`,r=1){return this.has(e,n,r,t)}orWhereHas(e,t,n=`>=`,r=1){return this.relationFilters.push({relation:e,callback:t,operator:n,count:r,boolean:`OR`}),this}whereDoesntHave(e,t){return this.whereHas(e,t,`<`,1)}orWhereDoesntHave(e,t){return this.orWhereHas(e,t,`<`,1)}withCount(e){return(Array.isArray(e)?e:[e]).forEach(e=>{this.relationAggregates.push({type:`count`,relation:e})}),this}withExists(e){return(Array.isArray(e)?e:[e]).forEach(e=>{this.relationAggregates.push({type:`exists`,relation:e})}),this}withSum(e,t){return this.relationAggregates.push({type:`sum`,relation:e,column:t}),this}withAvg(e,t){return this.relationAggregates.push({type:`avg`,relation:e,column:t}),this}withMin(e,t){return this.relationAggregates.push({type:`min`,relation:e,column:t}),this}withMax(e,t){return this.relationAggregates.push({type:`max`,relation:e,column:t}),this}withTrashed(){return this.includeTrashed=!0,this.onlyTrashedRecords=!1,this}onlyTrashed(){return this.onlyTrashedRecords=!0,this.includeTrashed=!1,this}withoutTrashed(){return this.includeTrashed=!1,this.onlyTrashedRecords=!1,this}scope(e,...t){let n=`scope${e.charAt(0).toUpperCase()}${e.slice(1)}`,r=this.model.prototype?.[n];if(typeof r!=`function`)throw new y(`Scope [${e}] is not defined.`);let i=r.call(void 0,this,...t);return i&&i!==this?i:this}when(e,t,n){let r=typeof e==`function`?e():e;return r?t(this,r):n?n(this,r):this}unless(e,t,n){let r=typeof e==`function`?e():e;return r?n?n(this,r):this:t(this,r)}tap(e){return e(this),this}pipe(e){return e(this)}select(e){return this.args.select=e,this}skip(e){return this.args.skip=e,this}offset(e){return this.skip(e)}take(e){return this.args.take=e,this}limit(e){return this.take(e)}forPage(e,t=15){let n=Math.max(1,e),r=Math.max(1,t);return this.skip((n-1)*r).take(r)}async get(){let e=new WeakMap,t=await this.delegate.findMany(this.buildFindArgs()),n=this.randomOrderEnabled?this.shuffleRows(t):t,r=this.model.hydrateMany(n),i=r;if(this.hasRelationFilters())if(this.hasOrRelationFilters()&&this.args.where){let t=new Set(r.map(e=>this.getModelId(e)).filter(e=>e!=null)),n=await this.delegate.findMany({...this.args,where:this.buildSoftDeleteOnlyWhere()}),a=this.model.hydrateMany(n);i=await this.filterModelsByRelationConstraints(a,e,t)}else i=await this.filterModelsByRelationConstraints(r,e);return this.hasRelationAggregates()&&await this.applyRelationAggregates(i,e),await Promise.all(i.map(async e=>{await e.load(this.eagerLoads)})),new J(i)}async first(){if(this.hasRelationFilters()||this.hasRelationAggregates())return(await this.get()).all()[0]??null;if(this.randomOrderEnabled){let e=await this.delegate.findMany(this.buildFindArgs());if(e.length===0)return null;let t=this.shuffleRows(e)[0];if(!t)return null;let n=this.model.hydrate(t);return await n.load(this.eagerLoads),n}let e=await this.delegate.findFirst(this.buildFindArgs());if(!e)return null;let t=this.model.hydrate(e);return await t.load(this.eagerLoads),t}async firstOrFail(){let e=await this.first();if(!e)throw new Y(`Record not found.`);return e}async find(e,t=`id`){return this.where({[t]:e}).first()}async findOr(e,t,n){let r=typeof t==`string`?t:`id`,i=typeof t==`function`?t:n;if(!i)throw new y(`findOr requires a fallback callback.`);return await this.find(e,r)||i()}async value(e){let t=await this.delegate.findFirst(this.buildFindArgs());return t?t[e]??null:null}async valueOrFail(e){let t=await this.value(e);if(t==null)throw new Y(`Record not found.`);return t}async pluck(e,t){let n=await this.delegate.findMany(this.buildFindArgs());return t?new J(n.sort((e,n)=>String(e[t]).localeCompare(String(n[t]))).map(t=>t[e])):new J(n.map(t=>t[e]))}async create(e){let t=await this.delegate.create({data:e});return this.model.hydrate(t)}async update(e){let t=this.buildWhere();if(!t)throw new y(`Update requires a where clause.`);let n=await this.resolveUniqueWhere(t),r=await this.delegate.update({where:n,data:e});return this.model.hydrate(r)}async delete(){let e=this.buildWhere();if(!e)throw new y(`Delete requires a where clause.`);let t=await this.resolveUniqueWhere(e),n=await this.delegate.delete({where:t});return this.model.hydrate(n)}async count(){return this.hasRelationFilters()?(await this.get()).all().length:this.delegate.count({where:this.buildWhere()})}async exists(){return this.hasRelationFilters()?await this.count()>0:await this.delegate.findFirst(this.buildFindArgs())!=null}async doesntExist(){return!await this.exists()}async existsOr(e){return await this.exists()?!0:e()}async doesntExistOr(e){return await this.doesntExist()?!0:e()}async min(e){let t=await this.delegate.findMany(this.buildFindArgs());if(t.length===0)return null;let n=t.map(t=>t[e]).filter(e=>e!=null);return n.length===0?null:n.reduce((e,t)=>t<e?t:e)}async max(e){let t=await this.delegate.findMany(this.buildFindArgs());if(t.length===0)return null;let n=t.map(t=>t[e]).filter(e=>e!=null);return n.length===0?null:n.reduce((e,t)=>t>e?t:e)}async sum(e){return(await this.delegate.findMany(this.buildFindArgs())).reduce((t,n)=>{let r=n[e],i=typeof r==`number`?r:Number(r);return Number.isFinite(i)?t+i:t},0)}async avg(e){let t=(await this.delegate.findMany(this.buildFindArgs())).map(t=>{let n=t[e];return typeof n==`number`?n:Number(n)}).filter(e=>Number.isFinite(e));return t.length===0?null:t.reduce((e,t)=>e+t,0)/t.length}whereRaw(e,t=[]){let n=this.delegate;if(typeof n.applyRawWhere!=`function`)throw new y(`Raw where clauses are not supported by the current adapter.`);return this.args.where=n.applyRawWhere(this.buildWhere(),e,t),this}orWhereRaw(e,t=[]){let n=this.delegate;if(typeof n.applyRawWhere!=`function`)throw new y(`Raw where clauses are not supported by the current adapter.`);let r=n.applyRawWhere(void 0,e,t);return this.orWhere(r)}async paginate(e=1,t=15,n={}){if(this.hasRelationFilters()||this.hasRelationAggregates()){let r=Math.max(1,e),i=Math.max(1,t),a=(await this.get()).all(),o=(r-1)*i;return new Q(new J(a.slice(o,o+i)),a.length,i,r,n)}let r=Math.max(1,e),i=Math.max(1,t),a=await this.count();return new Q(await this.clone().skip((r-1)*i).take(i).get(),a,i,r,n)}async simplePaginate(e=15,t=1,n={}){if(this.hasRelationFilters()||this.hasRelationAggregates()){let r=Math.max(1,t),i=Math.max(1,e),a=(await this.get()).all(),o=(r-1)*i,s=a.slice(o,o+i),c=o+i<a.length;return new $(new J(s),i,r,c,n)}let r=Math.max(1,t),i=Math.max(1,e),a=await this.clone().skip((r-1)*i).take(i+1).get(),o=a.all().length>i;return new $(o?new J(a.all().slice(0,i)):a,i,r,o,n)}clone(){let t=new e(this.delegate,this.model);return t.args.where=this.args.where,t.args.include=this.args.include,t.args.orderBy=this.args.orderBy,t.args.select=this.args.select,t.args.skip=this.args.skip,t.args.take=this.args.take,t.includeTrashed=this.includeTrashed,t.onlyTrashedRecords=this.onlyTrashedRecords,t.randomOrderEnabled=this.randomOrderEnabled,this.relationFilters.forEach(e=>{t.relationFilters.push({...e})}),this.relationAggregates.forEach(e=>{t.relationAggregates.push({...e})}),Object.entries(this.eagerLoads).forEach(([e,n])=>{t.eagerLoads[e]=n}),t}normalizeWith(e){return typeof e==`string`?{[e]:void 0}:Array.isArray(e)?e.reduce((e,t)=>(e[t]=void 0,e),{}):e}buildWhere(){let e=this.model.getSoftDeleteConfig();if(!e.enabled||this.includeTrashed)return this.args.where;let t=this.onlyTrashedRecords?{[e.column]:{not:null}}:{[e.column]:null};return this.args.where?{AND:[this.args.where,t]}:t}buildFindArgs(){return{...this.args,where:this.buildWhere()}}async resolveUniqueWhere(e){if(this.isUniqueWhere(e))return e;let t=await this.delegate.findFirst({where:e});if(!t)throw new y(`Record not found for update/delete operation.`);let n=t;if(!Object.prototype.hasOwnProperty.call(n,`id`))throw new y(`Unable to resolve a unique identifier for update/delete operation. Include an id in the query constraints.`);return{id:n.id}}isUniqueWhere(e){return Object.keys(e).length===1&&Object.prototype.hasOwnProperty.call(e,`id`)}shuffleRows(e){let t=[...e];for(let e=t.length-1;e>0;e--){let n=Math.floor(Math.random()*(e+1)),r=t[e];t[e]=t[n],t[n]=r}return t}hasRelationFilters(){return this.relationFilters.length>0}hasOrRelationFilters(){return this.relationFilters.some(e=>e.boolean===`OR`)}hasRelationAggregates(){return this.relationAggregates.length>0}async filterModelsByRelationConstraints(e,t,n){return(await Promise.all(e.map(async e=>{let r=null;n&&(r=n.has(this.getModelId(e)));for(let n of this.relationFilters){let i=await this.resolveRelatedCount(e,n.relation,t,n.callback),a=this.compareCount(i,n.operator,n.count);r=r==null?a:n.boolean===`AND`?r&&a:r||a}return{model:e,passes:r??!0}}))).filter(e=>e.passes).map(e=>e.model)}getModelId(e){let t=e;if(typeof t.getAttribute!=`function`)return null;let n=t.getAttribute(`id`);return typeof n==`number`||typeof n==`string`?n:null}buildSoftDeleteOnlyWhere(){let e=this.model.getSoftDeleteConfig();if(e.enabled&&!this.includeTrashed)return this.onlyTrashedRecords?{[e.column]:{not:null}}:{[e.column]:null}}async applyRelationAggregates(e,t){let n=t??new WeakMap;await Promise.all(e.map(async e=>{for(let t of this.relationAggregates){let r=await this.resolveRelatedResults(e,t.relation,n),i=Array.isArray(r)?r:r?[r]:[],a=this.buildAggregateAttributeKey(t);if(t.type===`count`){this.assignAggregate(e,a,i.length);continue}if(t.type===`exists`){this.assignAggregate(e,a,i.length>0);continue}let o=i.map(e=>e.getAttribute(t.column)).filter(e=>e!=null);if(t.type===`sum`){let t=o.reduce((e,t)=>{let n=typeof t==`number`?t:Number(t);return Number.isFinite(n)?e+n:e},0);this.assignAggregate(e,a,t);continue}if(t.type===`avg`){let t=o.map(e=>typeof e==`number`?e:Number(e)).filter(e=>Number.isFinite(e)),n=t.length===0?null:t.reduce((e,t)=>e+t,0)/t.length;this.assignAggregate(e,a,n);continue}if(t.type===`min`){let t=o.length===0?null:o.reduce((e,t)=>t<e?t:e);this.assignAggregate(e,a,t);continue}let s=o.length===0?null:o.reduce((e,t)=>t>e?t:e);this.assignAggregate(e,a,s)}}))}async resolveRelatedCount(e,t,n,r){let i=await this.resolveRelatedResults(e,t,n,r);return Array.isArray(i)?i.length:i?1:0}async resolveRelatedResults(e,t,n,r){let i=e,a=r??`__none__`,o=n.get(i);o||(o=new Map,n.set(i,o));let s=o.get(t);s||(s=new Map,o.set(t,s));let c=s.get(a);if(c)return await c;let l=(async()=>{let n=e[t];if(typeof n!=`function`)throw new y(`Relation [${t}] is not defined on the model.`);let i=n.call(e);if(r&&typeof i.constrain==`function`&&i.constrain(e=>r(e)??e),typeof i.get==`function`){let e=await i.get();return e instanceof J?e.all():e}if(typeof i.getResults==`function`){let e=await i.getResults();return e instanceof J?e.all():e}throw new y(`Relation [${t}] does not support result resolution.`)})();return s.set(a,l),await l}compareCount(e,t,n){return t===`>=`?e>=n:t===`>`?e>n:t===`=`?e===n:t===`!=`?e!==n:t===`<=`?e<=n:e<n}buildAggregateAttributeKey(e){let t=e.relation;if(e.type===`count`)return`${t}Count`;if(e.type===`exists`)return`${t}Exists`;let n=e.column?`${e.column.charAt(0).toUpperCase()}${e.column.slice(1)}`:``;return`${t}${`${e.type.charAt(0).toUpperCase()}${e.type.slice(1)}`}${n}`}assignAggregate(e,t,n){let r=e;if(typeof r.setAttribute==`function`){r.setAttribute(t,n);return}e[t]=n}},Ye=class e{static factoryClass;static client;static delegate;static softDeletes=!1;static deletedAtColumn=`deletedAt`;static globalScopes={};static eventListeners={};casts={};hidden=[];visible=[];appends=[];attributes;constructor(e={}){return this.attributes={},this.fill(e),new Proxy(this,{get:(e,t,n)=>typeof t!=`string`||t in e?Reflect.get(e,t,n):e.getAttribute(t),set:(e,t,n,r)=>typeof t!=`string`||t in e?Reflect.set(e,t,n,r):(e.setAttribute(t,n),!0)})}static setClient(e){this.client=e}static setFactory(e){this.factoryClass=e}static factory(e){let t=this.factoryClass;if(!t)throw new y(`Factory is not configured for model [${this.name}].`);let n=new t;return typeof e==`number`&&n.count(e),n}static addGlobalScope(e,t){this.ensureOwnGlobalScopes(),this.globalScopes[e]=t}static removeGlobalScope(e){this.ensureOwnGlobalScopes(),delete this.globalScopes[e]}static clearGlobalScopes(){this.globalScopes={}}static on(e,t){this.ensureOwnEventListeners(),this.eventListeners[e]||(this.eventListeners[e]=[]),this.eventListeners[e]?.push(t)}static off(e,t){if(this.ensureOwnEventListeners(),!t){delete this.eventListeners[e];return}this.eventListeners[e]=(this.eventListeners[e]||[]).filter(e=>e!==t)}static clearEventListeners(){this.eventListeners={}}static getDelegate(e){xe();let t=e||this.delegate||`${(0,m.str)(this.name).camel().plural()}`,n=[t,`${(0,m.str)(t).camel()}`,`${(0,m.str)(t).singular()}`,`${(0,m.str)(t).camel().singular()}`],r=Se(),i=n.map(e=>this.client?.[e]??r?.[e]).find(e=>q(e));if(!i)throw new y(`Database delegate [${t}] is not configured.`);return i}static query(){let e=new Je(this.getDelegate(),this),t=this;return t.ensureOwnGlobalScopes(),Object.values(t.globalScopes).forEach(t=>{let n=t(e);n&&n!==e&&(e=n)}),e}static withTrashed(){return this.query().withTrashed()}static onlyTrashed(){return this.query().onlyTrashed()}static scope(e,...t){return this.query().scope(e,...t)}static getSoftDeleteConfig(){return{enabled:this.softDeletes,column:this.deletedAtColumn}}static hydrate(e){return new this(e)}static hydrateMany(e){return e.map(e=>new this(e))}fill(e){return Object.entries(e).forEach(([e,t])=>{this.setAttribute(e,t)}),this}getAttribute(e){let t=this.resolveGetMutator(e),n=this.casts[e],r=this.attributes[e];return n&&(r=v(n).get(r)),t?t.call(this,r):r}setAttribute(e,t){let n=this.resolveSetMutator(e),r=this.casts[e],i=t;return n&&(i=n.call(this,i)),r&&(i=v(r).set(i)),this.attributes[e]=i,this}async save(){let t=this.getAttribute(`id`),n=this.getRawAttributes(),r=this.constructor;if(t==null){await e.dispatchEvent(r,`saving`,this),await e.dispatchEvent(r,`creating`,this);let t=await r.query().create(n);return this.fill(t.getRawAttributes()),await e.dispatchEvent(r,`created`,this),await e.dispatchEvent(r,`saved`,this),this}await e.dispatchEvent(r,`saving`,this),await e.dispatchEvent(r,`updating`,this);let i=await r.query().where({id:t}).update(n);return this.fill(i.getRawAttributes()),await e.dispatchEvent(r,`updated`,this),await e.dispatchEvent(r,`saved`,this),this}async delete(){let t=this.getAttribute(`id`);if(t==null)throw new y(`Cannot delete a model without an id.`);let n=this.constructor;await e.dispatchEvent(n,`deleting`,this);let r=n.getSoftDeleteConfig();if(r.enabled){let i=await n.query().where({id:t}).update({[r.column]:new Date});return this.fill(i.getRawAttributes()),await e.dispatchEvent(n,`deleted`,this),this}let i=await n.query().where({id:t}).delete();return this.fill(i.getRawAttributes()),await e.dispatchEvent(n,`deleted`,this),this}async forceDelete(){let t=this.getAttribute(`id`);if(t==null)throw new y(`Cannot force delete a model without an id.`);let n=this.constructor;await e.dispatchEvent(n,`forceDeleting`,this),await e.dispatchEvent(n,`deleting`,this);let r=await n.query().withTrashed().where({id:t}).delete();return this.fill(r.getRawAttributes()),await e.dispatchEvent(n,`deleted`,this),await e.dispatchEvent(n,`forceDeleted`,this),this}async restore(){let t=this.getAttribute(`id`);if(t==null)throw new y(`Cannot restore a model without an id.`);let n=this.constructor,r=n.getSoftDeleteConfig();if(!r.enabled)return this;await e.dispatchEvent(n,`restoring`,this);let i=await n.query().withTrashed().where({id:t}).update({[r.column]:null});return this.fill(i.getRawAttributes()),await e.dispatchEvent(n,`restored`,this),this}async load(e){let t=this.normalizeRelationMap(e);return await Promise.all(Object.entries(t).map(async([e,t])=>{let n=this[e];if(typeof n!=`function`)return;let r=n.call(this);t&&r.constrain(t);let i=await r.getResults();this.attributes[e]=i})),this}getRawAttributes(){return{...this.attributes}}toObject(){let e=(this.visible.length>0?this.visible:Object.keys(this.attributes).filter(e=>!this.hidden.includes(e))).reduce((e,t)=>{let n=this.getAttribute(t);return n instanceof Date&&(n=n.toISOString()),e[t]=n,e},{});return this.appends.forEach(t=>{e[t]=this.getAttribute(t)}),e}toJSON(){return this.toObject()}hasOne(e,t,n=`id`){return new Ue(this,e,t,n)}hasMany(e,t,n=`id`){return new Ve(this,e,t,n)}belongsTo(e,t,n=`id`){return new Be(this,e,t,n)}belongsToMany(e,t,n,r,i=`id`,a=`id`){return new ze(this,e,t,n,r,i,a)}hasOneThrough(e,t,n,r,i=`id`,a=`id`){return new We(this,e,t,n,r,i,a)}hasManyThrough(e,t,n,r,i=`id`,a=`id`){return new He(this,e,t,n,r,i,a)}morphOne(e,t,n=`id`){return new Ke(this,e,t,n)}morphMany(e,t,n=`id`){return new Ge(this,e,t,n)}morphToMany(e,t,n,r,i=`id`,a=`id`){return new qe(this,e,t,n,r,i,a)}resolveGetMutator(e){let t=`get${(0,m.str)(e).studly()}Attribute`,n=this[t];return typeof n==`function`?n:null}resolveSetMutator(e){let t=`set${(0,m.str)(e).studly()}Attribute`,n=this[t];return typeof n==`function`?n:null}static ensureOwnGlobalScopes(){Object.prototype.hasOwnProperty.call(this,`globalScopes`)||(this.globalScopes={...this.globalScopes||{}})}static ensureOwnEventListeners(){Object.prototype.hasOwnProperty.call(this,`eventListeners`)||(this.eventListeners={...this.eventListeners||{}})}static async dispatchEvent(e,t,n){e.ensureOwnEventListeners();let r=e.eventListeners[t]||[];for(let e of r)await e(n)}normalizeRelationMap(e){return typeof e==`string`?{[e]:void 0}:Array.isArray(e)?e.reduce((e,t)=>(e[t]=void 0,e),{}):e}};exports.ArkormCollection=J,exports.ArkormException=y,exports.CliApp=Ce,exports.InitCommand=we,exports.InlineFactory=Pe,exports.LengthAwarePaginator=Q,exports.MakeFactoryCommand=Te,exports.MakeMigrationCommand=Ee,exports.MakeModelCommand=De,exports.MakeSeederCommand=Oe,exports.MigrateCommand=ke,exports.Migration=b,exports.Model=Ye,exports.ModelFactory=Ne,exports.ModelNotFoundException=Y,exports.ModelsSyncCommand=Ae,exports.PRISMA_MODEL_REGEX=ie,exports.Paginator=$,exports.QueryBuilder=Je,exports.SchemaBuilder=re,exports.SeedCommand=Me,exports.Seeder=je,exports.TableBuilder=x,exports.URLDriver=Z,exports.applyAlterTableOperation=k,exports.applyCreateTableOperation=O,exports.applyDropTableOperation=A,exports.applyMigrationToPrismaSchema=P,exports.applyOperationsToPrismaSchema=se,exports.buildFieldLine=T,exports.buildIndexLine=E,exports.buildMigrationSource=ue,exports.buildModelBlock=oe,exports.configureArkormRuntime=H,exports.createMigrationTimestamp=N,exports.createPrismaAdapter=Ie,exports.createPrismaDelegateMap=Le,exports.defineConfig=ge,exports.defineFactory=Fe,exports.ensureArkormConfigLoading=xe,exports.escapeRegex=C,exports.findModelBlock=D,exports.formatDefaultValue=w,exports.generateMigrationFile=de,exports.getDefaultStubsPath=G,exports.getMigrationPlan=fe,exports.getRuntimePaginationURLDriverFactory=K,exports.getRuntimePrismaClient=Se,exports.getUserConfig=V,exports.inferDelegateName=Re,exports.isDelegateLike=q,exports.loadArkormConfig=W,exports.pad=M,exports.resetArkormRuntimeForTests=_e,exports.resolveCast=v,exports.resolveMigrationClassName=ce,exports.resolvePrismaType=ae,exports.runMigrationWithPrisma=pe,exports.runPrismaCommand=j,exports.toMigrationFileSlug=le,exports.toModelName=S;
|
|
2045
|
+
`;
|
|
2046
|
+
|
|
2047
|
+
//#endregion
|
|
2048
|
+
//#region src/Collection.ts
|
|
2049
|
+
var ArkormCollection = class extends _h3ravel_collect_js.Collection {};
|
|
2050
|
+
|
|
2051
|
+
//#endregion
|
|
2052
|
+
//#region src/database/factories.ts
|
|
2053
|
+
/**
|
|
2054
|
+
* Base class for defining model factories.
|
|
2055
|
+
* Not meant to be used directly.
|
|
2056
|
+
*
|
|
2057
|
+
* @template TModel The type of model the factory creates.
|
|
2058
|
+
* @template TAttributes The type of attributes used to create the model.
|
|
2059
|
+
* @author Legacy (3m1n3nc3)
|
|
2060
|
+
* @since 0.1.0
|
|
2061
|
+
*/
|
|
2062
|
+
var ModelFactory = class {
|
|
2063
|
+
amount = 1;
|
|
2064
|
+
sequence = 0;
|
|
2065
|
+
states = [];
|
|
2066
|
+
/**
|
|
2067
|
+
* Set the number of models to create.
|
|
2068
|
+
*
|
|
2069
|
+
* @param amount
|
|
2070
|
+
* @returns
|
|
2071
|
+
*/
|
|
2072
|
+
count(amount) {
|
|
2073
|
+
this.amount = Math.max(1, Math.floor(amount));
|
|
2074
|
+
return this;
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Define a state transformation for the factory.
|
|
2078
|
+
* States are applied in the order they were defined.
|
|
2079
|
+
*
|
|
2080
|
+
* @param resolver A function that takes the current attributes and sequence number, and returns the transformed attributes.
|
|
2081
|
+
* @returns The factory instance for chaining.
|
|
2082
|
+
*/
|
|
2083
|
+
state(resolver) {
|
|
2084
|
+
this.states.push(resolver);
|
|
2085
|
+
return this;
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Create a new model instance without saving it to the database.
|
|
2089
|
+
*
|
|
2090
|
+
* @param overrides
|
|
2091
|
+
* @returns
|
|
2092
|
+
*/
|
|
2093
|
+
make(overrides = {}) {
|
|
2094
|
+
const attributes = this.buildAttributes(overrides);
|
|
2095
|
+
return new this.model(attributes);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Create multiple model instances without saving them to the database.
|
|
2099
|
+
*
|
|
2100
|
+
* @param amount
|
|
2101
|
+
* @param overrides
|
|
2102
|
+
* @returns
|
|
2103
|
+
*/
|
|
2104
|
+
makeMany(amount = this.amount, overrides = {}) {
|
|
2105
|
+
const total = Math.max(1, Math.floor(amount));
|
|
2106
|
+
return Array.from({ length: total }, () => this.make(overrides));
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Create a new model instance and save it to the database.
|
|
2110
|
+
*
|
|
2111
|
+
* @param overrides
|
|
2112
|
+
* @returns
|
|
2113
|
+
*/
|
|
2114
|
+
async create(overrides = {}) {
|
|
2115
|
+
const model = this.make(overrides);
|
|
2116
|
+
if (typeof model.save !== "function") throw new Error("Factory model does not support save().");
|
|
2117
|
+
return await model.save();
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Create multiple model instances and save them to the database.
|
|
2121
|
+
*
|
|
2122
|
+
* @param amount
|
|
2123
|
+
* @param overrides
|
|
2124
|
+
* @returns
|
|
2125
|
+
*/
|
|
2126
|
+
async createMany(amount = this.amount, overrides = {}) {
|
|
2127
|
+
const models = this.makeMany(amount, overrides);
|
|
2128
|
+
return await Promise.all(models.map(async (model) => {
|
|
2129
|
+
if (typeof model.save !== "function") throw new Error("Factory model does not support save().");
|
|
2130
|
+
return await model.save();
|
|
2131
|
+
}));
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Build the attributes for a model instance, applying the factory
|
|
2135
|
+
* definition and any defined states, and merging in any overrides.
|
|
2136
|
+
*
|
|
2137
|
+
* @param overrides
|
|
2138
|
+
* @returns
|
|
2139
|
+
*/
|
|
2140
|
+
buildAttributes(overrides) {
|
|
2141
|
+
const sequence = this.sequence;
|
|
2142
|
+
this.sequence += 1;
|
|
2143
|
+
let resolved = this.definition(sequence);
|
|
2144
|
+
for (const state of this.states) resolved = state(resolved, sequence);
|
|
2145
|
+
return {
|
|
2146
|
+
...resolved,
|
|
2147
|
+
...overrides
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
/**
|
|
2152
|
+
* A helper class for defining factories using an inline definition
|
|
2153
|
+
* function, without needing to create a separate factory class.
|
|
2154
|
+
*
|
|
2155
|
+
* @template TModel
|
|
2156
|
+
* @template TAttributes
|
|
2157
|
+
* @author Legacy (3m1n3nc3)
|
|
2158
|
+
* @since 0.1.0
|
|
2159
|
+
*/
|
|
2160
|
+
var InlineFactory = class extends ModelFactory {
|
|
2161
|
+
model;
|
|
2162
|
+
constructor(model, resolver) {
|
|
2163
|
+
super();
|
|
2164
|
+
this.resolver = resolver;
|
|
2165
|
+
this.model = model;
|
|
2166
|
+
}
|
|
2167
|
+
definition(sequence) {
|
|
2168
|
+
return this.resolver(sequence);
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
/**
|
|
2172
|
+
* Define a factory for a given model using an inline definition function.
|
|
2173
|
+
*
|
|
2174
|
+
* @template TModel The type of model the factory creates.
|
|
2175
|
+
* @template TAttributes The type of attributes used to create the model.
|
|
2176
|
+
* @param model The model constructor.
|
|
2177
|
+
* @param definition The factory definition function.
|
|
2178
|
+
* @returns A new instance of the model factory.
|
|
2179
|
+
*/
|
|
2180
|
+
const defineFactory = (model, definition) => {
|
|
2181
|
+
return new InlineFactory(model, definition);
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
//#endregion
|
|
2185
|
+
//#region src/Exceptions/ModelNotFoundException.ts
|
|
2186
|
+
/**
|
|
2187
|
+
* The ModelNotFoundException class is a custom error type for handling
|
|
2188
|
+
* cases where a requested model instance cannot be found in the database.
|
|
2189
|
+
*
|
|
2190
|
+
* @author Legacy (3m1n3nc3)
|
|
2191
|
+
* @since 0.1.0
|
|
2192
|
+
*/
|
|
2193
|
+
var ModelNotFoundException = class extends ArkormException {
|
|
2194
|
+
constructor(message = "No query results for the given model.") {
|
|
2195
|
+
super(message);
|
|
2196
|
+
this.name = "ModelNotFoundException";
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
|
|
2200
|
+
//#endregion
|
|
2201
|
+
//#region src/helpers/prisma.ts
|
|
2202
|
+
/**
|
|
2203
|
+
* Create an adapter to convert a Prisma client instance into a format
|
|
2204
|
+
* compatible with ArkORM's expectations.
|
|
2205
|
+
*
|
|
2206
|
+
* @param prisma The Prisma client instance to adapt.
|
|
2207
|
+
* @param mapping An optional mapping of Prisma delegate names to ArkORM delegate names.
|
|
2208
|
+
* @returns A record of adapted Prisma delegates compatible with ArkORM.
|
|
2209
|
+
*/
|
|
2210
|
+
function createPrismaAdapter(prisma) {
|
|
2211
|
+
return Object.entries(prisma).reduce((accumulator, [key, value]) => {
|
|
2212
|
+
if (!isDelegateLike(value)) return accumulator;
|
|
2213
|
+
accumulator[key] = value;
|
|
2214
|
+
return accumulator;
|
|
2215
|
+
}, {});
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Create a delegate mapping record for Model.setClient() from a Prisma client.
|
|
2219
|
+
*
|
|
2220
|
+
* @param prisma The Prisma client instance.
|
|
2221
|
+
* @param mapping Optional mapping of Arkormˣ delegate names to Prisma delegate names.
|
|
2222
|
+
* @returns A delegate map keyed by Arkormˣ delegate names.
|
|
2223
|
+
*/
|
|
2224
|
+
function createPrismaDelegateMap(prisma) {
|
|
2225
|
+
return createPrismaAdapter(prisma);
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Infer the Prisma delegate name for a given model name using a simple convention.
|
|
2229
|
+
*
|
|
2230
|
+
* @param modelName The name of the model to infer the delegate name for.
|
|
2231
|
+
* @returns The inferred Prisma delegate name.
|
|
2232
|
+
*/
|
|
2233
|
+
function inferDelegateName(modelName) {
|
|
2234
|
+
return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}s`;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
//#endregion
|
|
2238
|
+
//#region src/relationship/Relation.ts
|
|
2239
|
+
/**
|
|
2240
|
+
* Base class for all relationship types. Not meant to be used directly.
|
|
2241
|
+
*
|
|
2242
|
+
* @author Legacy (3m1n3nc3)
|
|
2243
|
+
* @since 0.1.0
|
|
2244
|
+
*/
|
|
2245
|
+
var Relation = class {
|
|
2246
|
+
constraint = null;
|
|
2247
|
+
/**
|
|
2248
|
+
* Apply a constraint to the relationship query.
|
|
2249
|
+
*
|
|
2250
|
+
* @param constraint The constraint function to apply to the query.
|
|
2251
|
+
* @returns The current relation instance.
|
|
2252
|
+
*/
|
|
2253
|
+
constrain(constraint) {
|
|
2254
|
+
if (!this.constraint) {
|
|
2255
|
+
this.constraint = constraint;
|
|
2256
|
+
return this;
|
|
2257
|
+
}
|
|
2258
|
+
const previousConstraint = this.constraint;
|
|
2259
|
+
this.constraint = (query) => {
|
|
2260
|
+
const constrained = previousConstraint(query) ?? query;
|
|
2261
|
+
return constraint(constrained) ?? constrained;
|
|
2262
|
+
};
|
|
2263
|
+
return this;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Add a where clause to the relationship query.
|
|
2267
|
+
*
|
|
2268
|
+
* @param where
|
|
2269
|
+
* @returns
|
|
2270
|
+
*/
|
|
2271
|
+
where(where) {
|
|
2272
|
+
return this.constrain((query) => query.where(where));
|
|
2273
|
+
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Add a strongly-typed where key clause to the relationship query.
|
|
2276
|
+
*
|
|
2277
|
+
* @param key
|
|
2278
|
+
* @param value
|
|
2279
|
+
* @returns
|
|
2280
|
+
*/
|
|
2281
|
+
whereKey(key, value) {
|
|
2282
|
+
return this.constrain((query) => query.whereKey(key, value));
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Add a strongly-typed where in clause to the relationship query.
|
|
2286
|
+
*
|
|
2287
|
+
* @param key
|
|
2288
|
+
* @param values
|
|
2289
|
+
* @returns
|
|
2290
|
+
*/
|
|
2291
|
+
whereIn(key, values) {
|
|
2292
|
+
return this.constrain((query) => query.whereIn(key, values));
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Add an order by clause to the relationship query.
|
|
2296
|
+
*
|
|
2297
|
+
* @param orderBy
|
|
2298
|
+
* @returns
|
|
2299
|
+
*/
|
|
2300
|
+
orderBy(orderBy) {
|
|
2301
|
+
return this.constrain((query) => query.orderBy(orderBy));
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Add an include clause to the relationship query.
|
|
2305
|
+
*
|
|
2306
|
+
* @param include
|
|
2307
|
+
* @returns
|
|
2308
|
+
*/
|
|
2309
|
+
include(include) {
|
|
2310
|
+
return this.constrain((query) => query.include(include));
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Add eager loading relations to the relationship query.
|
|
2314
|
+
*
|
|
2315
|
+
* @param relations
|
|
2316
|
+
* @returns
|
|
2317
|
+
*/
|
|
2318
|
+
with(relations) {
|
|
2319
|
+
return this.constrain((query) => query.with(relations));
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* Add a select clause to the relationship query.
|
|
2323
|
+
*
|
|
2324
|
+
* @param select
|
|
2325
|
+
* @returns
|
|
2326
|
+
*/
|
|
2327
|
+
select(select) {
|
|
2328
|
+
return this.constrain((query) => query.select(select));
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Add a skip clause to the relationship query.
|
|
2332
|
+
*
|
|
2333
|
+
* @param skip
|
|
2334
|
+
* @returns
|
|
2335
|
+
*/
|
|
2336
|
+
skip(skip) {
|
|
2337
|
+
return this.constrain((query) => query.skip(skip));
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Add a take clause to the relationship query.
|
|
2341
|
+
*
|
|
2342
|
+
* @param take
|
|
2343
|
+
* @returns
|
|
2344
|
+
*/
|
|
2345
|
+
take(take) {
|
|
2346
|
+
return this.constrain((query) => query.take(take));
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Include soft-deleted records in the relationship query.
|
|
2350
|
+
*
|
|
2351
|
+
* @returns
|
|
2352
|
+
*/
|
|
2353
|
+
withTrashed() {
|
|
2354
|
+
return this.constrain((query) => query.withTrashed());
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Limit relationship query to only soft-deleted records.
|
|
2358
|
+
*
|
|
2359
|
+
* @returns
|
|
2360
|
+
*/
|
|
2361
|
+
onlyTrashed() {
|
|
2362
|
+
return this.constrain((query) => query.onlyTrashed());
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Exclude soft-deleted records from the relationship query.
|
|
2366
|
+
*
|
|
2367
|
+
* @returns
|
|
2368
|
+
*/
|
|
2369
|
+
withoutTrashed() {
|
|
2370
|
+
return this.constrain((query) => query.withoutTrashed());
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Apply a scope to the relationship query.
|
|
2374
|
+
*
|
|
2375
|
+
* @param name
|
|
2376
|
+
* @param args
|
|
2377
|
+
* @returns
|
|
2378
|
+
*/
|
|
2379
|
+
scope(name, ...args) {
|
|
2380
|
+
return this.constrain((query) => query.scope(name, ...args));
|
|
2381
|
+
}
|
|
2382
|
+
/**
|
|
2383
|
+
* Apply the defined constraint to the given query, if any.
|
|
2384
|
+
*
|
|
2385
|
+
* @param query The query builder instance to apply the constraint to.
|
|
2386
|
+
*
|
|
2387
|
+
* @returns The query builder instance with the constraint applied, if any.
|
|
2388
|
+
*/
|
|
2389
|
+
applyConstraint(query) {
|
|
2390
|
+
if (!this.constraint) return query;
|
|
2391
|
+
return this.constraint(query) ?? query;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Execute the relationship query and return relation results.
|
|
2395
|
+
*
|
|
2396
|
+
* @returns
|
|
2397
|
+
*/
|
|
2398
|
+
async get() {
|
|
2399
|
+
return this.getResults();
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Execute the relationship query and return the first related model.
|
|
2403
|
+
*
|
|
2404
|
+
* @returns
|
|
2405
|
+
*/
|
|
2406
|
+
async first() {
|
|
2407
|
+
const results = await this.getResults();
|
|
2408
|
+
if (results instanceof ArkormCollection) return results.all()[0] ?? null;
|
|
2409
|
+
return results;
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
|
|
2413
|
+
//#endregion
|
|
2414
|
+
//#region src/relationship/BelongsToManyRelation.ts
|
|
2415
|
+
/**
|
|
2416
|
+
* Defines a many-to-many relationship.
|
|
2417
|
+
*
|
|
2418
|
+
* @author Legacy (3m1n3nc3)
|
|
2419
|
+
* @since 0.1.0
|
|
2420
|
+
*/
|
|
2421
|
+
var BelongsToManyRelation = class extends Relation {
|
|
2422
|
+
constructor(parent, related, throughDelegate, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
|
|
2423
|
+
super();
|
|
2424
|
+
this.parent = parent;
|
|
2425
|
+
this.related = related;
|
|
2426
|
+
this.throughDelegate = throughDelegate;
|
|
2427
|
+
this.foreignPivotKey = foreignPivotKey;
|
|
2428
|
+
this.relatedPivotKey = relatedPivotKey;
|
|
2429
|
+
this.parentKey = parentKey;
|
|
2430
|
+
this.relatedKey = relatedKey;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Fetches the related models for this relationship.
|
|
2434
|
+
*
|
|
2435
|
+
* @returns
|
|
2436
|
+
*/
|
|
2437
|
+
async getResults() {
|
|
2438
|
+
const parentValue = this.parent.getAttribute(this.parentKey);
|
|
2439
|
+
const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.foreignPivotKey]: parentValue } })).map((row) => row[this.relatedPivotKey]);
|
|
2440
|
+
return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
//#endregion
|
|
2445
|
+
//#region src/relationship/BelongsToRelation.ts
|
|
2446
|
+
/**
|
|
2447
|
+
* Defines an inverse one-to-one or many relationship.
|
|
2448
|
+
*
|
|
2449
|
+
* @author Legacy (3m1n3nc3)
|
|
2450
|
+
* @since 0.1.0
|
|
2451
|
+
*/
|
|
2452
|
+
var BelongsToRelation = class extends Relation {
|
|
2453
|
+
constructor(parent, related, foreignKey, ownerKey) {
|
|
2454
|
+
super();
|
|
2455
|
+
this.parent = parent;
|
|
2456
|
+
this.related = related;
|
|
2457
|
+
this.foreignKey = foreignKey;
|
|
2458
|
+
this.ownerKey = ownerKey;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Fetches the related models for this relationship.
|
|
2462
|
+
*
|
|
2463
|
+
* @returns
|
|
2464
|
+
*/
|
|
2465
|
+
async getResults() {
|
|
2466
|
+
const foreignValue = this.parent.getAttribute(this.foreignKey);
|
|
2467
|
+
return this.applyConstraint(this.related.query().where({ [this.ownerKey]: foreignValue })).first();
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
//#endregion
|
|
2472
|
+
//#region src/relationship/HasManyRelation.ts
|
|
2473
|
+
/**
|
|
2474
|
+
* Defines a one-to-many relationship.
|
|
2475
|
+
*
|
|
2476
|
+
* @author Legacy (3m1n3nc3)
|
|
2477
|
+
* @since 0.1.0
|
|
2478
|
+
*/
|
|
2479
|
+
var HasManyRelation = class extends Relation {
|
|
2480
|
+
constructor(parent, related, foreignKey, localKey) {
|
|
2481
|
+
super();
|
|
2482
|
+
this.parent = parent;
|
|
2483
|
+
this.related = related;
|
|
2484
|
+
this.foreignKey = foreignKey;
|
|
2485
|
+
this.localKey = localKey;
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Fetches the related models for this relationship.
|
|
2489
|
+
*
|
|
2490
|
+
* @returns
|
|
2491
|
+
*/
|
|
2492
|
+
async getResults() {
|
|
2493
|
+
const localValue = this.parent.getAttribute(this.localKey);
|
|
2494
|
+
return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue })).get();
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
|
|
2498
|
+
//#endregion
|
|
2499
|
+
//#region src/relationship/HasManyThroughRelation.ts
|
|
2500
|
+
/**
|
|
2501
|
+
* Defines a has-many-through relationship, which provides a convenient way to access
|
|
2502
|
+
* distant relations via an intermediate relation.
|
|
2503
|
+
*
|
|
2504
|
+
* @author Legacy (3m1n3nc3)
|
|
2505
|
+
* @since 0.1.0
|
|
2506
|
+
*/
|
|
2507
|
+
var HasManyThroughRelation = class extends Relation {
|
|
2508
|
+
constructor(parent, related, throughDelegate, firstKey, secondKey, localKey, secondLocalKey) {
|
|
2509
|
+
super();
|
|
2510
|
+
this.parent = parent;
|
|
2511
|
+
this.related = related;
|
|
2512
|
+
this.throughDelegate = throughDelegate;
|
|
2513
|
+
this.firstKey = firstKey;
|
|
2514
|
+
this.secondKey = secondKey;
|
|
2515
|
+
this.localKey = localKey;
|
|
2516
|
+
this.secondLocalKey = secondLocalKey;
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Fetches the related models for this relationship.
|
|
2520
|
+
*
|
|
2521
|
+
* @returns
|
|
2522
|
+
*/
|
|
2523
|
+
async getResults() {
|
|
2524
|
+
const localValue = this.parent.getAttribute(this.localKey);
|
|
2525
|
+
const keys = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.firstKey]: localValue } })).map((row) => row[this.secondLocalKey]);
|
|
2526
|
+
return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: keys } })).get();
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
//#endregion
|
|
2531
|
+
//#region src/relationship/HasOneRelation.ts
|
|
2532
|
+
/**
|
|
2533
|
+
* Represents a "has one" relationship between two models.
|
|
2534
|
+
*
|
|
2535
|
+
* @author Legacy (3m1n3nc3)
|
|
2536
|
+
* @since 0.1.0
|
|
2537
|
+
*/
|
|
2538
|
+
var HasOneRelation = class extends Relation {
|
|
2539
|
+
constructor(parent, related, foreignKey, localKey) {
|
|
2540
|
+
super();
|
|
2541
|
+
this.parent = parent;
|
|
2542
|
+
this.related = related;
|
|
2543
|
+
this.foreignKey = foreignKey;
|
|
2544
|
+
this.localKey = localKey;
|
|
2545
|
+
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Fetches the related models for this relationship.
|
|
2548
|
+
*
|
|
2549
|
+
* @returns
|
|
2550
|
+
*/
|
|
2551
|
+
async getResults() {
|
|
2552
|
+
const localValue = this.parent.getAttribute(this.localKey);
|
|
2553
|
+
return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue })).first();
|
|
2554
|
+
}
|
|
2555
|
+
};
|
|
2556
|
+
|
|
2557
|
+
//#endregion
|
|
2558
|
+
//#region src/relationship/HasOneThroughRelation.ts
|
|
2559
|
+
/**
|
|
2560
|
+
* Represents a "has one through" relationship, where the parent model is related
|
|
2561
|
+
* to exactly one instance of the related model through an intermediate model.
|
|
2562
|
+
*
|
|
2563
|
+
* @author Legacy (3m1n3nc3)
|
|
2564
|
+
* @since 0.1.0
|
|
2565
|
+
*/
|
|
2566
|
+
var HasOneThroughRelation = class extends Relation {
|
|
2567
|
+
constructor(parent, related, throughDelegate, firstKey, secondKey, localKey, secondLocalKey) {
|
|
2568
|
+
super();
|
|
2569
|
+
this.parent = parent;
|
|
2570
|
+
this.related = related;
|
|
2571
|
+
this.throughDelegate = throughDelegate;
|
|
2572
|
+
this.firstKey = firstKey;
|
|
2573
|
+
this.secondKey = secondKey;
|
|
2574
|
+
this.localKey = localKey;
|
|
2575
|
+
this.secondLocalKey = secondLocalKey;
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Fetches the related models for this relationship.
|
|
2579
|
+
*
|
|
2580
|
+
* @returns
|
|
2581
|
+
*/
|
|
2582
|
+
async getResults() {
|
|
2583
|
+
const localValue = this.parent.getAttribute(this.localKey);
|
|
2584
|
+
const intermediate = await this.related.getDelegate(this.throughDelegate).findFirst({ where: { [this.firstKey]: localValue } });
|
|
2585
|
+
if (!intermediate) return null;
|
|
2586
|
+
return this.applyConstraint(this.related.query().where({ [this.secondKey]: intermediate[this.secondLocalKey] })).first();
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
|
|
2590
|
+
//#endregion
|
|
2591
|
+
//#region src/relationship/MorphManyRelation.ts
|
|
2592
|
+
/**
|
|
2593
|
+
* Defines a polymorphic one-to-many relationship.
|
|
2594
|
+
*
|
|
2595
|
+
* @author Legacy (3m1n3nc3)
|
|
2596
|
+
* @since 0.1.0
|
|
2597
|
+
*/
|
|
2598
|
+
var MorphManyRelation = class extends Relation {
|
|
2599
|
+
constructor(parent, related, morphName, localKey) {
|
|
2600
|
+
super();
|
|
2601
|
+
this.parent = parent;
|
|
2602
|
+
this.related = related;
|
|
2603
|
+
this.morphName = morphName;
|
|
2604
|
+
this.localKey = localKey;
|
|
2605
|
+
}
|
|
2606
|
+
/**
|
|
2607
|
+
* Fetches the related models for this relationship.
|
|
2608
|
+
*
|
|
2609
|
+
* @returns
|
|
2610
|
+
*/
|
|
2611
|
+
async getResults() {
|
|
2612
|
+
const id = this.parent.getAttribute(this.localKey);
|
|
2613
|
+
const type = this.parent.constructor.name;
|
|
2614
|
+
return this.applyConstraint(this.related.query().where({
|
|
2615
|
+
[`${this.morphName}Id`]: id,
|
|
2616
|
+
[`${this.morphName}Type`]: type
|
|
2617
|
+
})).get();
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
|
|
2621
|
+
//#endregion
|
|
2622
|
+
//#region src/relationship/MorphOneRelation.ts
|
|
2623
|
+
/**
|
|
2624
|
+
* Defines a polymorphic one-to-one relationship.
|
|
2625
|
+
*
|
|
2626
|
+
* @author Legacy (3m1n3nc3)
|
|
2627
|
+
* @since 0.1.0
|
|
2628
|
+
*/
|
|
2629
|
+
var MorphOneRelation = class extends Relation {
|
|
2630
|
+
constructor(parent, related, morphName, localKey) {
|
|
2631
|
+
super();
|
|
2632
|
+
this.parent = parent;
|
|
2633
|
+
this.related = related;
|
|
2634
|
+
this.morphName = morphName;
|
|
2635
|
+
this.localKey = localKey;
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Fetches the related models for this relationship.
|
|
2639
|
+
*
|
|
2640
|
+
* @returns
|
|
2641
|
+
*/
|
|
2642
|
+
async getResults() {
|
|
2643
|
+
const id = this.parent.getAttribute(this.localKey);
|
|
2644
|
+
const type = this.parent.constructor.name;
|
|
2645
|
+
return this.applyConstraint(this.related.query().where({
|
|
2646
|
+
[`${this.morphName}Id`]: id,
|
|
2647
|
+
[`${this.morphName}Type`]: type
|
|
2648
|
+
})).first();
|
|
2649
|
+
}
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2652
|
+
//#endregion
|
|
2653
|
+
//#region src/relationship/MorphToManyRelation.ts
|
|
2654
|
+
/**
|
|
2655
|
+
* Defines a polymorphic many-to-many relationship.
|
|
2656
|
+
*
|
|
2657
|
+
* @author Legacy (3m1n3nc3)
|
|
2658
|
+
* @since 0.1.0
|
|
2659
|
+
*/
|
|
2660
|
+
var MorphToManyRelation = class extends Relation {
|
|
2661
|
+
constructor(parent, related, throughDelegate, morphName, relatedPivotKey, parentKey, relatedKey) {
|
|
2662
|
+
super();
|
|
2663
|
+
this.parent = parent;
|
|
2664
|
+
this.related = related;
|
|
2665
|
+
this.throughDelegate = throughDelegate;
|
|
2666
|
+
this.morphName = morphName;
|
|
2667
|
+
this.relatedPivotKey = relatedPivotKey;
|
|
2668
|
+
this.parentKey = parentKey;
|
|
2669
|
+
this.relatedKey = relatedKey;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Fetches the related models for this relationship.
|
|
2673
|
+
*
|
|
2674
|
+
* @returns
|
|
2675
|
+
*/
|
|
2676
|
+
async getResults() {
|
|
2677
|
+
const parentValue = this.parent.getAttribute(this.parentKey);
|
|
2678
|
+
const morphType = this.parent.constructor.name;
|
|
2679
|
+
const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: {
|
|
2680
|
+
[`${this.morphName}Id`]: parentValue,
|
|
2681
|
+
[`${this.morphName}Type`]: morphType
|
|
2682
|
+
} })).map((row) => row[this.relatedPivotKey]);
|
|
2683
|
+
return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
|
|
2687
|
+
//#endregion
|
|
2688
|
+
//#region src/URLDriver.ts
|
|
2689
|
+
/**
|
|
2690
|
+
* URLDriver builds pagination URLs from paginator options.
|
|
2691
|
+
*
|
|
2692
|
+
* @author Legacy (3m1n3nc3)
|
|
2693
|
+
* @since 0.1.0
|
|
2694
|
+
*/
|
|
2695
|
+
var URLDriver = class URLDriver {
|
|
2696
|
+
static DEFAULT_PAGE_NAME = "page";
|
|
2697
|
+
path;
|
|
2698
|
+
query;
|
|
2699
|
+
fragment;
|
|
2700
|
+
pageName;
|
|
2701
|
+
constructor(options = {}) {
|
|
2702
|
+
this.path = options.path ?? "/";
|
|
2703
|
+
this.query = options.query ?? {};
|
|
2704
|
+
this.fragment = options.fragment ?? "";
|
|
2705
|
+
this.pageName = options.pageName ?? URLDriver.DEFAULT_PAGE_NAME;
|
|
2706
|
+
}
|
|
2707
|
+
getPageName() {
|
|
2708
|
+
return this.pageName;
|
|
2709
|
+
}
|
|
2710
|
+
url(page) {
|
|
2711
|
+
const targetPage = Math.max(1, page);
|
|
2712
|
+
const [basePath, pathQuery = ""] = this.path.split("?");
|
|
2713
|
+
const search = new URLSearchParams(pathQuery);
|
|
2714
|
+
Object.entries(this.query).forEach(([key, value]) => {
|
|
2715
|
+
if (value == null) {
|
|
2716
|
+
search.delete(key);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
search.set(key, String(value));
|
|
2720
|
+
});
|
|
2721
|
+
search.set(this.pageName, String(targetPage));
|
|
2722
|
+
const queryString = search.toString();
|
|
2723
|
+
const normalizedFragment = this.fragment.replace(/^#/, "");
|
|
2724
|
+
if (!queryString && !normalizedFragment) return basePath;
|
|
2725
|
+
if (!normalizedFragment) return `${basePath}?${queryString}`;
|
|
2726
|
+
if (!queryString) return `${basePath}#${normalizedFragment}`;
|
|
2727
|
+
return `${basePath}?${queryString}#${normalizedFragment}`;
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
//#endregion
|
|
2732
|
+
//#region src/Paginator.ts
|
|
2733
|
+
/**
|
|
2734
|
+
* The LengthAwarePaginator class encapsulates paginated results with full
|
|
2735
|
+
* metadata including the total result count and last page.
|
|
2736
|
+
*
|
|
2737
|
+
* @template T The type of the data being paginated.
|
|
2738
|
+
* @author Legacy (3m1n3nc3)
|
|
2739
|
+
* @since 0.1.0
|
|
2740
|
+
*/
|
|
2741
|
+
var LengthAwarePaginator = class {
|
|
2742
|
+
data;
|
|
2743
|
+
meta;
|
|
2744
|
+
urlDriver;
|
|
2745
|
+
/**
|
|
2746
|
+
* Creates a new LengthAwarePaginator instance.
|
|
2747
|
+
*
|
|
2748
|
+
* @param data The collection of data being paginated.
|
|
2749
|
+
* @param total The total number of items.
|
|
2750
|
+
* @param perPage The number of items per page.
|
|
2751
|
+
* @param currentPage The current page number.
|
|
2752
|
+
* @param options URL generation options.
|
|
2753
|
+
*/
|
|
2754
|
+
constructor(data, total, perPage, currentPage, options = {}) {
|
|
2755
|
+
const lastPage = Math.max(1, Math.ceil(total / perPage));
|
|
2756
|
+
const from = total === 0 ? null : (currentPage - 1) * perPage + 1;
|
|
2757
|
+
const to = total === 0 ? null : Math.min(currentPage * perPage, total);
|
|
2758
|
+
this.data = data;
|
|
2759
|
+
const urlDriverFactory = getRuntimePaginationURLDriverFactory();
|
|
2760
|
+
this.urlDriver = urlDriverFactory ? urlDriverFactory(options) : new URLDriver(options);
|
|
2761
|
+
this.meta = {
|
|
2762
|
+
total,
|
|
2763
|
+
perPage,
|
|
2764
|
+
currentPage,
|
|
2765
|
+
lastPage,
|
|
2766
|
+
from,
|
|
2767
|
+
to
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
getPageName() {
|
|
2771
|
+
return this.urlDriver.getPageName();
|
|
2772
|
+
}
|
|
2773
|
+
url(page) {
|
|
2774
|
+
return this.urlDriver.url(page);
|
|
2775
|
+
}
|
|
2776
|
+
nextPageUrl() {
|
|
2777
|
+
if (this.meta.currentPage >= this.meta.lastPage) return null;
|
|
2778
|
+
return this.url(this.meta.currentPage + 1);
|
|
2779
|
+
}
|
|
2780
|
+
previousPageUrl() {
|
|
2781
|
+
if (this.meta.currentPage <= 1) return null;
|
|
2782
|
+
return this.url(this.meta.currentPage - 1);
|
|
2783
|
+
}
|
|
2784
|
+
firstPageUrl() {
|
|
2785
|
+
return this.url(1);
|
|
2786
|
+
}
|
|
2787
|
+
lastPageUrl() {
|
|
2788
|
+
return this.url(this.meta.lastPage);
|
|
2789
|
+
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Converts the paginator instance to a JSON-serializable object.
|
|
2792
|
+
*
|
|
2793
|
+
* @returns
|
|
2794
|
+
*/
|
|
2795
|
+
toJSON() {
|
|
2796
|
+
return {
|
|
2797
|
+
data: this.data,
|
|
2798
|
+
meta: this.meta,
|
|
2799
|
+
links: {
|
|
2800
|
+
first: this.firstPageUrl(),
|
|
2801
|
+
last: this.lastPageUrl(),
|
|
2802
|
+
prev: this.previousPageUrl(),
|
|
2803
|
+
next: this.nextPageUrl()
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
/**
|
|
2809
|
+
* The Paginator class encapsulates simple pagination results without total count.
|
|
2810
|
+
*
|
|
2811
|
+
* @template T The type of the data being paginated.
|
|
2812
|
+
*/
|
|
2813
|
+
var Paginator = class {
|
|
2814
|
+
data;
|
|
2815
|
+
meta;
|
|
2816
|
+
urlDriver;
|
|
2817
|
+
/**
|
|
2818
|
+
* Creates a new simple Paginator instance.
|
|
2819
|
+
*
|
|
2820
|
+
* @param data The collection of data being paginated.
|
|
2821
|
+
* @param perPage The number of items per page.
|
|
2822
|
+
* @param currentPage The current page number.
|
|
2823
|
+
* @param hasMorePages Indicates whether additional pages exist.
|
|
2824
|
+
* @param options URL generation options.
|
|
2825
|
+
*/
|
|
2826
|
+
constructor(data, perPage, currentPage, hasMorePages, options = {}) {
|
|
2827
|
+
const count = data.all().length;
|
|
2828
|
+
const from = count === 0 ? null : (currentPage - 1) * perPage + 1;
|
|
2829
|
+
const to = count === 0 ? null : (from ?? 1) + count - 1;
|
|
2830
|
+
this.data = data;
|
|
2831
|
+
const urlDriverFactory = getRuntimePaginationURLDriverFactory();
|
|
2832
|
+
this.urlDriver = urlDriverFactory ? urlDriverFactory(options) : new URLDriver(options);
|
|
2833
|
+
this.meta = {
|
|
2834
|
+
perPage,
|
|
2835
|
+
currentPage,
|
|
2836
|
+
from,
|
|
2837
|
+
to,
|
|
2838
|
+
hasMorePages
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
getPageName() {
|
|
2842
|
+
return this.urlDriver.getPageName();
|
|
2843
|
+
}
|
|
2844
|
+
url(page) {
|
|
2845
|
+
return this.urlDriver.url(page);
|
|
2846
|
+
}
|
|
2847
|
+
nextPageUrl() {
|
|
2848
|
+
if (!this.meta.hasMorePages) return null;
|
|
2849
|
+
return this.url(this.meta.currentPage + 1);
|
|
2850
|
+
}
|
|
2851
|
+
previousPageUrl() {
|
|
2852
|
+
if (this.meta.currentPage <= 1) return null;
|
|
2853
|
+
return this.url(this.meta.currentPage - 1);
|
|
2854
|
+
}
|
|
2855
|
+
toJSON() {
|
|
2856
|
+
return {
|
|
2857
|
+
data: this.data,
|
|
2858
|
+
meta: this.meta,
|
|
2859
|
+
links: {
|
|
2860
|
+
prev: this.previousPageUrl(),
|
|
2861
|
+
next: this.nextPageUrl()
|
|
2862
|
+
}
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
//#endregion
|
|
2868
|
+
//#region src/QueryBuilder.ts
|
|
2869
|
+
/**
|
|
2870
|
+
* The QueryBuilder class provides a fluent interface for building and
|
|
2871
|
+
* executing database queries.
|
|
2872
|
+
*
|
|
2873
|
+
* @template TModel The type of the model being queried.
|
|
2874
|
+
* @author Legacy (3m1n3nc3)
|
|
2875
|
+
* @since 0.1.0
|
|
2876
|
+
*/
|
|
2877
|
+
var QueryBuilder = class QueryBuilder {
|
|
2878
|
+
args = {};
|
|
2879
|
+
eagerLoads = {};
|
|
2880
|
+
includeTrashed = false;
|
|
2881
|
+
onlyTrashedRecords = false;
|
|
2882
|
+
randomOrderEnabled = false;
|
|
2883
|
+
relationFilters = [];
|
|
2884
|
+
relationAggregates = [];
|
|
2885
|
+
/**
|
|
2886
|
+
* Creates a new QueryBuilder instance.
|
|
2887
|
+
*
|
|
2888
|
+
* @param delegate
|
|
2889
|
+
* @param model
|
|
2890
|
+
*/
|
|
2891
|
+
constructor(delegate, model) {
|
|
2892
|
+
this.delegate = delegate;
|
|
2893
|
+
this.model = model;
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2896
|
+
* Adds a where clause to the query. Multiple calls to where will combine
|
|
2897
|
+
* the clauses with AND logic.
|
|
2898
|
+
*
|
|
2899
|
+
* @param where
|
|
2900
|
+
* @returns
|
|
2901
|
+
*/
|
|
2902
|
+
where(where) {
|
|
2903
|
+
return this.addLogicalWhere("AND", where);
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Adds an OR where clause to the query.
|
|
2907
|
+
*
|
|
2908
|
+
* @param where
|
|
2909
|
+
* @returns
|
|
2910
|
+
*/
|
|
2911
|
+
orWhere(where) {
|
|
2912
|
+
return this.addLogicalWhere("OR", where);
|
|
2913
|
+
}
|
|
2914
|
+
/**
|
|
2915
|
+
* Adds a NOT where clause to the query.
|
|
2916
|
+
*
|
|
2917
|
+
* @param where
|
|
2918
|
+
* @returns
|
|
2919
|
+
*/
|
|
2920
|
+
whereNot(where) {
|
|
2921
|
+
return this.where({ NOT: where });
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Adds an OR NOT where clause to the query.
|
|
2925
|
+
*
|
|
2926
|
+
* @param where
|
|
2927
|
+
* @returns
|
|
2928
|
+
*/
|
|
2929
|
+
orWhereNot(where) {
|
|
2930
|
+
return this.orWhere({ NOT: where });
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Adds a null check for a key.
|
|
2934
|
+
*
|
|
2935
|
+
* @param key
|
|
2936
|
+
* @returns
|
|
2937
|
+
*/
|
|
2938
|
+
whereNull(key) {
|
|
2939
|
+
return this.where({ [key]: null });
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Adds a not-null check for a key.
|
|
2943
|
+
*
|
|
2944
|
+
* @param key
|
|
2945
|
+
* @returns
|
|
2946
|
+
*/
|
|
2947
|
+
whereNotNull(key) {
|
|
2948
|
+
return this.where({ [key]: { not: null } });
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Adds a between range clause for a key.
|
|
2952
|
+
*
|
|
2953
|
+
* @param key
|
|
2954
|
+
* @param range
|
|
2955
|
+
* @returns
|
|
2956
|
+
*/
|
|
2957
|
+
whereBetween(key, range) {
|
|
2958
|
+
const [min, max] = range;
|
|
2959
|
+
return this.where({ [key]: {
|
|
2960
|
+
gte: min,
|
|
2961
|
+
lte: max
|
|
2962
|
+
} });
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Adds a date-only equality clause for a date-like key.
|
|
2966
|
+
*
|
|
2967
|
+
* @param key
|
|
2968
|
+
* @param value
|
|
2969
|
+
* @returns
|
|
2970
|
+
*/
|
|
2971
|
+
whereDate(key, value) {
|
|
2972
|
+
const target = this.coerceDate(value);
|
|
2973
|
+
const start = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth(), target.getUTCDate()));
|
|
2974
|
+
const end = new Date(start);
|
|
2975
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
2976
|
+
return this.where({ [key]: {
|
|
2977
|
+
gte: start,
|
|
2978
|
+
lt: end
|
|
2979
|
+
} });
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Adds a month clause for a date-like key.
|
|
2983
|
+
*
|
|
2984
|
+
* @param key
|
|
2985
|
+
* @param month
|
|
2986
|
+
* @param year
|
|
2987
|
+
* @returns
|
|
2988
|
+
*/
|
|
2989
|
+
whereMonth(key, month, year = (/* @__PURE__ */ new Date()).getUTCFullYear()) {
|
|
2990
|
+
const normalizedMonth = Math.min(12, Math.max(1, month));
|
|
2991
|
+
const start = new Date(Date.UTC(year, normalizedMonth - 1, 1));
|
|
2992
|
+
const end = new Date(Date.UTC(year, normalizedMonth, 1));
|
|
2993
|
+
return this.where({ [key]: {
|
|
2994
|
+
gte: start,
|
|
2995
|
+
lt: end
|
|
2996
|
+
} });
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Adds a year clause for a date-like key.
|
|
3000
|
+
*
|
|
3001
|
+
* @param key
|
|
3002
|
+
* @param year
|
|
3003
|
+
* @returns
|
|
3004
|
+
*/
|
|
3005
|
+
whereYear(key, year) {
|
|
3006
|
+
const start = new Date(Date.UTC(year, 0, 1));
|
|
3007
|
+
const end = new Date(Date.UTC(year + 1, 0, 1));
|
|
3008
|
+
return this.where({ [key]: {
|
|
3009
|
+
gte: start,
|
|
3010
|
+
lt: end
|
|
3011
|
+
} });
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Adds a strongly-typed inequality where clause for a single attribute key.
|
|
3015
|
+
*
|
|
3016
|
+
* @param key
|
|
3017
|
+
* @param value
|
|
3018
|
+
* @returns
|
|
3019
|
+
*/
|
|
3020
|
+
whereKeyNot(key, value) {
|
|
3021
|
+
return this.where({ [key]: { not: value } });
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Adds a strongly-typed OR IN where clause for a single attribute key.
|
|
3025
|
+
*
|
|
3026
|
+
* @param key
|
|
3027
|
+
* @param values
|
|
3028
|
+
* @returns
|
|
3029
|
+
*/
|
|
3030
|
+
orWhereIn(key, values) {
|
|
3031
|
+
return this.orWhere({ [key]: { in: values } });
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Adds a strongly-typed NOT IN where clause for a single attribute key.
|
|
3035
|
+
*
|
|
3036
|
+
* @param key
|
|
3037
|
+
* @param values
|
|
3038
|
+
* @returns
|
|
3039
|
+
*/
|
|
3040
|
+
whereNotIn(key, values) {
|
|
3041
|
+
return this.where({ [key]: { notIn: values } });
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Adds a strongly-typed OR NOT IN where clause for a single attribute key.
|
|
3045
|
+
*
|
|
3046
|
+
* @param key
|
|
3047
|
+
* @param values
|
|
3048
|
+
* @returns
|
|
3049
|
+
*/
|
|
3050
|
+
orWhereNotIn(key, values) {
|
|
3051
|
+
return this.orWhere({ [key]: { notIn: values } });
|
|
3052
|
+
}
|
|
3053
|
+
async firstWhere(key, operatorOrValue, maybeValue) {
|
|
3054
|
+
const hasOperator = maybeValue !== void 0;
|
|
3055
|
+
const operator = hasOperator ? operatorOrValue : "=";
|
|
3056
|
+
const value = hasOperator ? maybeValue : operatorOrValue;
|
|
3057
|
+
return this.clone().where(this.buildComparisonWhere(key, operator, value)).first();
|
|
3058
|
+
}
|
|
3059
|
+
addLogicalWhere(operator, where) {
|
|
3060
|
+
if (!this.args.where) {
|
|
3061
|
+
this.args.where = where;
|
|
3062
|
+
return this;
|
|
3063
|
+
}
|
|
3064
|
+
this.args.where = { [operator]: [this.args.where, where] };
|
|
3065
|
+
return this;
|
|
3066
|
+
}
|
|
3067
|
+
buildComparisonWhere(key, operator, value) {
|
|
3068
|
+
if (operator === "=") return { [key]: value };
|
|
3069
|
+
if (operator === "!=") return { [key]: { not: value } };
|
|
3070
|
+
if (operator === ">") return { [key]: { gt: value } };
|
|
3071
|
+
if (operator === ">=") return { [key]: { gte: value } };
|
|
3072
|
+
if (operator === "<") return { [key]: { lt: value } };
|
|
3073
|
+
return { [key]: { lte: value } };
|
|
3074
|
+
}
|
|
3075
|
+
coerceDate(value) {
|
|
3076
|
+
const parsed = value instanceof Date ? new Date(value.getTime()) : new Date(value);
|
|
3077
|
+
if (Number.isNaN(parsed.getTime())) throw new ArkormException("Invalid date value for date-based query helper.");
|
|
3078
|
+
return parsed;
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Adds a strongly-typed equality where clause for a single attribute key.
|
|
3082
|
+
*
|
|
3083
|
+
* @param key
|
|
3084
|
+
* @param value
|
|
3085
|
+
* @returns
|
|
3086
|
+
*/
|
|
3087
|
+
whereKey(key, value) {
|
|
3088
|
+
return this.where({ [key]: value });
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* Adds a strongly-typed IN where clause for a single attribute key.
|
|
3092
|
+
*
|
|
3093
|
+
* @param key
|
|
3094
|
+
* @param values
|
|
3095
|
+
* @returns
|
|
3096
|
+
*/
|
|
3097
|
+
whereIn(key, values) {
|
|
3098
|
+
return this.where({ [key]: { in: values } });
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Adds an orderBy clause to the query. This will overwrite any existing orderBy clause.
|
|
3102
|
+
*
|
|
3103
|
+
* @param orderBy
|
|
3104
|
+
* @returns
|
|
3105
|
+
*/
|
|
3106
|
+
orderBy(orderBy) {
|
|
3107
|
+
this.randomOrderEnabled = false;
|
|
3108
|
+
this.args.orderBy = orderBy;
|
|
3109
|
+
return this;
|
|
3110
|
+
}
|
|
3111
|
+
/**
|
|
3112
|
+
* Puts the query results in random order.
|
|
3113
|
+
*
|
|
3114
|
+
* @returns
|
|
3115
|
+
*/
|
|
3116
|
+
inRandomOrder() {
|
|
3117
|
+
this.randomOrderEnabled = true;
|
|
3118
|
+
return this;
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Removes existing order clauses and optionally applies a new one.
|
|
3122
|
+
*
|
|
3123
|
+
* @param column
|
|
3124
|
+
* @param direction
|
|
3125
|
+
* @returns
|
|
3126
|
+
*/
|
|
3127
|
+
reorder(column, direction = "asc") {
|
|
3128
|
+
this.args.orderBy = void 0;
|
|
3129
|
+
this.randomOrderEnabled = false;
|
|
3130
|
+
if (!column) return this;
|
|
3131
|
+
return this.orderBy({ [column]: direction });
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Adds an orderBy descending clause for a timestamp-like column.
|
|
3135
|
+
*
|
|
3136
|
+
* @param column
|
|
3137
|
+
* @returns
|
|
3138
|
+
*/
|
|
3139
|
+
latest(column = "createdAt") {
|
|
3140
|
+
return this.orderBy({ [column]: "desc" });
|
|
3141
|
+
}
|
|
3142
|
+
/**
|
|
3143
|
+
* Adds an orderBy ascending clause for a timestamp-like column.
|
|
3144
|
+
*
|
|
3145
|
+
* @param column
|
|
3146
|
+
* @returns
|
|
3147
|
+
*/
|
|
3148
|
+
oldest(column = "createdAt") {
|
|
3149
|
+
return this.orderBy({ [column]: "asc" });
|
|
3150
|
+
}
|
|
3151
|
+
/**
|
|
3152
|
+
* Adds an include clause to the query. This will overwrite any existing include clause.
|
|
3153
|
+
*
|
|
3154
|
+
* @param include
|
|
3155
|
+
* @returns
|
|
3156
|
+
*/
|
|
3157
|
+
include(include) {
|
|
3158
|
+
this.args.include = include;
|
|
3159
|
+
return this;
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Adds eager loading for the specified relations.
|
|
3163
|
+
* This will merge with any existing include clause.
|
|
3164
|
+
*
|
|
3165
|
+
* @param relations
|
|
3166
|
+
* @returns
|
|
3167
|
+
*/
|
|
3168
|
+
with(relations) {
|
|
3169
|
+
const relationMap = this.normalizeWith(relations);
|
|
3170
|
+
const names = Object.keys(relationMap);
|
|
3171
|
+
this.args.include = {
|
|
3172
|
+
...this.args.include || {},
|
|
3173
|
+
...names.reduce((accumulator, name) => {
|
|
3174
|
+
accumulator[name] = true;
|
|
3175
|
+
return accumulator;
|
|
3176
|
+
}, {})
|
|
3177
|
+
};
|
|
3178
|
+
Object.entries(relationMap).forEach(([name, constraint]) => {
|
|
3179
|
+
this.eagerLoads[name] = constraint;
|
|
3180
|
+
});
|
|
3181
|
+
return this;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Add a relationship count/existence constraint.
|
|
3185
|
+
*
|
|
3186
|
+
* @param relation
|
|
3187
|
+
* @param operator
|
|
3188
|
+
* @param count
|
|
3189
|
+
* @param callback
|
|
3190
|
+
* @returns
|
|
3191
|
+
*/
|
|
3192
|
+
has(relation, operator = ">=", count = 1, callback) {
|
|
3193
|
+
this.relationFilters.push({
|
|
3194
|
+
relation,
|
|
3195
|
+
callback,
|
|
3196
|
+
operator,
|
|
3197
|
+
count,
|
|
3198
|
+
boolean: "AND"
|
|
3199
|
+
});
|
|
3200
|
+
return this;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Add an OR relationship count/existence constraint.
|
|
3204
|
+
*
|
|
3205
|
+
* @param relation
|
|
3206
|
+
* @param operator
|
|
3207
|
+
* @param count
|
|
3208
|
+
* @returns
|
|
3209
|
+
*/
|
|
3210
|
+
orHas(relation, operator = ">=", count = 1) {
|
|
3211
|
+
this.relationFilters.push({
|
|
3212
|
+
relation,
|
|
3213
|
+
operator,
|
|
3214
|
+
count,
|
|
3215
|
+
boolean: "OR"
|
|
3216
|
+
});
|
|
3217
|
+
return this;
|
|
3218
|
+
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Add a relationship does-not-have constraint.
|
|
3221
|
+
*
|
|
3222
|
+
* @param relation
|
|
3223
|
+
* @param callback
|
|
3224
|
+
* @returns
|
|
3225
|
+
*/
|
|
3226
|
+
doesntHave(relation, callback) {
|
|
3227
|
+
return this.has(relation, "<", 1, callback);
|
|
3228
|
+
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Add an OR relationship does-not-have constraint.
|
|
3231
|
+
*
|
|
3232
|
+
* @param relation
|
|
3233
|
+
* @returns
|
|
3234
|
+
*/
|
|
3235
|
+
orDoesntHave(relation) {
|
|
3236
|
+
return this.orHas(relation, "<", 1);
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Add a constrained relationship has clause.
|
|
3240
|
+
*
|
|
3241
|
+
* @param relation
|
|
3242
|
+
* @param callback
|
|
3243
|
+
* @param operator
|
|
3244
|
+
* @param count
|
|
3245
|
+
* @returns
|
|
3246
|
+
*/
|
|
3247
|
+
whereHas(relation, callback, operator = ">=", count = 1) {
|
|
3248
|
+
return this.has(relation, operator, count, callback);
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Add an OR constrained relationship has clause.
|
|
3252
|
+
*
|
|
3253
|
+
* @param relation
|
|
3254
|
+
* @param callback
|
|
3255
|
+
* @param operator
|
|
3256
|
+
* @param count
|
|
3257
|
+
* @returns
|
|
3258
|
+
*/
|
|
3259
|
+
orWhereHas(relation, callback, operator = ">=", count = 1) {
|
|
3260
|
+
this.relationFilters.push({
|
|
3261
|
+
relation,
|
|
3262
|
+
callback,
|
|
3263
|
+
operator,
|
|
3264
|
+
count,
|
|
3265
|
+
boolean: "OR"
|
|
3266
|
+
});
|
|
3267
|
+
return this;
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Add a constrained relationship does-not-have clause.
|
|
3271
|
+
*
|
|
3272
|
+
* @param relation
|
|
3273
|
+
* @param callback
|
|
3274
|
+
* @returns
|
|
3275
|
+
*/
|
|
3276
|
+
whereDoesntHave(relation, callback) {
|
|
3277
|
+
return this.whereHas(relation, callback, "<", 1);
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Add an OR constrained relationship does-not-have clause.
|
|
3281
|
+
*
|
|
3282
|
+
* @param relation
|
|
3283
|
+
* @param callback
|
|
3284
|
+
* @returns
|
|
3285
|
+
*/
|
|
3286
|
+
orWhereDoesntHave(relation, callback) {
|
|
3287
|
+
return this.orWhereHas(relation, callback, "<", 1);
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Add relationship count aggregate attributes.
|
|
3291
|
+
*
|
|
3292
|
+
* @param relations
|
|
3293
|
+
* @returns
|
|
3294
|
+
*/
|
|
3295
|
+
withCount(relations) {
|
|
3296
|
+
(Array.isArray(relations) ? relations : [relations]).forEach((relation) => {
|
|
3297
|
+
this.relationAggregates.push({
|
|
3298
|
+
type: "count",
|
|
3299
|
+
relation
|
|
3300
|
+
});
|
|
3301
|
+
});
|
|
3302
|
+
return this;
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Add relationship existence aggregate attributes.
|
|
3306
|
+
*
|
|
3307
|
+
* @param relations
|
|
3308
|
+
* @returns
|
|
3309
|
+
*/
|
|
3310
|
+
withExists(relations) {
|
|
3311
|
+
(Array.isArray(relations) ? relations : [relations]).forEach((relation) => {
|
|
3312
|
+
this.relationAggregates.push({
|
|
3313
|
+
type: "exists",
|
|
3314
|
+
relation
|
|
3315
|
+
});
|
|
3316
|
+
});
|
|
3317
|
+
return this;
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Add relationship sum aggregate attribute.
|
|
3321
|
+
*
|
|
3322
|
+
* @param relation
|
|
3323
|
+
* @param column
|
|
3324
|
+
* @returns
|
|
3325
|
+
*/
|
|
3326
|
+
withSum(relation, column) {
|
|
3327
|
+
this.relationAggregates.push({
|
|
3328
|
+
type: "sum",
|
|
3329
|
+
relation,
|
|
3330
|
+
column
|
|
3331
|
+
});
|
|
3332
|
+
return this;
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* Add relationship average aggregate attribute.
|
|
3336
|
+
*
|
|
3337
|
+
* @param relation
|
|
3338
|
+
* @param column
|
|
3339
|
+
* @returns
|
|
3340
|
+
*/
|
|
3341
|
+
withAvg(relation, column) {
|
|
3342
|
+
this.relationAggregates.push({
|
|
3343
|
+
type: "avg",
|
|
3344
|
+
relation,
|
|
3345
|
+
column
|
|
3346
|
+
});
|
|
3347
|
+
return this;
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Add relationship minimum aggregate attribute.
|
|
3351
|
+
*
|
|
3352
|
+
* @param relation
|
|
3353
|
+
* @param column
|
|
3354
|
+
* @returns
|
|
3355
|
+
*/
|
|
3356
|
+
withMin(relation, column) {
|
|
3357
|
+
this.relationAggregates.push({
|
|
3358
|
+
type: "min",
|
|
3359
|
+
relation,
|
|
3360
|
+
column
|
|
3361
|
+
});
|
|
3362
|
+
return this;
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Add relationship maximum aggregate attribute.
|
|
3366
|
+
*
|
|
3367
|
+
* @param relation
|
|
3368
|
+
* @param column
|
|
3369
|
+
* @returns
|
|
3370
|
+
*/
|
|
3371
|
+
withMax(relation, column) {
|
|
3372
|
+
this.relationAggregates.push({
|
|
3373
|
+
type: "max",
|
|
3374
|
+
relation,
|
|
3375
|
+
column
|
|
3376
|
+
});
|
|
3377
|
+
return this;
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Includes soft-deleted records in the query results.
|
|
3381
|
+
* This method is only applicable if the model has soft delete enabled.
|
|
3382
|
+
*
|
|
3383
|
+
* @returns
|
|
3384
|
+
*/
|
|
3385
|
+
withTrashed() {
|
|
3386
|
+
this.includeTrashed = true;
|
|
3387
|
+
this.onlyTrashedRecords = false;
|
|
3388
|
+
return this;
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* Limits the query results to only soft-deleted records.
|
|
3392
|
+
* This method is only applicable if the model has soft delete enabled.
|
|
3393
|
+
*
|
|
3394
|
+
* @returns
|
|
3395
|
+
*/
|
|
3396
|
+
onlyTrashed() {
|
|
3397
|
+
this.onlyTrashedRecords = true;
|
|
3398
|
+
this.includeTrashed = false;
|
|
3399
|
+
return this;
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* Excludes soft-deleted records from the query results.
|
|
3403
|
+
* This is the default behavior, but this method can be used to explicitly
|
|
3404
|
+
* enforce it after using withTrashed or onlyTrashed.
|
|
3405
|
+
*
|
|
3406
|
+
* @returns
|
|
3407
|
+
*/
|
|
3408
|
+
withoutTrashed() {
|
|
3409
|
+
this.includeTrashed = false;
|
|
3410
|
+
this.onlyTrashedRecords = false;
|
|
3411
|
+
return this;
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Applies a named scope to the query. A scope is a reusable query constraint
|
|
3415
|
+
* defined as a static method on the model. The scope method will look for a
|
|
3416
|
+
* method with the name `scope{Name}` on the model's prototype.
|
|
3417
|
+
* If found, it will call that method with the current query builder
|
|
3418
|
+
* instance and any additional arguments provided.
|
|
3419
|
+
*
|
|
3420
|
+
* @param name
|
|
3421
|
+
* @param args
|
|
3422
|
+
* @returns
|
|
3423
|
+
*/
|
|
3424
|
+
scope(name, ...args) {
|
|
3425
|
+
const methodName = `scope${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
3426
|
+
const scope = this.model.prototype?.[methodName];
|
|
3427
|
+
if (typeof scope !== "function") throw new ArkormException(`Scope [${name}] is not defined.`);
|
|
3428
|
+
const scoped = scope.call(void 0, this, ...args);
|
|
3429
|
+
if (scoped && scoped !== this) return scoped;
|
|
3430
|
+
return this;
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* Apply the callback when value is truthy.
|
|
3434
|
+
*
|
|
3435
|
+
* @param value
|
|
3436
|
+
* @param callback
|
|
3437
|
+
* @param defaultCallback
|
|
3438
|
+
* @returns
|
|
3439
|
+
*/
|
|
3440
|
+
when(value, callback, defaultCallback) {
|
|
3441
|
+
const resolved = typeof value === "function" ? value() : value;
|
|
3442
|
+
if (resolved) return callback(this, resolved);
|
|
3443
|
+
if (defaultCallback) return defaultCallback(this, resolved);
|
|
3444
|
+
return this;
|
|
3445
|
+
}
|
|
3446
|
+
/**
|
|
3447
|
+
* Apply the callback when value is falsy.
|
|
3448
|
+
*
|
|
3449
|
+
* @param value
|
|
3450
|
+
* @param callback
|
|
3451
|
+
* @param defaultCallback
|
|
3452
|
+
* @returns
|
|
3453
|
+
*/
|
|
3454
|
+
unless(value, callback, defaultCallback) {
|
|
3455
|
+
const resolved = typeof value === "function" ? value() : value;
|
|
3456
|
+
if (!resolved) return callback(this, resolved);
|
|
3457
|
+
if (defaultCallback) return defaultCallback(this, resolved);
|
|
3458
|
+
return this;
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* Pass the query builder into a callback and return this.
|
|
3462
|
+
*
|
|
3463
|
+
* @param callback
|
|
3464
|
+
* @returns
|
|
3465
|
+
*/
|
|
3466
|
+
tap(callback) {
|
|
3467
|
+
callback(this);
|
|
3468
|
+
return this;
|
|
3469
|
+
}
|
|
3470
|
+
/**
|
|
3471
|
+
* Pass the query builder into a callback and return callback result.
|
|
3472
|
+
*
|
|
3473
|
+
* @param callback
|
|
3474
|
+
* @returns
|
|
3475
|
+
*/
|
|
3476
|
+
pipe(callback) {
|
|
3477
|
+
return callback(this);
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Adds a select clause to the query. This will overwrite any existing select clause.
|
|
3481
|
+
*
|
|
3482
|
+
* @param select
|
|
3483
|
+
* @returns
|
|
3484
|
+
*/
|
|
3485
|
+
select(select) {
|
|
3486
|
+
this.args.select = select;
|
|
3487
|
+
return this;
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Adds a skip clause to the query for pagination.
|
|
3491
|
+
* This will overwrite any existing skip clause.
|
|
3492
|
+
*
|
|
3493
|
+
* @param skip
|
|
3494
|
+
* @returns
|
|
3495
|
+
*/
|
|
3496
|
+
skip(skip) {
|
|
3497
|
+
this.args.skip = skip;
|
|
3498
|
+
return this;
|
|
3499
|
+
}
|
|
3500
|
+
/**
|
|
3501
|
+
* Alias for skip.
|
|
3502
|
+
*
|
|
3503
|
+
* @param value
|
|
3504
|
+
* @returns
|
|
3505
|
+
*/
|
|
3506
|
+
offset(value) {
|
|
3507
|
+
return this.skip(value);
|
|
3508
|
+
}
|
|
3509
|
+
/**
|
|
3510
|
+
* Adds a take clause to the query for pagination.
|
|
3511
|
+
*
|
|
3512
|
+
* @param take
|
|
3513
|
+
* @returns
|
|
3514
|
+
*/
|
|
3515
|
+
take(take) {
|
|
3516
|
+
this.args.take = take;
|
|
3517
|
+
return this;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Alias for take.
|
|
3521
|
+
*
|
|
3522
|
+
* @param value
|
|
3523
|
+
* @returns
|
|
3524
|
+
*/
|
|
3525
|
+
limit(value) {
|
|
3526
|
+
return this.take(value);
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Sets offset/limit for a 1-based page.
|
|
3530
|
+
*
|
|
3531
|
+
* @param page
|
|
3532
|
+
* @param perPage
|
|
3533
|
+
* @returns
|
|
3534
|
+
*/
|
|
3535
|
+
forPage(page, perPage = 15) {
|
|
3536
|
+
const currentPage = Math.max(1, page);
|
|
3537
|
+
const pageSize = Math.max(1, perPage);
|
|
3538
|
+
return this.skip((currentPage - 1) * pageSize).take(pageSize);
|
|
3539
|
+
}
|
|
3540
|
+
/**
|
|
3541
|
+
* Executes the query and returns the results as a collection of model instances.
|
|
3542
|
+
*
|
|
3543
|
+
* @returns
|
|
3544
|
+
*/
|
|
3545
|
+
async get() {
|
|
3546
|
+
const relationCache = /* @__PURE__ */ new WeakMap();
|
|
3547
|
+
const rows = await this.delegate.findMany(this.buildFindArgs());
|
|
3548
|
+
const normalizedRows = this.randomOrderEnabled ? this.shuffleRows(rows) : rows;
|
|
3549
|
+
const models = this.model.hydrateMany(normalizedRows);
|
|
3550
|
+
let filteredModels = models;
|
|
3551
|
+
if (this.hasRelationFilters()) if (this.hasOrRelationFilters() && this.args.where) {
|
|
3552
|
+
const baseIds = new Set(models.map((model) => this.getModelId(model)).filter((id) => id != null));
|
|
3553
|
+
const allRows = await this.delegate.findMany({
|
|
3554
|
+
...this.args,
|
|
3555
|
+
where: this.buildSoftDeleteOnlyWhere()
|
|
3556
|
+
});
|
|
3557
|
+
const allModels = this.model.hydrateMany(allRows);
|
|
3558
|
+
filteredModels = await this.filterModelsByRelationConstraints(allModels, relationCache, baseIds);
|
|
3559
|
+
} else filteredModels = await this.filterModelsByRelationConstraints(models, relationCache);
|
|
3560
|
+
if (this.hasRelationAggregates()) await this.applyRelationAggregates(filteredModels, relationCache);
|
|
3561
|
+
await Promise.all(filteredModels.map(async (model) => {
|
|
3562
|
+
await model.load(this.eagerLoads);
|
|
3563
|
+
}));
|
|
3564
|
+
return new ArkormCollection(filteredModels);
|
|
3565
|
+
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Executes the query and returns the first result as a model
|
|
3568
|
+
* instance, or null if no results are found.
|
|
3569
|
+
*
|
|
3570
|
+
* @returns
|
|
3571
|
+
*/
|
|
3572
|
+
async first() {
|
|
3573
|
+
if (this.hasRelationFilters() || this.hasRelationAggregates()) return (await this.get()).all()[0] ?? null;
|
|
3574
|
+
if (this.randomOrderEnabled) {
|
|
3575
|
+
const rows = await this.delegate.findMany(this.buildFindArgs());
|
|
3576
|
+
if (rows.length === 0) return null;
|
|
3577
|
+
const row = this.shuffleRows(rows)[0];
|
|
3578
|
+
if (!row) return null;
|
|
3579
|
+
const model = this.model.hydrate(row);
|
|
3580
|
+
await model.load(this.eagerLoads);
|
|
3581
|
+
return model;
|
|
3582
|
+
}
|
|
3583
|
+
const row = await this.delegate.findFirst(this.buildFindArgs());
|
|
3584
|
+
if (!row) return null;
|
|
3585
|
+
const model = this.model.hydrate(row);
|
|
3586
|
+
await model.load(this.eagerLoads);
|
|
3587
|
+
return model;
|
|
3588
|
+
}
|
|
3589
|
+
/**
|
|
3590
|
+
* Executes the query and returns the first result as a model instance.
|
|
3591
|
+
*
|
|
3592
|
+
* @returns
|
|
3593
|
+
*/
|
|
3594
|
+
async firstOrFail() {
|
|
3595
|
+
const model = await this.first();
|
|
3596
|
+
if (!model) throw new ModelNotFoundException("Record not found.");
|
|
3597
|
+
return model;
|
|
3598
|
+
}
|
|
3599
|
+
async find(value, key = "id") {
|
|
3600
|
+
return this.where({ [key]: value }).first();
|
|
3601
|
+
}
|
|
3602
|
+
async findOr(value, keyOrCallback, maybeCallback) {
|
|
3603
|
+
const key = typeof keyOrCallback === "string" ? keyOrCallback : "id";
|
|
3604
|
+
const callback = typeof keyOrCallback === "function" ? keyOrCallback : maybeCallback;
|
|
3605
|
+
if (!callback) throw new ArkormException("findOr requires a fallback callback.");
|
|
3606
|
+
const found = await this.find(value, key);
|
|
3607
|
+
if (found) return found;
|
|
3608
|
+
return callback();
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* Returns a single column value from the first record.
|
|
3612
|
+
*
|
|
3613
|
+
* @param column
|
|
3614
|
+
* @returns
|
|
3615
|
+
*/
|
|
3616
|
+
async value(column) {
|
|
3617
|
+
const row = await this.delegate.findFirst(this.buildFindArgs());
|
|
3618
|
+
if (!row) return null;
|
|
3619
|
+
return row[column] ?? null;
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Returns a single column value from the first record or throws.
|
|
3623
|
+
*
|
|
3624
|
+
* @param column
|
|
3625
|
+
* @returns
|
|
3626
|
+
*/
|
|
3627
|
+
async valueOrFail(column) {
|
|
3628
|
+
const result = await this.value(column);
|
|
3629
|
+
if (result == null) throw new ModelNotFoundException("Record not found.");
|
|
3630
|
+
return result;
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Returns a collection with values for the given column.
|
|
3634
|
+
*
|
|
3635
|
+
* @param column
|
|
3636
|
+
* @param key
|
|
3637
|
+
* @returns
|
|
3638
|
+
*/
|
|
3639
|
+
async pluck(column, key) {
|
|
3640
|
+
const rows = await this.delegate.findMany(this.buildFindArgs());
|
|
3641
|
+
if (!key) return new ArkormCollection(rows.map((row) => row[column]));
|
|
3642
|
+
return new ArkormCollection(rows.sort((leftRow, rightRow) => String(leftRow[key]).localeCompare(String(rightRow[key]))).map((row) => row[column]));
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Creates a new record with the specified data and returns it as a model instance.
|
|
3646
|
+
*
|
|
3647
|
+
* @param data
|
|
3648
|
+
* @returns
|
|
3649
|
+
*/
|
|
3650
|
+
async create(data) {
|
|
3651
|
+
const created = await this.delegate.create({ data });
|
|
3652
|
+
return this.model.hydrate(created);
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* Updates records matching the current query constraints with the
|
|
3656
|
+
* specified data and returns the updated record(s) as model instance(s).
|
|
3657
|
+
*
|
|
3658
|
+
* @param data
|
|
3659
|
+
* @returns
|
|
3660
|
+
*/
|
|
3661
|
+
async update(data) {
|
|
3662
|
+
const where = this.buildWhere();
|
|
3663
|
+
if (!where) throw new ArkormException("Update requires a where clause.");
|
|
3664
|
+
const uniqueWhere = await this.resolveUniqueWhere(where);
|
|
3665
|
+
const updated = await this.delegate.update({
|
|
3666
|
+
where: uniqueWhere,
|
|
3667
|
+
data
|
|
3668
|
+
});
|
|
3669
|
+
return this.model.hydrate(updated);
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Deletes records matching the current query constraints and returns
|
|
3673
|
+
* the deleted record(s) as model instance(s).
|
|
3674
|
+
*
|
|
3675
|
+
* @returns
|
|
3676
|
+
*/
|
|
3677
|
+
async delete() {
|
|
3678
|
+
const where = this.buildWhere();
|
|
3679
|
+
if (!where) throw new ArkormException("Delete requires a where clause.");
|
|
3680
|
+
const uniqueWhere = await this.resolveUniqueWhere(where);
|
|
3681
|
+
const deleted = await this.delegate.delete({ where: uniqueWhere });
|
|
3682
|
+
return this.model.hydrate(deleted);
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Counts the number of records matching the current query constraints.
|
|
3686
|
+
*
|
|
3687
|
+
* @returns
|
|
3688
|
+
*/
|
|
3689
|
+
async count() {
|
|
3690
|
+
if (this.hasRelationFilters()) return (await this.get()).all().length;
|
|
3691
|
+
return this.delegate.count({ where: this.buildWhere() });
|
|
3692
|
+
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Determines if any records exist for the current query constraints.
|
|
3695
|
+
*
|
|
3696
|
+
* @returns
|
|
3697
|
+
*/
|
|
3698
|
+
async exists() {
|
|
3699
|
+
if (this.hasRelationFilters()) return await this.count() > 0;
|
|
3700
|
+
return await this.delegate.findFirst(this.buildFindArgs()) != null;
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* Determines if no records exist for the current query constraints.
|
|
3704
|
+
*
|
|
3705
|
+
* @returns
|
|
3706
|
+
*/
|
|
3707
|
+
async doesntExist() {
|
|
3708
|
+
return !await this.exists();
|
|
3709
|
+
}
|
|
3710
|
+
/**
|
|
3711
|
+
* Execute callback when no records exist.
|
|
3712
|
+
*
|
|
3713
|
+
* @param callback
|
|
3714
|
+
* @returns
|
|
3715
|
+
*/
|
|
3716
|
+
async existsOr(callback) {
|
|
3717
|
+
if (await this.exists()) return true;
|
|
3718
|
+
return callback();
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Execute callback when records exist.
|
|
3722
|
+
*
|
|
3723
|
+
* @param callback
|
|
3724
|
+
* @returns
|
|
3725
|
+
*/
|
|
3726
|
+
async doesntExistOr(callback) {
|
|
3727
|
+
if (await this.doesntExist()) return true;
|
|
3728
|
+
return callback();
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Returns minimum value for a column.
|
|
3732
|
+
*
|
|
3733
|
+
* @param column
|
|
3734
|
+
* @returns
|
|
3735
|
+
*/
|
|
3736
|
+
async min(column) {
|
|
3737
|
+
const rows = await this.delegate.findMany(this.buildFindArgs());
|
|
3738
|
+
if (rows.length === 0) return null;
|
|
3739
|
+
const values = rows.map((row) => row[column]).filter((value) => value != null);
|
|
3740
|
+
if (values.length === 0) return null;
|
|
3741
|
+
return values.reduce((minValue, currentValue) => currentValue < minValue ? currentValue : minValue);
|
|
3742
|
+
}
|
|
3743
|
+
/**
|
|
3744
|
+
* Returns maximum value for a column.
|
|
3745
|
+
*
|
|
3746
|
+
* @param column
|
|
3747
|
+
* @returns
|
|
3748
|
+
*/
|
|
3749
|
+
async max(column) {
|
|
3750
|
+
const rows = await this.delegate.findMany(this.buildFindArgs());
|
|
3751
|
+
if (rows.length === 0) return null;
|
|
3752
|
+
const values = rows.map((row) => row[column]).filter((value) => value != null);
|
|
3753
|
+
if (values.length === 0) return null;
|
|
3754
|
+
return values.reduce((maxValue, currentValue) => currentValue > maxValue ? currentValue : maxValue);
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Returns sum of numeric values for a column.
|
|
3758
|
+
*
|
|
3759
|
+
* @param column
|
|
3760
|
+
* @returns
|
|
3761
|
+
*/
|
|
3762
|
+
async sum(column) {
|
|
3763
|
+
return (await this.delegate.findMany(this.buildFindArgs())).reduce((total, row) => {
|
|
3764
|
+
const value = row[column];
|
|
3765
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
3766
|
+
return Number.isFinite(numeric) ? total + numeric : total;
|
|
3767
|
+
}, 0);
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Returns average of numeric values for a column.
|
|
3771
|
+
*
|
|
3772
|
+
* @param column
|
|
3773
|
+
* @returns
|
|
3774
|
+
*/
|
|
3775
|
+
async avg(column) {
|
|
3776
|
+
const values = (await this.delegate.findMany(this.buildFindArgs())).map((row) => {
|
|
3777
|
+
const value = row[column];
|
|
3778
|
+
return typeof value === "number" ? value : Number(value);
|
|
3779
|
+
}).filter((value) => Number.isFinite(value));
|
|
3780
|
+
if (values.length === 0) return null;
|
|
3781
|
+
return values.reduce((total, value) => total + value, 0) / values.length;
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Adds a raw where clause when supported by the adapter.
|
|
3785
|
+
*
|
|
3786
|
+
* @param sql
|
|
3787
|
+
* @param bindings
|
|
3788
|
+
* @returns
|
|
3789
|
+
*/
|
|
3790
|
+
whereRaw(sql, bindings = []) {
|
|
3791
|
+
const delegate = this.delegate;
|
|
3792
|
+
if (typeof delegate.applyRawWhere !== "function") throw new ArkormException("Raw where clauses are not supported by the current adapter.");
|
|
3793
|
+
this.args.where = delegate.applyRawWhere(this.buildWhere(), sql, bindings);
|
|
3794
|
+
return this;
|
|
3795
|
+
}
|
|
3796
|
+
/**
|
|
3797
|
+
* Adds a raw OR where clause when supported by the adapter.
|
|
3798
|
+
*
|
|
3799
|
+
* @param sql
|
|
3800
|
+
* @param bindings
|
|
3801
|
+
* @returns
|
|
3802
|
+
*/
|
|
3803
|
+
orWhereRaw(sql, bindings = []) {
|
|
3804
|
+
const delegate = this.delegate;
|
|
3805
|
+
if (typeof delegate.applyRawWhere !== "function") throw new ArkormException("Raw where clauses are not supported by the current adapter.");
|
|
3806
|
+
const rawWhere = delegate.applyRawWhere(void 0, sql, bindings);
|
|
3807
|
+
return this.orWhere(rawWhere);
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Paginates the query results and returns a LengthAwarePaginator instance
|
|
3811
|
+
* containing data and total-aware pagination metadata.
|
|
3812
|
+
*
|
|
3813
|
+
* @param page
|
|
3814
|
+
* @param perPage
|
|
3815
|
+
* @param options
|
|
3816
|
+
* @returns
|
|
3817
|
+
*/
|
|
3818
|
+
async paginate(page = 1, perPage = 15, options = {}) {
|
|
3819
|
+
if (this.hasRelationFilters() || this.hasRelationAggregates()) {
|
|
3820
|
+
const currentPage = Math.max(1, page);
|
|
3821
|
+
const pageSize = Math.max(1, perPage);
|
|
3822
|
+
const rows = (await this.get()).all();
|
|
3823
|
+
const start = (currentPage - 1) * pageSize;
|
|
3824
|
+
return new LengthAwarePaginator(new ArkormCollection(rows.slice(start, start + pageSize)), rows.length, pageSize, currentPage, options);
|
|
3825
|
+
}
|
|
3826
|
+
const currentPage = Math.max(1, page);
|
|
3827
|
+
const pageSize = Math.max(1, perPage);
|
|
3828
|
+
const total = await this.count();
|
|
3829
|
+
return new LengthAwarePaginator(await this.clone().skip((currentPage - 1) * pageSize).take(pageSize).get(), total, pageSize, currentPage, options);
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* Paginates results without calculating total row count.
|
|
3833
|
+
*
|
|
3834
|
+
* @param perPage
|
|
3835
|
+
* @param page
|
|
3836
|
+
* @returns
|
|
3837
|
+
*/
|
|
3838
|
+
async simplePaginate(perPage = 15, page = 1, options = {}) {
|
|
3839
|
+
if (this.hasRelationFilters() || this.hasRelationAggregates()) {
|
|
3840
|
+
const currentPage = Math.max(1, page);
|
|
3841
|
+
const pageSize = Math.max(1, perPage);
|
|
3842
|
+
const rows = (await this.get()).all();
|
|
3843
|
+
const start = (currentPage - 1) * pageSize;
|
|
3844
|
+
const pageRows = rows.slice(start, start + pageSize);
|
|
3845
|
+
const hasMorePages = start + pageSize < rows.length;
|
|
3846
|
+
return new Paginator(new ArkormCollection(pageRows), pageSize, currentPage, hasMorePages, options);
|
|
3847
|
+
}
|
|
3848
|
+
const currentPage = Math.max(1, page);
|
|
3849
|
+
const pageSize = Math.max(1, perPage);
|
|
3850
|
+
const items = await this.clone().skip((currentPage - 1) * pageSize).take(pageSize + 1).get();
|
|
3851
|
+
const hasMorePages = items.all().length > pageSize;
|
|
3852
|
+
return new Paginator(hasMorePages ? new ArkormCollection(items.all().slice(0, pageSize)) : items, pageSize, currentPage, hasMorePages, options);
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Creates a clone of the current query builder instance with the same state.
|
|
3856
|
+
*
|
|
3857
|
+
* @returns
|
|
3858
|
+
*/
|
|
3859
|
+
clone() {
|
|
3860
|
+
const builder = new QueryBuilder(this.delegate, this.model);
|
|
3861
|
+
builder.args.where = this.args.where;
|
|
3862
|
+
builder.args.include = this.args.include;
|
|
3863
|
+
builder.args.orderBy = this.args.orderBy;
|
|
3864
|
+
builder.args.select = this.args.select;
|
|
3865
|
+
builder.args.skip = this.args.skip;
|
|
3866
|
+
builder.args.take = this.args.take;
|
|
3867
|
+
builder.includeTrashed = this.includeTrashed;
|
|
3868
|
+
builder.onlyTrashedRecords = this.onlyTrashedRecords;
|
|
3869
|
+
builder.randomOrderEnabled = this.randomOrderEnabled;
|
|
3870
|
+
this.relationFilters.forEach((filter) => {
|
|
3871
|
+
builder.relationFilters.push({ ...filter });
|
|
3872
|
+
});
|
|
3873
|
+
this.relationAggregates.forEach((aggregate) => {
|
|
3874
|
+
builder.relationAggregates.push({ ...aggregate });
|
|
3875
|
+
});
|
|
3876
|
+
Object.entries(this.eagerLoads).forEach(([key, value]) => {
|
|
3877
|
+
builder.eagerLoads[key] = value;
|
|
3878
|
+
});
|
|
3879
|
+
return builder;
|
|
3880
|
+
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Normalizes the input for eager loading relations into a consistent format.
|
|
3883
|
+
*
|
|
3884
|
+
* @param relations
|
|
3885
|
+
* @returns
|
|
3886
|
+
*/
|
|
3887
|
+
normalizeWith(relations) {
|
|
3888
|
+
if (typeof relations === "string") return { [relations]: void 0 };
|
|
3889
|
+
if (Array.isArray(relations)) return relations.reduce((accumulator, relation) => {
|
|
3890
|
+
accumulator[relation] = void 0;
|
|
3891
|
+
return accumulator;
|
|
3892
|
+
}, {});
|
|
3893
|
+
return relations;
|
|
3894
|
+
}
|
|
3895
|
+
/**
|
|
3896
|
+
* Builds the where clause for the query, taking into account soft delete
|
|
3897
|
+
* settings if applicable.
|
|
3898
|
+
*
|
|
3899
|
+
* @returns
|
|
3900
|
+
*/
|
|
3901
|
+
buildWhere() {
|
|
3902
|
+
const softDeleteConfig = this.model.getSoftDeleteConfig();
|
|
3903
|
+
if (!softDeleteConfig.enabled) return this.args.where;
|
|
3904
|
+
if (this.includeTrashed) return this.args.where;
|
|
3905
|
+
const softDeleteClause = this.onlyTrashedRecords ? { [softDeleteConfig.column]: { not: null } } : { [softDeleteConfig.column]: null };
|
|
3906
|
+
if (!this.args.where) return softDeleteClause;
|
|
3907
|
+
return { AND: [this.args.where, softDeleteClause] };
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Builds the arguments for the findMany delegate method, including the where clause.
|
|
3911
|
+
*
|
|
3912
|
+
* @returns
|
|
3913
|
+
*/
|
|
3914
|
+
buildFindArgs() {
|
|
3915
|
+
return {
|
|
3916
|
+
...this.args,
|
|
3917
|
+
where: this.buildWhere()
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Resolves a unique where clause for update and delete operations.
|
|
3922
|
+
*
|
|
3923
|
+
* @param where
|
|
3924
|
+
* @returns
|
|
3925
|
+
*/
|
|
3926
|
+
async resolveUniqueWhere(where) {
|
|
3927
|
+
if (this.isUniqueWhere(where)) return where;
|
|
3928
|
+
const row = await this.delegate.findFirst({ where });
|
|
3929
|
+
if (!row) throw new ArkormException("Record not found for update/delete operation.");
|
|
3930
|
+
const record = row;
|
|
3931
|
+
if (!Object.prototype.hasOwnProperty.call(record, "id")) throw new ArkormException("Unable to resolve a unique identifier for update/delete operation. Include an id in the query constraints.");
|
|
3932
|
+
return { id: record.id };
|
|
3933
|
+
}
|
|
3934
|
+
/**
|
|
3935
|
+
* Checks if the provided where clause is already a unique
|
|
3936
|
+
* identifier (i.e., contains only an 'id' field).
|
|
3937
|
+
*
|
|
3938
|
+
* @param where
|
|
3939
|
+
* @returns
|
|
3940
|
+
*/
|
|
3941
|
+
isUniqueWhere(where) {
|
|
3942
|
+
return Object.keys(where).length === 1 && Object.prototype.hasOwnProperty.call(where, "id");
|
|
3943
|
+
}
|
|
3944
|
+
shuffleRows(rows) {
|
|
3945
|
+
const shuffled = [...rows];
|
|
3946
|
+
for (let index = shuffled.length - 1; index > 0; index--) {
|
|
3947
|
+
const swapIndex = Math.floor(Math.random() * (index + 1));
|
|
3948
|
+
const current = shuffled[index];
|
|
3949
|
+
shuffled[index] = shuffled[swapIndex];
|
|
3950
|
+
shuffled[swapIndex] = current;
|
|
3951
|
+
}
|
|
3952
|
+
return shuffled;
|
|
3953
|
+
}
|
|
3954
|
+
hasRelationFilters() {
|
|
3955
|
+
return this.relationFilters.length > 0;
|
|
3956
|
+
}
|
|
3957
|
+
hasOrRelationFilters() {
|
|
3958
|
+
return this.relationFilters.some((filter) => filter.boolean === "OR");
|
|
3959
|
+
}
|
|
3960
|
+
hasRelationAggregates() {
|
|
3961
|
+
return this.relationAggregates.length > 0;
|
|
3962
|
+
}
|
|
3963
|
+
async filterModelsByRelationConstraints(models, relationCache, baseIds) {
|
|
3964
|
+
return (await Promise.all(models.map(async (model) => {
|
|
3965
|
+
let result = null;
|
|
3966
|
+
if (baseIds) result = baseIds.has(this.getModelId(model));
|
|
3967
|
+
for (const filter of this.relationFilters) {
|
|
3968
|
+
const relatedCount = await this.resolveRelatedCount(model, filter.relation, relationCache, filter.callback);
|
|
3969
|
+
const condition = this.compareCount(relatedCount, filter.operator, filter.count);
|
|
3970
|
+
if (result == null) result = condition;
|
|
3971
|
+
else result = filter.boolean === "AND" ? result && condition : result || condition;
|
|
3972
|
+
}
|
|
3973
|
+
return {
|
|
3974
|
+
model,
|
|
3975
|
+
passes: result ?? true
|
|
3976
|
+
};
|
|
3977
|
+
}))).filter((entry) => entry.passes).map((entry) => entry.model);
|
|
3978
|
+
}
|
|
3979
|
+
getModelId(model) {
|
|
3980
|
+
const readable = model;
|
|
3981
|
+
if (typeof readable.getAttribute !== "function") return null;
|
|
3982
|
+
const id = readable.getAttribute("id");
|
|
3983
|
+
if (typeof id === "number" || typeof id === "string") return id;
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3986
|
+
buildSoftDeleteOnlyWhere() {
|
|
3987
|
+
const softDeleteConfig = this.model.getSoftDeleteConfig();
|
|
3988
|
+
if (!softDeleteConfig.enabled) return void 0;
|
|
3989
|
+
if (this.includeTrashed) return void 0;
|
|
3990
|
+
return this.onlyTrashedRecords ? { [softDeleteConfig.column]: { not: null } } : { [softDeleteConfig.column]: null };
|
|
3991
|
+
}
|
|
3992
|
+
async applyRelationAggregates(models, relationCache) {
|
|
3993
|
+
const cache = relationCache ?? /* @__PURE__ */ new WeakMap();
|
|
3994
|
+
await Promise.all(models.map(async (model) => {
|
|
3995
|
+
for (const aggregate of this.relationAggregates) {
|
|
3996
|
+
const results = await this.resolveRelatedResults(model, aggregate.relation, cache);
|
|
3997
|
+
const list = Array.isArray(results) ? results : results ? [results] : [];
|
|
3998
|
+
const attributeKey = this.buildAggregateAttributeKey(aggregate);
|
|
3999
|
+
if (aggregate.type === "count") {
|
|
4000
|
+
this.assignAggregate(model, attributeKey, list.length);
|
|
4001
|
+
continue;
|
|
4002
|
+
}
|
|
4003
|
+
if (aggregate.type === "exists") {
|
|
4004
|
+
this.assignAggregate(model, attributeKey, list.length > 0);
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
const values = list.map((item) => item.getAttribute(aggregate.column)).filter((value) => value != null);
|
|
4008
|
+
if (aggregate.type === "sum") {
|
|
4009
|
+
const sum = values.reduce((total, value) => {
|
|
4010
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
4011
|
+
return Number.isFinite(numeric) ? total + numeric : total;
|
|
4012
|
+
}, 0);
|
|
4013
|
+
this.assignAggregate(model, attributeKey, sum);
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
4016
|
+
if (aggregate.type === "avg") {
|
|
4017
|
+
const numericValues = values.map((value) => typeof value === "number" ? value : Number(value)).filter((value) => Number.isFinite(value));
|
|
4018
|
+
const avg = numericValues.length === 0 ? null : numericValues.reduce((total, value) => total + value, 0) / numericValues.length;
|
|
4019
|
+
this.assignAggregate(model, attributeKey, avg);
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
4022
|
+
if (aggregate.type === "min") {
|
|
4023
|
+
const min = values.length === 0 ? null : values.reduce((left, right) => right < left ? right : left);
|
|
4024
|
+
this.assignAggregate(model, attributeKey, min);
|
|
4025
|
+
continue;
|
|
4026
|
+
}
|
|
4027
|
+
const max = values.length === 0 ? null : values.reduce((left, right) => right > left ? right : left);
|
|
4028
|
+
this.assignAggregate(model, attributeKey, max);
|
|
4029
|
+
}
|
|
4030
|
+
}));
|
|
4031
|
+
}
|
|
4032
|
+
async resolveRelatedCount(model, relation, relationCache, callback) {
|
|
4033
|
+
const results = await this.resolveRelatedResults(model, relation, relationCache, callback);
|
|
4034
|
+
if (Array.isArray(results)) return results.length;
|
|
4035
|
+
return results ? 1 : 0;
|
|
4036
|
+
}
|
|
4037
|
+
async resolveRelatedResults(model, relation, relationCache, callback) {
|
|
4038
|
+
const modelCacheKey = model;
|
|
4039
|
+
const callbackCacheKey = callback ?? "__none__";
|
|
4040
|
+
let relationMap = relationCache.get(modelCacheKey);
|
|
4041
|
+
if (!relationMap) {
|
|
4042
|
+
relationMap = /* @__PURE__ */ new Map();
|
|
4043
|
+
relationCache.set(modelCacheKey, relationMap);
|
|
4044
|
+
}
|
|
4045
|
+
let callbackMap = relationMap.get(relation);
|
|
4046
|
+
if (!callbackMap) {
|
|
4047
|
+
callbackMap = /* @__PURE__ */ new Map();
|
|
4048
|
+
relationMap.set(relation, callbackMap);
|
|
4049
|
+
}
|
|
4050
|
+
const cached = callbackMap.get(callbackCacheKey);
|
|
4051
|
+
if (cached) return await cached;
|
|
4052
|
+
const resolver = (async () => {
|
|
4053
|
+
const relationMethod = model[relation];
|
|
4054
|
+
if (typeof relationMethod !== "function") throw new ArkormException(`Relation [${relation}] is not defined on the model.`);
|
|
4055
|
+
const relationInstance = relationMethod.call(model);
|
|
4056
|
+
if (callback && typeof relationInstance.constrain === "function") relationInstance.constrain((query) => {
|
|
4057
|
+
return callback(query) ?? query;
|
|
4058
|
+
});
|
|
4059
|
+
if (typeof relationInstance.get === "function") {
|
|
4060
|
+
const results = await relationInstance.get();
|
|
4061
|
+
if (results instanceof ArkormCollection) return results.all();
|
|
4062
|
+
return results;
|
|
4063
|
+
}
|
|
4064
|
+
if (typeof relationInstance.getResults === "function") {
|
|
4065
|
+
const results = await relationInstance.getResults();
|
|
4066
|
+
if (results instanceof ArkormCollection) return results.all();
|
|
4067
|
+
return results;
|
|
4068
|
+
}
|
|
4069
|
+
throw new ArkormException(`Relation [${relation}] does not support result resolution.`);
|
|
4070
|
+
})();
|
|
4071
|
+
callbackMap.set(callbackCacheKey, resolver);
|
|
4072
|
+
return await resolver;
|
|
4073
|
+
}
|
|
4074
|
+
compareCount(left, operator, right) {
|
|
4075
|
+
if (operator === ">=") return left >= right;
|
|
4076
|
+
if (operator === ">") return left > right;
|
|
4077
|
+
if (operator === "=") return left === right;
|
|
4078
|
+
if (operator === "!=") return left !== right;
|
|
4079
|
+
if (operator === "<=") return left <= right;
|
|
4080
|
+
return left < right;
|
|
4081
|
+
}
|
|
4082
|
+
buildAggregateAttributeKey(aggregate) {
|
|
4083
|
+
const relationName = aggregate.relation;
|
|
4084
|
+
if (aggregate.type === "count") return `${relationName}Count`;
|
|
4085
|
+
if (aggregate.type === "exists") return `${relationName}Exists`;
|
|
4086
|
+
const columnName = aggregate.column ? `${aggregate.column.charAt(0).toUpperCase()}${aggregate.column.slice(1)}` : "";
|
|
4087
|
+
return `${relationName}${`${aggregate.type.charAt(0).toUpperCase()}${aggregate.type.slice(1)}`}${columnName}`;
|
|
4088
|
+
}
|
|
4089
|
+
assignAggregate(model, key, value) {
|
|
4090
|
+
const assignable = model;
|
|
4091
|
+
if (typeof assignable.setAttribute === "function") {
|
|
4092
|
+
assignable.setAttribute(key, value);
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
model[key] = value;
|
|
4096
|
+
}
|
|
4097
|
+
};
|
|
4098
|
+
|
|
4099
|
+
//#endregion
|
|
4100
|
+
//#region src/Model.ts
|
|
4101
|
+
/**
|
|
4102
|
+
* Base model class that all models should extend.
|
|
4103
|
+
*
|
|
4104
|
+
* @template TModel The type of the model extending this base class.
|
|
4105
|
+
*
|
|
4106
|
+
* @author Legacy (3m1n3nc3)
|
|
4107
|
+
* @since 0.1.0
|
|
4108
|
+
*/
|
|
4109
|
+
var Model = class Model {
|
|
4110
|
+
static factoryClass;
|
|
4111
|
+
static client;
|
|
4112
|
+
static delegate;
|
|
4113
|
+
static softDeletes = false;
|
|
4114
|
+
static deletedAtColumn = "deletedAt";
|
|
4115
|
+
static globalScopes = {};
|
|
4116
|
+
static eventListeners = {};
|
|
4117
|
+
casts = {};
|
|
4118
|
+
hidden = [];
|
|
4119
|
+
visible = [];
|
|
4120
|
+
appends = [];
|
|
4121
|
+
attributes;
|
|
4122
|
+
constructor(attributes = {}) {
|
|
4123
|
+
this.attributes = {};
|
|
4124
|
+
this.fill(attributes);
|
|
4125
|
+
return new Proxy(this, {
|
|
4126
|
+
get: (target, key, receiver) => {
|
|
4127
|
+
if (typeof key !== "string" || key in target) return Reflect.get(target, key, receiver);
|
|
4128
|
+
return target.getAttribute(key);
|
|
4129
|
+
},
|
|
4130
|
+
set: (target, key, value, receiver) => {
|
|
4131
|
+
if (typeof key !== "string" || key in target) return Reflect.set(target, key, value, receiver);
|
|
4132
|
+
target.setAttribute(key, value);
|
|
4133
|
+
return true;
|
|
4134
|
+
}
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
4137
|
+
/**
|
|
4138
|
+
* Set the Prisma client delegates for all models.
|
|
4139
|
+
*
|
|
4140
|
+
* @param client
|
|
4141
|
+
*/
|
|
4142
|
+
static setClient(client) {
|
|
4143
|
+
this.client = client;
|
|
4144
|
+
}
|
|
4145
|
+
static setFactory(factoryClass) {
|
|
4146
|
+
this.factoryClass = factoryClass;
|
|
4147
|
+
}
|
|
4148
|
+
static factory(count) {
|
|
4149
|
+
const factoryClass = this.factoryClass;
|
|
4150
|
+
if (!factoryClass) throw new ArkormException(`Factory is not configured for model [${this.name}].`);
|
|
4151
|
+
const factory = new factoryClass();
|
|
4152
|
+
if (typeof count === "number") factory.count(count);
|
|
4153
|
+
return factory;
|
|
4154
|
+
}
|
|
4155
|
+
/**
|
|
4156
|
+
* Register a global scope for the model.
|
|
4157
|
+
*
|
|
4158
|
+
* @param name
|
|
4159
|
+
* @param scope
|
|
4160
|
+
*/
|
|
4161
|
+
static addGlobalScope(name, scope) {
|
|
4162
|
+
this.ensureOwnGlobalScopes();
|
|
4163
|
+
this.globalScopes[name] = scope;
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Remove a global scope by name.
|
|
4167
|
+
*
|
|
4168
|
+
* @param name
|
|
4169
|
+
*/
|
|
4170
|
+
static removeGlobalScope(name) {
|
|
4171
|
+
this.ensureOwnGlobalScopes();
|
|
4172
|
+
delete this.globalScopes[name];
|
|
4173
|
+
}
|
|
4174
|
+
/**
|
|
4175
|
+
* Clear all global scopes for the model.
|
|
4176
|
+
*/
|
|
4177
|
+
static clearGlobalScopes() {
|
|
4178
|
+
this.globalScopes = {};
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Register an event listener for a model lifecycle event.
|
|
4182
|
+
*
|
|
4183
|
+
* @param event
|
|
4184
|
+
* @param listener
|
|
4185
|
+
*/
|
|
4186
|
+
static on(event, listener) {
|
|
4187
|
+
this.ensureOwnEventListeners();
|
|
4188
|
+
if (!this.eventListeners[event]) this.eventListeners[event] = [];
|
|
4189
|
+
this.eventListeners[event]?.push(listener);
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Remove listeners for an event. If listener is omitted, all listeners for that event are removed.
|
|
4193
|
+
*
|
|
4194
|
+
* @param event
|
|
4195
|
+
* @param listener
|
|
4196
|
+
*/
|
|
4197
|
+
static off(event, listener) {
|
|
4198
|
+
this.ensureOwnEventListeners();
|
|
4199
|
+
if (!listener) {
|
|
4200
|
+
delete this.eventListeners[event];
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
this.eventListeners[event] = (this.eventListeners[event] || []).filter((registered) => registered !== listener);
|
|
4204
|
+
}
|
|
4205
|
+
/**
|
|
4206
|
+
* Clears all event listeners for the model.
|
|
4207
|
+
*/
|
|
4208
|
+
static clearEventListeners() {
|
|
4209
|
+
this.eventListeners = {};
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* Get the Prisma delegate for the model.
|
|
4213
|
+
* If a delegate name is provided, it will attempt to resolve that delegate.
|
|
4214
|
+
* Otherwise, it will attempt to resolve a delegate based on the model's name or
|
|
4215
|
+
* the static `delegate` property.
|
|
4216
|
+
*
|
|
4217
|
+
* @param delegate
|
|
4218
|
+
* @returns
|
|
4219
|
+
*/
|
|
4220
|
+
static getDelegate(delegate) {
|
|
4221
|
+
ensureArkormConfigLoading();
|
|
4222
|
+
const key = delegate || this.delegate || `${(0, _h3ravel_support.str)(this.name).camel().plural()}`;
|
|
4223
|
+
const candidates = [
|
|
4224
|
+
key,
|
|
4225
|
+
`${(0, _h3ravel_support.str)(key).camel()}`,
|
|
4226
|
+
`${(0, _h3ravel_support.str)(key).singular()}`,
|
|
4227
|
+
`${(0, _h3ravel_support.str)(key).camel().singular()}`
|
|
4228
|
+
];
|
|
4229
|
+
const runtimeClient = getRuntimePrismaClient();
|
|
4230
|
+
const resolved = candidates.map((name) => this.client?.[name] ?? runtimeClient?.[name]).find((candidate) => isDelegateLike(candidate));
|
|
4231
|
+
if (!resolved) throw new ArkormException(`Database delegate [${key}] is not configured.`);
|
|
4232
|
+
return resolved;
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Get a new query builder instance for the model.
|
|
4236
|
+
*
|
|
4237
|
+
* @param this
|
|
4238
|
+
* @returns
|
|
4239
|
+
*/
|
|
4240
|
+
static query() {
|
|
4241
|
+
let builder = new QueryBuilder(this.getDelegate(), this);
|
|
4242
|
+
const modelClass = this;
|
|
4243
|
+
modelClass.ensureOwnGlobalScopes();
|
|
4244
|
+
Object.values(modelClass.globalScopes).forEach((scope) => {
|
|
4245
|
+
const scoped = scope(builder);
|
|
4246
|
+
if (scoped && scoped !== builder) builder = scoped;
|
|
4247
|
+
});
|
|
4248
|
+
return builder;
|
|
4249
|
+
}
|
|
4250
|
+
/**
|
|
4251
|
+
* Get a query builder instance that includes soft-deleted records.
|
|
4252
|
+
*
|
|
4253
|
+
* @param this
|
|
4254
|
+
* @returns
|
|
4255
|
+
*/
|
|
4256
|
+
static withTrashed() {
|
|
4257
|
+
return this.query().withTrashed();
|
|
4258
|
+
}
|
|
4259
|
+
/**
|
|
4260
|
+
* Get a query builder instance that only includes soft-deleted records.
|
|
4261
|
+
*
|
|
4262
|
+
* @param this
|
|
4263
|
+
* @returns
|
|
4264
|
+
*/
|
|
4265
|
+
static onlyTrashed() {
|
|
4266
|
+
return this.query().onlyTrashed();
|
|
4267
|
+
}
|
|
4268
|
+
/**
|
|
4269
|
+
* Get a query builder instance that excludes soft-deleted records.
|
|
4270
|
+
* This is the default behavior of the query builder, but this method can be used
|
|
4271
|
+
* to explicitly specify it after using `withTrashed` or `onlyTrashed`.
|
|
4272
|
+
*
|
|
4273
|
+
* @param this
|
|
4274
|
+
* @param name
|
|
4275
|
+
* @param args
|
|
4276
|
+
* @returns
|
|
4277
|
+
*/
|
|
4278
|
+
static scope(name, ...args) {
|
|
4279
|
+
return this.query().scope(name, ...args);
|
|
4280
|
+
}
|
|
4281
|
+
/**
|
|
4282
|
+
* Get the soft delete configuration for the model, including whether
|
|
4283
|
+
* soft deletes are enabled and the name of the deleted at column.
|
|
4284
|
+
*
|
|
4285
|
+
* @returns
|
|
4286
|
+
*/
|
|
4287
|
+
static getSoftDeleteConfig() {
|
|
4288
|
+
return {
|
|
4289
|
+
enabled: this.softDeletes,
|
|
4290
|
+
column: this.deletedAtColumn
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
/**
|
|
4294
|
+
* Hydrate a model instance from a plain object of attributes.
|
|
4295
|
+
*
|
|
4296
|
+
* @param this
|
|
4297
|
+
* @param attributes
|
|
4298
|
+
* @returns
|
|
4299
|
+
*/
|
|
4300
|
+
static hydrate(attributes) {
|
|
4301
|
+
return new this(attributes);
|
|
4302
|
+
}
|
|
4303
|
+
/**
|
|
4304
|
+
* Hydrate multiple model instances from an array of plain objects of attributes.
|
|
4305
|
+
*
|
|
4306
|
+
* @param this
|
|
4307
|
+
* @param attributes
|
|
4308
|
+
* @returns
|
|
4309
|
+
*/
|
|
4310
|
+
static hydrateMany(attributes) {
|
|
4311
|
+
return attributes.map((attribute) => new this(attribute));
|
|
4312
|
+
}
|
|
4313
|
+
fill(attributes) {
|
|
4314
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
4315
|
+
this.setAttribute(key, value);
|
|
4316
|
+
});
|
|
4317
|
+
return this;
|
|
4318
|
+
}
|
|
4319
|
+
getAttribute(key) {
|
|
4320
|
+
const mutator = this.resolveGetMutator(key);
|
|
4321
|
+
const cast = this.casts[key];
|
|
4322
|
+
let value = this.attributes[key];
|
|
4323
|
+
if (cast) value = resolveCast(cast).get(value);
|
|
4324
|
+
if (mutator) return mutator.call(this, value);
|
|
4325
|
+
return value;
|
|
4326
|
+
}
|
|
4327
|
+
setAttribute(key, value) {
|
|
4328
|
+
const mutator = this.resolveSetMutator(key);
|
|
4329
|
+
const cast = this.casts[key];
|
|
4330
|
+
let resolved = value;
|
|
4331
|
+
if (mutator) resolved = mutator.call(this, resolved);
|
|
4332
|
+
if (cast) resolved = resolveCast(cast).set(resolved);
|
|
4333
|
+
this.attributes[key] = resolved;
|
|
4334
|
+
return this;
|
|
4335
|
+
}
|
|
4336
|
+
/**
|
|
4337
|
+
* Save the model to the database.
|
|
4338
|
+
* If the model has an identifier (id), it will perform an update.
|
|
4339
|
+
* Otherwise, it will perform a create.
|
|
4340
|
+
*
|
|
4341
|
+
* @returns
|
|
4342
|
+
*/
|
|
4343
|
+
async save() {
|
|
4344
|
+
const identifier = this.getAttribute("id");
|
|
4345
|
+
const payload = this.getRawAttributes();
|
|
4346
|
+
const constructor = this.constructor;
|
|
4347
|
+
if (identifier == null) {
|
|
4348
|
+
await Model.dispatchEvent(constructor, "saving", this);
|
|
4349
|
+
await Model.dispatchEvent(constructor, "creating", this);
|
|
4350
|
+
const model = await constructor.query().create(payload);
|
|
4351
|
+
this.fill(model.getRawAttributes());
|
|
4352
|
+
await Model.dispatchEvent(constructor, "created", this);
|
|
4353
|
+
await Model.dispatchEvent(constructor, "saved", this);
|
|
4354
|
+
return this;
|
|
4355
|
+
}
|
|
4356
|
+
await Model.dispatchEvent(constructor, "saving", this);
|
|
4357
|
+
await Model.dispatchEvent(constructor, "updating", this);
|
|
4358
|
+
const model = await constructor.query().where({ id: identifier }).update(payload);
|
|
4359
|
+
this.fill(model.getRawAttributes());
|
|
4360
|
+
await Model.dispatchEvent(constructor, "updated", this);
|
|
4361
|
+
await Model.dispatchEvent(constructor, "saved", this);
|
|
4362
|
+
return this;
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Delete the model from the database.
|
|
4366
|
+
* If soft deletes are enabled, it will perform a soft delete by
|
|
4367
|
+
* setting the deleted at column to the current date.
|
|
4368
|
+
* Otherwise, it will perform a hard delete.
|
|
4369
|
+
*
|
|
4370
|
+
* @returns
|
|
4371
|
+
*/
|
|
4372
|
+
async delete() {
|
|
4373
|
+
const identifier = this.getAttribute("id");
|
|
4374
|
+
if (identifier == null) throw new ArkormException("Cannot delete a model without an id.");
|
|
4375
|
+
const constructor = this.constructor;
|
|
4376
|
+
await Model.dispatchEvent(constructor, "deleting", this);
|
|
4377
|
+
const softDeleteConfig = constructor.getSoftDeleteConfig();
|
|
4378
|
+
if (softDeleteConfig.enabled) {
|
|
4379
|
+
const model = await constructor.query().where({ id: identifier }).update({ [softDeleteConfig.column]: /* @__PURE__ */ new Date() });
|
|
4380
|
+
this.fill(model.getRawAttributes());
|
|
4381
|
+
await Model.dispatchEvent(constructor, "deleted", this);
|
|
4382
|
+
return this;
|
|
4383
|
+
}
|
|
4384
|
+
const deleted = await constructor.query().where({ id: identifier }).delete();
|
|
4385
|
+
this.fill(deleted.getRawAttributes());
|
|
4386
|
+
await Model.dispatchEvent(constructor, "deleted", this);
|
|
4387
|
+
return this;
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Permanently delete the model from the database, regardless of whether soft
|
|
4391
|
+
* deletes are enabled.
|
|
4392
|
+
*
|
|
4393
|
+
* @returns
|
|
4394
|
+
*/
|
|
4395
|
+
async forceDelete() {
|
|
4396
|
+
const identifier = this.getAttribute("id");
|
|
4397
|
+
if (identifier == null) throw new ArkormException("Cannot force delete a model without an id.");
|
|
4398
|
+
const constructor = this.constructor;
|
|
4399
|
+
await Model.dispatchEvent(constructor, "forceDeleting", this);
|
|
4400
|
+
await Model.dispatchEvent(constructor, "deleting", this);
|
|
4401
|
+
const deleted = await constructor.query().withTrashed().where({ id: identifier }).delete();
|
|
4402
|
+
this.fill(deleted.getRawAttributes());
|
|
4403
|
+
await Model.dispatchEvent(constructor, "deleted", this);
|
|
4404
|
+
await Model.dispatchEvent(constructor, "forceDeleted", this);
|
|
4405
|
+
return this;
|
|
4406
|
+
}
|
|
4407
|
+
/**
|
|
4408
|
+
* Restore a soft-deleted model by setting the deleted at column to null.
|
|
4409
|
+
*
|
|
4410
|
+
* @returns
|
|
4411
|
+
*/
|
|
4412
|
+
async restore() {
|
|
4413
|
+
const identifier = this.getAttribute("id");
|
|
4414
|
+
if (identifier == null) throw new ArkormException("Cannot restore a model without an id.");
|
|
4415
|
+
const constructor = this.constructor;
|
|
4416
|
+
const softDeleteConfig = constructor.getSoftDeleteConfig();
|
|
4417
|
+
if (!softDeleteConfig.enabled) return this;
|
|
4418
|
+
await Model.dispatchEvent(constructor, "restoring", this);
|
|
4419
|
+
const model = await constructor.query().withTrashed().where({ id: identifier }).update({ [softDeleteConfig.column]: null });
|
|
4420
|
+
this.fill(model.getRawAttributes());
|
|
4421
|
+
await Model.dispatchEvent(constructor, "restored", this);
|
|
4422
|
+
return this;
|
|
4423
|
+
}
|
|
4424
|
+
/**
|
|
4425
|
+
* Load related models onto the current model instance.
|
|
4426
|
+
*
|
|
4427
|
+
* @param relations
|
|
4428
|
+
* @returns
|
|
4429
|
+
*/
|
|
4430
|
+
async load(relations) {
|
|
4431
|
+
const relationMap = this.normalizeRelationMap(relations);
|
|
4432
|
+
await Promise.all(Object.entries(relationMap).map(async ([name, constraint]) => {
|
|
4433
|
+
const resolver = this[name];
|
|
4434
|
+
if (typeof resolver !== "function") return;
|
|
4435
|
+
const relation = resolver.call(this);
|
|
4436
|
+
if (constraint) relation.constrain(constraint);
|
|
4437
|
+
const results = await relation.getResults();
|
|
4438
|
+
this.attributes[name] = results;
|
|
4439
|
+
}));
|
|
4440
|
+
return this;
|
|
4441
|
+
}
|
|
4442
|
+
/**
|
|
4443
|
+
* Get the raw attributes of the model without applying any mutators or casts.
|
|
4444
|
+
*
|
|
4445
|
+
* @returns
|
|
4446
|
+
*/
|
|
4447
|
+
getRawAttributes() {
|
|
4448
|
+
return { ...this.attributes };
|
|
4449
|
+
}
|
|
4450
|
+
/**
|
|
4451
|
+
* Convert the model instance to a plain object, applying visibility
|
|
4452
|
+
* rules, appends, and mutators.
|
|
4453
|
+
*
|
|
4454
|
+
* @returns
|
|
4455
|
+
*/
|
|
4456
|
+
toObject() {
|
|
4457
|
+
const object = (this.visible.length > 0 ? this.visible : Object.keys(this.attributes).filter((key) => !this.hidden.includes(key))).reduce((accumulator, key) => {
|
|
4458
|
+
let value = this.getAttribute(key);
|
|
4459
|
+
if (value instanceof Date) value = value.toISOString();
|
|
4460
|
+
accumulator[key] = value;
|
|
4461
|
+
return accumulator;
|
|
4462
|
+
}, {});
|
|
4463
|
+
this.appends.forEach((attribute) => {
|
|
4464
|
+
object[attribute] = this.getAttribute(attribute);
|
|
4465
|
+
});
|
|
4466
|
+
return object;
|
|
4467
|
+
}
|
|
4468
|
+
/**
|
|
4469
|
+
* Convert the model instance to JSON by first converting it to a plain object.
|
|
4470
|
+
*
|
|
4471
|
+
* @returns
|
|
4472
|
+
*/
|
|
4473
|
+
toJSON() {
|
|
4474
|
+
return this.toObject();
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Define a has one relationship.
|
|
4478
|
+
*
|
|
4479
|
+
* @param related
|
|
4480
|
+
* @param foreignKey
|
|
4481
|
+
* @param localKey
|
|
4482
|
+
* @returns
|
|
4483
|
+
*/
|
|
4484
|
+
hasOne(related, foreignKey, localKey = "id") {
|
|
4485
|
+
return new HasOneRelation(this, related, foreignKey, localKey);
|
|
4486
|
+
}
|
|
4487
|
+
/**
|
|
4488
|
+
* Define a has many relationship.
|
|
4489
|
+
*
|
|
4490
|
+
* @param related
|
|
4491
|
+
* @param foreignKey
|
|
4492
|
+
* @param localKey
|
|
4493
|
+
* @returns
|
|
4494
|
+
*/
|
|
4495
|
+
hasMany(related, foreignKey, localKey = "id") {
|
|
4496
|
+
return new HasManyRelation(this, related, foreignKey, localKey);
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* Define a belongs to relationship.
|
|
4500
|
+
*
|
|
4501
|
+
* @param related
|
|
4502
|
+
* @param foreignKey
|
|
4503
|
+
* @param ownerKey
|
|
4504
|
+
* @returns
|
|
4505
|
+
*/
|
|
4506
|
+
belongsTo(related, foreignKey, ownerKey = "id") {
|
|
4507
|
+
return new BelongsToRelation(this, related, foreignKey, ownerKey);
|
|
4508
|
+
}
|
|
4509
|
+
/**
|
|
4510
|
+
* Define a belongs to many relationship.
|
|
4511
|
+
*
|
|
4512
|
+
* @param related
|
|
4513
|
+
* @param throughDelegate
|
|
4514
|
+
* @param foreignPivotKey
|
|
4515
|
+
* @param relatedPivotKey
|
|
4516
|
+
* @param parentKey
|
|
4517
|
+
* @param relatedKey
|
|
4518
|
+
* @returns
|
|
4519
|
+
*/
|
|
4520
|
+
belongsToMany(related, throughDelegate, foreignPivotKey, relatedPivotKey, parentKey = "id", relatedKey = "id") {
|
|
4521
|
+
return new BelongsToManyRelation(this, related, throughDelegate, foreignPivotKey, relatedPivotKey, parentKey, relatedKey);
|
|
4522
|
+
}
|
|
4523
|
+
/**
|
|
4524
|
+
* Define a has one through relationship.
|
|
4525
|
+
*
|
|
4526
|
+
* @param related
|
|
4527
|
+
* @param throughDelegate
|
|
4528
|
+
* @param firstKey
|
|
4529
|
+
* @param secondKey
|
|
4530
|
+
* @param localKey
|
|
4531
|
+
* @param secondLocalKey
|
|
4532
|
+
* @returns
|
|
4533
|
+
*/
|
|
4534
|
+
hasOneThrough(related, throughDelegate, firstKey, secondKey, localKey = "id", secondLocalKey = "id") {
|
|
4535
|
+
return new HasOneThroughRelation(this, related, throughDelegate, firstKey, secondKey, localKey, secondLocalKey);
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4538
|
+
* Define a has many through relationship.
|
|
4539
|
+
*
|
|
4540
|
+
* @param related
|
|
4541
|
+
* @param throughDelegate
|
|
4542
|
+
* @param firstKey
|
|
4543
|
+
* @param secondKey
|
|
4544
|
+
* @param localKey
|
|
4545
|
+
* @param secondLocalKey
|
|
4546
|
+
* @returns
|
|
4547
|
+
*/
|
|
4548
|
+
hasManyThrough(related, throughDelegate, firstKey, secondKey, localKey = "id", secondLocalKey = "id") {
|
|
4549
|
+
return new HasManyThroughRelation(this, related, throughDelegate, firstKey, secondKey, localKey, secondLocalKey);
|
|
4550
|
+
}
|
|
4551
|
+
/**
|
|
4552
|
+
* Define a polymorphic one to one relationship.
|
|
4553
|
+
*
|
|
4554
|
+
* @param related
|
|
4555
|
+
* @param morphName
|
|
4556
|
+
* @param localKey
|
|
4557
|
+
* @returns
|
|
4558
|
+
*/
|
|
4559
|
+
morphOne(related, morphName, localKey = "id") {
|
|
4560
|
+
return new MorphOneRelation(this, related, morphName, localKey);
|
|
4561
|
+
}
|
|
4562
|
+
/**
|
|
4563
|
+
* Define a polymorphic one to many relationship.
|
|
4564
|
+
*
|
|
4565
|
+
* @param related
|
|
4566
|
+
* @param morphName
|
|
4567
|
+
* @param localKey
|
|
4568
|
+
* @returns
|
|
4569
|
+
*/
|
|
4570
|
+
morphMany(related, morphName, localKey = "id") {
|
|
4571
|
+
return new MorphManyRelation(this, related, morphName, localKey);
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Define a polymorphic many to many relationship.
|
|
4575
|
+
*
|
|
4576
|
+
* @param related
|
|
4577
|
+
* @param throughDelegate
|
|
4578
|
+
* @param morphName
|
|
4579
|
+
* @param relatedPivotKey
|
|
4580
|
+
* @param parentKey
|
|
4581
|
+
* @param relatedKey
|
|
4582
|
+
* @returns
|
|
4583
|
+
*/
|
|
4584
|
+
morphToMany(related, throughDelegate, morphName, relatedPivotKey, parentKey = "id", relatedKey = "id") {
|
|
4585
|
+
return new MorphToManyRelation(this, related, throughDelegate, morphName, relatedPivotKey, parentKey, relatedKey);
|
|
4586
|
+
}
|
|
4587
|
+
/**
|
|
4588
|
+
* Resolve a get mutator method for a given attribute key, if it exists.
|
|
4589
|
+
*
|
|
4590
|
+
* @param key
|
|
4591
|
+
* @returns
|
|
4592
|
+
*/
|
|
4593
|
+
resolveGetMutator(key) {
|
|
4594
|
+
const methodName = `get${(0, _h3ravel_support.str)(key).studly()}Attribute`;
|
|
4595
|
+
const method = this[methodName];
|
|
4596
|
+
return typeof method === "function" ? method : null;
|
|
4597
|
+
}
|
|
4598
|
+
/**
|
|
4599
|
+
* Resolve a set mutator method for a given attribute key, if it exists.
|
|
4600
|
+
*
|
|
4601
|
+
* @param key
|
|
4602
|
+
* @returns
|
|
4603
|
+
*/
|
|
4604
|
+
resolveSetMutator(key) {
|
|
4605
|
+
const methodName = `set${(0, _h3ravel_support.str)(key).studly()}Attribute`;
|
|
4606
|
+
const method = this[methodName];
|
|
4607
|
+
return typeof method === "function" ? method : null;
|
|
4608
|
+
}
|
|
4609
|
+
/**
|
|
4610
|
+
* Ensures global scopes are own properties on subclass constructors.
|
|
4611
|
+
*/
|
|
4612
|
+
static ensureOwnGlobalScopes() {
|
|
4613
|
+
if (!Object.prototype.hasOwnProperty.call(this, "globalScopes")) this.globalScopes = { ...this.globalScopes || {} };
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Ensures event listeners are own properties on subclass constructors.
|
|
4617
|
+
*/
|
|
4618
|
+
static ensureOwnEventListeners() {
|
|
4619
|
+
if (!Object.prototype.hasOwnProperty.call(this, "eventListeners")) this.eventListeners = { ...this.eventListeners || {} };
|
|
4620
|
+
}
|
|
4621
|
+
/**
|
|
4622
|
+
* Dispatches lifecycle events to registered listeners.
|
|
4623
|
+
*
|
|
4624
|
+
* @param modelClass
|
|
4625
|
+
* @param event
|
|
4626
|
+
* @param model
|
|
4627
|
+
*/
|
|
4628
|
+
static async dispatchEvent(modelClass, event, model) {
|
|
4629
|
+
modelClass.ensureOwnEventListeners();
|
|
4630
|
+
const listeners = modelClass.eventListeners[event] || [];
|
|
4631
|
+
for (const listener of listeners) await listener(model);
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Normalize the relation map for eager loading.
|
|
4635
|
+
*
|
|
4636
|
+
* @param relations
|
|
4637
|
+
* @returns
|
|
4638
|
+
*/
|
|
4639
|
+
normalizeRelationMap(relations) {
|
|
4640
|
+
if (typeof relations === "string") return { [relations]: void 0 };
|
|
4641
|
+
if (Array.isArray(relations)) return relations.reduce((accumulator, relation) => {
|
|
4642
|
+
accumulator[relation] = void 0;
|
|
4643
|
+
return accumulator;
|
|
4644
|
+
}, {});
|
|
4645
|
+
return relations;
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
|
|
4649
|
+
//#endregion
|
|
4650
|
+
exports.ArkormCollection = ArkormCollection;
|
|
4651
|
+
exports.ArkormException = ArkormException;
|
|
4652
|
+
exports.CliApp = CliApp;
|
|
4653
|
+
exports.InitCommand = InitCommand;
|
|
4654
|
+
exports.InlineFactory = InlineFactory;
|
|
4655
|
+
exports.LengthAwarePaginator = LengthAwarePaginator;
|
|
4656
|
+
exports.MakeFactoryCommand = MakeFactoryCommand;
|
|
4657
|
+
exports.MakeMigrationCommand = MakeMigrationCommand;
|
|
4658
|
+
exports.MakeModelCommand = MakeModelCommand;
|
|
4659
|
+
exports.MakeSeederCommand = MakeSeederCommand;
|
|
4660
|
+
exports.MigrateCommand = MigrateCommand;
|
|
4661
|
+
exports.Migration = Migration;
|
|
4662
|
+
exports.Model = Model;
|
|
4663
|
+
exports.ModelFactory = ModelFactory;
|
|
4664
|
+
exports.ModelNotFoundException = ModelNotFoundException;
|
|
4665
|
+
exports.ModelsSyncCommand = ModelsSyncCommand;
|
|
4666
|
+
exports.PRISMA_MODEL_REGEX = PRISMA_MODEL_REGEX;
|
|
4667
|
+
exports.Paginator = Paginator;
|
|
4668
|
+
exports.QueryBuilder = QueryBuilder;
|
|
4669
|
+
exports.SchemaBuilder = SchemaBuilder;
|
|
4670
|
+
exports.SeedCommand = SeedCommand;
|
|
4671
|
+
exports.Seeder = Seeder;
|
|
4672
|
+
exports.TableBuilder = TableBuilder;
|
|
4673
|
+
exports.URLDriver = URLDriver;
|
|
4674
|
+
exports.applyAlterTableOperation = applyAlterTableOperation;
|
|
4675
|
+
exports.applyCreateTableOperation = applyCreateTableOperation;
|
|
4676
|
+
exports.applyDropTableOperation = applyDropTableOperation;
|
|
4677
|
+
exports.applyMigrationToPrismaSchema = applyMigrationToPrismaSchema;
|
|
4678
|
+
exports.applyOperationsToPrismaSchema = applyOperationsToPrismaSchema;
|
|
4679
|
+
exports.buildFieldLine = buildFieldLine;
|
|
4680
|
+
exports.buildIndexLine = buildIndexLine;
|
|
4681
|
+
exports.buildMigrationSource = buildMigrationSource;
|
|
4682
|
+
exports.buildModelBlock = buildModelBlock;
|
|
4683
|
+
exports.configureArkormRuntime = configureArkormRuntime;
|
|
4684
|
+
exports.createMigrationTimestamp = createMigrationTimestamp;
|
|
4685
|
+
exports.createPrismaAdapter = createPrismaAdapter;
|
|
4686
|
+
exports.createPrismaDelegateMap = createPrismaDelegateMap;
|
|
4687
|
+
exports.defineConfig = defineConfig;
|
|
4688
|
+
exports.defineFactory = defineFactory;
|
|
4689
|
+
exports.ensureArkormConfigLoading = ensureArkormConfigLoading;
|
|
4690
|
+
exports.escapeRegex = escapeRegex;
|
|
4691
|
+
exports.findModelBlock = findModelBlock;
|
|
4692
|
+
exports.formatDefaultValue = formatDefaultValue;
|
|
4693
|
+
exports.generateMigrationFile = generateMigrationFile;
|
|
4694
|
+
exports.getDefaultStubsPath = getDefaultStubsPath;
|
|
4695
|
+
exports.getMigrationPlan = getMigrationPlan;
|
|
4696
|
+
exports.getRuntimePaginationURLDriverFactory = getRuntimePaginationURLDriverFactory;
|
|
4697
|
+
exports.getRuntimePrismaClient = getRuntimePrismaClient;
|
|
4698
|
+
exports.getUserConfig = getUserConfig;
|
|
4699
|
+
exports.inferDelegateName = inferDelegateName;
|
|
4700
|
+
exports.isDelegateLike = isDelegateLike;
|
|
4701
|
+
exports.loadArkormConfig = loadArkormConfig;
|
|
4702
|
+
exports.pad = pad;
|
|
4703
|
+
exports.resetArkormRuntimeForTests = resetArkormRuntimeForTests;
|
|
4704
|
+
exports.resolveCast = resolveCast;
|
|
4705
|
+
exports.resolveMigrationClassName = resolveMigrationClassName;
|
|
4706
|
+
exports.resolvePrismaType = resolvePrismaType;
|
|
4707
|
+
exports.runMigrationWithPrisma = runMigrationWithPrisma;
|
|
4708
|
+
exports.runPrismaCommand = runPrismaCommand;
|
|
4709
|
+
exports.toMigrationFileSlug = toMigrationFileSlug;
|
|
4710
|
+
exports.toModelName = toModelName;
|