arkormx 0.1.8 → 0.1.9
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/cli.mjs +1831 -21
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,33 +1,1594 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
|
|
3
|
+
import path, { dirname, extname, join, relative } from "path";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
6
|
+
import { join as join$1, resolve } from "node:path";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { str } from "@h3ravel/support";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
10
|
+
import { Logger } from "@h3ravel/shared";
|
|
11
|
+
import { Command, Kernel } from "@h3ravel/musket";
|
|
12
|
+
import { pathToFileURL as pathToFileURL$1 } from "node:url";
|
|
13
|
+
|
|
14
|
+
//#region src/Exceptions/ArkormException.ts
|
|
15
|
+
/**
|
|
16
|
+
* The ArkormException class is a custom error type for handling
|
|
17
|
+
* exceptions specific to the Arkormˣ.
|
|
18
|
+
*
|
|
19
|
+
* @author Legacy (3m1n3nc3)
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
var ArkormException = class extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ArkormException";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/database/Migration.ts
|
|
31
|
+
/**
|
|
32
|
+
* The Migration class serves as a base for defining database migrations, requiring
|
|
33
|
+
* the implementation of `up` and `down` methods to specify the changes to be
|
|
34
|
+
* applied or reverted in the database schema.
|
|
35
|
+
*
|
|
36
|
+
* @author Legacy (3m1n3nc3)
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
var Migration = class {};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/database/TableBuilder.ts
|
|
43
|
+
/**
|
|
44
|
+
* The TableBuilder class provides a fluent interface for defining
|
|
45
|
+
* the structure of a database table in a migration, including columns to add or drop.
|
|
46
|
+
*
|
|
47
|
+
* @author Legacy (3m1n3nc3)
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
*/
|
|
50
|
+
var TableBuilder = class {
|
|
51
|
+
columns = [];
|
|
52
|
+
dropColumnNames = [];
|
|
53
|
+
indexes = [];
|
|
54
|
+
latestColumnName;
|
|
55
|
+
/**
|
|
56
|
+
* Defines a primary key column in the table.
|
|
57
|
+
*
|
|
58
|
+
* @param columnNameOrOptions
|
|
59
|
+
* @param options
|
|
60
|
+
* @returns
|
|
61
|
+
*/
|
|
62
|
+
primary(columnNameOrOptions, options) {
|
|
63
|
+
const config = typeof columnNameOrOptions === "string" ? {
|
|
64
|
+
columnName: columnNameOrOptions,
|
|
65
|
+
...options ?? {}
|
|
66
|
+
} : columnNameOrOptions ?? {};
|
|
67
|
+
const column = this.resolveColumn(config.columnName);
|
|
68
|
+
column.primary = true;
|
|
69
|
+
if (typeof config.autoIncrement === "boolean") column.autoIncrement = config.autoIncrement;
|
|
70
|
+
if (Object.prototype.hasOwnProperty.call(config, "default")) column.default = config.default;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Defines an auto-incrementing primary key column.
|
|
75
|
+
*
|
|
76
|
+
* @param name The name of the primary key column.
|
|
77
|
+
* @default 'id'
|
|
78
|
+
* @returns The current TableBuilder instance for chaining.
|
|
79
|
+
*/
|
|
80
|
+
id(name = "id", type = "id") {
|
|
81
|
+
return this.column(name, type, { primary: true });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Defines a UUID column in the table.
|
|
85
|
+
*
|
|
86
|
+
* @param name The name of the UUID column.
|
|
87
|
+
* @param options Additional options for the UUID column.
|
|
88
|
+
* @returns The current TableBuilder instance for chaining.
|
|
89
|
+
*/
|
|
90
|
+
uuid(name, options = {}) {
|
|
91
|
+
return this.column(name, "uuid", options);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Defines a string column in the table.
|
|
95
|
+
*
|
|
96
|
+
* @param name The name of the string column.
|
|
97
|
+
* @param options Additional options for the string column.
|
|
98
|
+
* @returns The current TableBuilder instance for chaining.
|
|
99
|
+
*/
|
|
100
|
+
string(name, options = {}) {
|
|
101
|
+
return this.column(name, "string", options);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Defines a text column in the table.
|
|
105
|
+
*
|
|
106
|
+
* @param name The name of the text column.
|
|
107
|
+
* @param options Additional options for the text column.
|
|
108
|
+
* @returns The current TableBuilder instance for chaining.
|
|
109
|
+
*/
|
|
110
|
+
text(name, options = {}) {
|
|
111
|
+
return this.column(name, "text", options);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Defines an integer column in the table.
|
|
115
|
+
*
|
|
116
|
+
* @param name The name of the integer column.
|
|
117
|
+
* @param options Additional options for the integer column.
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
integer(name, options = {}) {
|
|
121
|
+
return this.column(name, "integer", options);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Defines a big integer column in the table.
|
|
125
|
+
*
|
|
126
|
+
* @param name The name of the big integer column.
|
|
127
|
+
* @param options Additional options for the big integer column.
|
|
128
|
+
* @returns
|
|
129
|
+
*/
|
|
130
|
+
bigInteger(name, options = {}) {
|
|
131
|
+
return this.column(name, "bigInteger", options);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Defines a float column in the table.
|
|
135
|
+
*
|
|
136
|
+
* @param name The name of the float column.
|
|
137
|
+
* @param options Additional options for the float column.
|
|
138
|
+
* @returns
|
|
139
|
+
*/
|
|
140
|
+
float(name, options = {}) {
|
|
141
|
+
return this.column(name, "float", options);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Defines a boolean column in the table.
|
|
145
|
+
*
|
|
146
|
+
* @param name The name of the boolean column.
|
|
147
|
+
* @param options Additional options for the boolean column.
|
|
148
|
+
* @returns
|
|
149
|
+
*/
|
|
150
|
+
boolean(name, options = {}) {
|
|
151
|
+
return this.column(name, "boolean", options);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Defines a JSON column in the table.
|
|
155
|
+
*
|
|
156
|
+
* @param name The name of the JSON column.
|
|
157
|
+
* @param options Additional options for the JSON column.
|
|
158
|
+
* @returns
|
|
159
|
+
*/
|
|
160
|
+
json(name, options = {}) {
|
|
161
|
+
return this.column(name, "json", options);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Defines a date column in the table.
|
|
165
|
+
*
|
|
166
|
+
* @param name The name of the date column.
|
|
167
|
+
* @param options Additional options for the date column.
|
|
168
|
+
* @returns
|
|
169
|
+
*/
|
|
170
|
+
date(name, options = {}) {
|
|
171
|
+
return this.column(name, "date", options);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Defines colonns for a polymorphic relationship in the table.
|
|
175
|
+
*
|
|
176
|
+
* @param name The base name for the polymorphic relationship columns.
|
|
177
|
+
* @returns
|
|
178
|
+
*/
|
|
179
|
+
morphs(name, nullable = false) {
|
|
180
|
+
this.string(`${name}Type`, { nullable });
|
|
181
|
+
this.integer(`${name}Id`, { nullable });
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Defines nullable columns for a polymorphic relationship in the table.
|
|
186
|
+
*
|
|
187
|
+
* @param name The base name for the polymorphic relationship columns.
|
|
188
|
+
* @returns
|
|
189
|
+
*/
|
|
190
|
+
nullableMorphs(name) {
|
|
191
|
+
return this.morphs(name, true);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Defines a timestamp column in the table.
|
|
195
|
+
*
|
|
196
|
+
* @param name The name of the timestamp column.
|
|
197
|
+
* @param options Additional options for the timestamp column.
|
|
198
|
+
* @returns
|
|
199
|
+
*/
|
|
200
|
+
timestamp(name, options = {}) {
|
|
201
|
+
return this.column(name, "timestamp", options);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Defines both createdAt and updatedAt timestamp columns in the table.
|
|
205
|
+
*
|
|
206
|
+
* @returns
|
|
207
|
+
*/
|
|
208
|
+
timestamps() {
|
|
209
|
+
this.timestamp("createdAt", { nullable: false });
|
|
210
|
+
this.timestamp("updatedAt", { nullable: false });
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Defines a soft delete timestamp column in the table.
|
|
215
|
+
*
|
|
216
|
+
* @param column The name of the soft delete column.
|
|
217
|
+
* @returns
|
|
218
|
+
*/
|
|
219
|
+
softDeletes(column = "deletedAt") {
|
|
220
|
+
this.timestamp(column, { nullable: true });
|
|
221
|
+
return this;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Defines a column to be dropped from the table in an alterTable operation.
|
|
225
|
+
*
|
|
226
|
+
* @param name The name of the column to drop.
|
|
227
|
+
* @returns
|
|
228
|
+
*/
|
|
229
|
+
dropColumn(name) {
|
|
230
|
+
this.dropColumnNames.push(name);
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Marks a column as nullable.
|
|
235
|
+
*
|
|
236
|
+
* @param columnName Optional explicit column name. When omitted, applies to the latest defined column.
|
|
237
|
+
* @returns The current TableBuilder instance for chaining.
|
|
238
|
+
*/
|
|
239
|
+
nullable(columnName) {
|
|
240
|
+
const column = this.resolveColumn(columnName);
|
|
241
|
+
column.nullable = true;
|
|
242
|
+
return this;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Sets the column position to appear after another column when possible.
|
|
246
|
+
*
|
|
247
|
+
* @param referenceColumn The column that the target column should be placed after.
|
|
248
|
+
* @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
|
|
249
|
+
* @returns The current TableBuilder instance for chaining.
|
|
250
|
+
*/
|
|
251
|
+
after(referenceColumn, columnName) {
|
|
252
|
+
const column = this.resolveColumn(columnName);
|
|
253
|
+
column.after = referenceColumn;
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Maps the column to a custom database column name.
|
|
258
|
+
*
|
|
259
|
+
* @param name The custom database column name.
|
|
260
|
+
* @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
|
|
261
|
+
* @returns The current TableBuilder instance for chaining.
|
|
262
|
+
*/
|
|
263
|
+
map(name, columnName) {
|
|
264
|
+
const column = this.resolveColumn(columnName);
|
|
265
|
+
column.map = name;
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Defines an index on one or more columns.
|
|
270
|
+
*
|
|
271
|
+
* @param columns Optional target columns. When omitted, applies to the latest defined column.
|
|
272
|
+
* @param name Optional index name.
|
|
273
|
+
* @returns The current TableBuilder instance for chaining.
|
|
274
|
+
*/
|
|
275
|
+
index(columns, name) {
|
|
276
|
+
const columnList = Array.isArray(columns) ? columns : typeof columns === "string" ? [columns] : [this.resolveColumn().name];
|
|
277
|
+
this.indexes.push({
|
|
278
|
+
columns: [...columnList],
|
|
279
|
+
name
|
|
280
|
+
});
|
|
281
|
+
return this;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Returns a deep copy of the defined columns for the table.
|
|
285
|
+
*
|
|
286
|
+
* @returns
|
|
287
|
+
*/
|
|
288
|
+
getColumns() {
|
|
289
|
+
return this.columns.map((column) => ({ ...column }));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Returns a copy of the defined column names to be dropped from the table.
|
|
293
|
+
*
|
|
294
|
+
* @returns
|
|
295
|
+
*/
|
|
296
|
+
getDropColumns() {
|
|
297
|
+
return [...this.dropColumnNames];
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Returns a deep copy of the defined indexes for the table.
|
|
301
|
+
*
|
|
302
|
+
* @returns
|
|
303
|
+
*/
|
|
304
|
+
getIndexes() {
|
|
305
|
+
return this.indexes.map((index) => ({
|
|
306
|
+
...index,
|
|
307
|
+
columns: [...index.columns]
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Defines a column in the table with the given name.
|
|
312
|
+
*
|
|
313
|
+
* @param name The name of the column.
|
|
314
|
+
* @param type The type of the column.
|
|
315
|
+
* @param options Additional options for the column.
|
|
316
|
+
* @returns
|
|
317
|
+
*/
|
|
318
|
+
column(name, type, options) {
|
|
319
|
+
this.columns.push({
|
|
320
|
+
name,
|
|
321
|
+
type,
|
|
322
|
+
map: options.map,
|
|
323
|
+
nullable: options.nullable,
|
|
324
|
+
unique: options.unique,
|
|
325
|
+
primary: options.primary,
|
|
326
|
+
autoIncrement: options.autoIncrement,
|
|
327
|
+
after: options.after,
|
|
328
|
+
default: options.default
|
|
329
|
+
});
|
|
330
|
+
this.latestColumnName = name;
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Resolve a target column by name or fallback to the latest defined column.
|
|
335
|
+
*
|
|
336
|
+
* @param columnName
|
|
337
|
+
* @returns
|
|
338
|
+
*/
|
|
339
|
+
resolveColumn(columnName) {
|
|
340
|
+
const targetName = columnName ?? this.latestColumnName;
|
|
341
|
+
if (!targetName) throw new Error("No column available for this operation.");
|
|
342
|
+
const column = this.columns.find((item) => item.name === targetName);
|
|
343
|
+
if (!column) throw new Error(`Column [${targetName}] was not found in the table definition.`);
|
|
344
|
+
return column;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/database/SchemaBuilder.ts
|
|
350
|
+
/**
|
|
351
|
+
* The SchemaBuilder class provides methods for defining the operations to be
|
|
352
|
+
* performed in a migration, such as creating, altering, or dropping tables.
|
|
353
|
+
*
|
|
354
|
+
* @author Legacy (3m1n3nc3)
|
|
355
|
+
* @since 0.1.0
|
|
356
|
+
*/
|
|
357
|
+
var SchemaBuilder = class {
|
|
358
|
+
operations = [];
|
|
359
|
+
/**
|
|
360
|
+
* Defines a new table to be created in the migration.
|
|
361
|
+
*
|
|
362
|
+
* @param table The name of the table to create.
|
|
363
|
+
* @param callback A callback function to define the table's columns and structure.
|
|
364
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
365
|
+
*/
|
|
366
|
+
createTable(table, callback) {
|
|
367
|
+
const builder = new TableBuilder();
|
|
368
|
+
callback(builder);
|
|
369
|
+
this.operations.push({
|
|
370
|
+
type: "createTable",
|
|
371
|
+
table,
|
|
372
|
+
columns: builder.getColumns(),
|
|
373
|
+
indexes: builder.getIndexes()
|
|
374
|
+
});
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Defines alterations to an existing table in the migration.
|
|
379
|
+
*
|
|
380
|
+
* @param table The name of the table to alter.
|
|
381
|
+
* @param callback A callback function to define the alterations to the table's columns and structure.
|
|
382
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
383
|
+
*/
|
|
384
|
+
alterTable(table, callback) {
|
|
385
|
+
const builder = new TableBuilder();
|
|
386
|
+
callback(builder);
|
|
387
|
+
this.operations.push({
|
|
388
|
+
type: "alterTable",
|
|
389
|
+
table,
|
|
390
|
+
addColumns: builder.getColumns(),
|
|
391
|
+
dropColumns: builder.getDropColumns(),
|
|
392
|
+
addIndexes: builder.getIndexes()
|
|
393
|
+
});
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Defines a table to be dropped in the migration.
|
|
398
|
+
*
|
|
399
|
+
* @param table The name of the table to drop.
|
|
400
|
+
* @returns The current SchemaBuilder instance for chaining.
|
|
401
|
+
*/
|
|
402
|
+
dropTable(table) {
|
|
403
|
+
this.operations.push({
|
|
404
|
+
type: "dropTable",
|
|
405
|
+
table
|
|
406
|
+
});
|
|
407
|
+
return this;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Returns a deep copy of the defined schema operations for the migration/
|
|
411
|
+
*
|
|
412
|
+
* @returns An array of schema operations for the migration.
|
|
413
|
+
*/
|
|
414
|
+
getOperations() {
|
|
415
|
+
return this.operations.map((operation) => {
|
|
416
|
+
if (operation.type === "createTable") return {
|
|
417
|
+
...operation,
|
|
418
|
+
columns: operation.columns.map((column) => ({ ...column })),
|
|
419
|
+
indexes: operation.indexes.map((index) => ({
|
|
420
|
+
...index,
|
|
421
|
+
columns: [...index.columns]
|
|
422
|
+
}))
|
|
423
|
+
};
|
|
424
|
+
if (operation.type === "alterTable") return {
|
|
425
|
+
...operation,
|
|
426
|
+
addColumns: operation.addColumns.map((column) => ({ ...column })),
|
|
427
|
+
dropColumns: [...operation.dropColumns],
|
|
428
|
+
addIndexes: operation.addIndexes.map((index) => ({
|
|
429
|
+
...index,
|
|
430
|
+
columns: [...index.columns]
|
|
431
|
+
}))
|
|
432
|
+
};
|
|
433
|
+
return { ...operation };
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/helpers/migrations.ts
|
|
440
|
+
const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
|
|
441
|
+
/**
|
|
442
|
+
* Convert a table name to a PascalCase model name, with basic singularization.
|
|
443
|
+
*
|
|
444
|
+
* @param tableName The name of the table to convert.
|
|
445
|
+
* @returns The corresponding PascalCase model name.
|
|
446
|
+
*/
|
|
447
|
+
const toModelName = (tableName) => {
|
|
448
|
+
const normalized = tableName.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
449
|
+
const parts = (normalized.endsWith("s") && normalized.length > 1 ? normalized.slice(0, -1) : normalized).split(/\s+/g).filter(Boolean);
|
|
450
|
+
if (parts.length === 0) return "GeneratedModel";
|
|
451
|
+
return parts.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* Escape special characters in a string for use in a regular expression.
|
|
455
|
+
*
|
|
456
|
+
* @param value
|
|
457
|
+
* @returns
|
|
458
|
+
*/
|
|
459
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
460
|
+
/**
|
|
461
|
+
* Convert a SchemaColumn definition to a Prisma field type string, including modifiers.
|
|
462
|
+
*
|
|
463
|
+
* @param column
|
|
464
|
+
* @returns
|
|
465
|
+
*/
|
|
466
|
+
const resolvePrismaType = (column) => {
|
|
467
|
+
if (column.type === "id") return "Int";
|
|
468
|
+
if (column.type === "uuid") return "String";
|
|
469
|
+
if (column.type === "string" || column.type === "text") return "String";
|
|
470
|
+
if (column.type === "integer") return "Int";
|
|
471
|
+
if (column.type === "bigInteger") return "BigInt";
|
|
472
|
+
if (column.type === "float") return "Float";
|
|
473
|
+
if (column.type === "boolean") return "Boolean";
|
|
474
|
+
if (column.type === "json") return "Json";
|
|
475
|
+
return "DateTime";
|
|
476
|
+
};
|
|
477
|
+
/**
|
|
478
|
+
* Format a default value for inclusion in a Prisma schema field definition, based on its type.
|
|
479
|
+
*
|
|
480
|
+
* @param value
|
|
481
|
+
* @returns
|
|
482
|
+
*/
|
|
483
|
+
const formatDefaultValue = (value) => {
|
|
484
|
+
if (value == null) return void 0;
|
|
485
|
+
if (typeof value === "string") return `@default("${value.replace(/"/g, "\\\"")}")`;
|
|
486
|
+
if (typeof value === "number" || typeof value === "bigint") return `@default(${value})`;
|
|
487
|
+
if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
|
|
491
|
+
*
|
|
492
|
+
* @param column
|
|
493
|
+
* @returns
|
|
494
|
+
*/
|
|
495
|
+
const buildFieldLine = (column) => {
|
|
496
|
+
if (column.type === "id") {
|
|
497
|
+
const primary = column.primary === false ? "" : " @id";
|
|
498
|
+
const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
|
|
499
|
+
const configuredDefault = formatDefaultValue(column.default);
|
|
500
|
+
const shouldAutoIncrement = column.autoIncrement ?? column.primary !== false;
|
|
501
|
+
const defaultSuffix = configuredDefault ? ` ${configuredDefault}` : shouldAutoIncrement && primary ? " @default(autoincrement())" : "";
|
|
502
|
+
return ` ${column.name} Int${primary}${defaultSuffix}${mapped}`;
|
|
503
|
+
}
|
|
504
|
+
const scalar = resolvePrismaType(column);
|
|
505
|
+
const nullable = column.nullable ? "?" : "";
|
|
506
|
+
const unique = column.unique ? " @unique" : "";
|
|
507
|
+
const primary = column.primary ? " @id" : "";
|
|
508
|
+
const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
|
|
509
|
+
const defaultValue = formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
|
|
510
|
+
const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
|
|
511
|
+
return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${mapped}`;
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* Build a Prisma model-level @@index definition line.
|
|
515
|
+
*
|
|
516
|
+
* @param index
|
|
517
|
+
* @returns
|
|
518
|
+
*/
|
|
519
|
+
const buildIndexLine = (index) => {
|
|
520
|
+
return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
|
|
521
|
+
};
|
|
522
|
+
/**
|
|
523
|
+
* Build a Prisma model block string based on a SchemaTableCreateOperation, including
|
|
524
|
+
* all fields and any necessary mapping.
|
|
525
|
+
*
|
|
526
|
+
* @param operation The schema table create operation to convert.
|
|
527
|
+
* @returns The corresponding Prisma model block string.
|
|
528
|
+
*/
|
|
529
|
+
const buildModelBlock = (operation) => {
|
|
530
|
+
const modelName = toModelName(operation.table);
|
|
531
|
+
const mapped = operation.table !== modelName.toLowerCase();
|
|
532
|
+
const fields = operation.columns.map(buildFieldLine);
|
|
533
|
+
const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
|
|
534
|
+
return `model ${modelName} {\n${(metadata.length > 0 ? [
|
|
535
|
+
...fields,
|
|
536
|
+
"",
|
|
537
|
+
...metadata
|
|
538
|
+
] : fields).join("\n")}\n}`;
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* Find the Prisma model block in a schema string that corresponds to a given
|
|
542
|
+
* table name, using both explicit mapping and naming conventions.
|
|
543
|
+
*
|
|
544
|
+
* @param schema
|
|
545
|
+
* @param table
|
|
546
|
+
* @returns
|
|
547
|
+
*/
|
|
548
|
+
const findModelBlock = (schema, table) => {
|
|
549
|
+
const candidates = [...schema.matchAll(PRISMA_MODEL_REGEX)];
|
|
550
|
+
const explicitMapRegex = new RegExp(`@@map\\("${escapeRegex(table)}"\\)`);
|
|
551
|
+
for (const match of candidates) {
|
|
552
|
+
const block = match[0];
|
|
553
|
+
const modelName = match[1];
|
|
554
|
+
const start = match.index ?? 0;
|
|
555
|
+
const end = start + block.length;
|
|
556
|
+
if (explicitMapRegex.test(block)) return {
|
|
557
|
+
modelName,
|
|
558
|
+
block,
|
|
559
|
+
start,
|
|
560
|
+
end
|
|
561
|
+
};
|
|
562
|
+
if (modelName.toLowerCase() === table.toLowerCase()) return {
|
|
563
|
+
modelName,
|
|
564
|
+
block,
|
|
565
|
+
start,
|
|
566
|
+
end
|
|
567
|
+
};
|
|
568
|
+
if (modelName.toLowerCase() === toModelName(table).toLowerCase()) return {
|
|
569
|
+
modelName,
|
|
570
|
+
block,
|
|
571
|
+
start,
|
|
572
|
+
end
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Apply a create table operation to a Prisma schema string, adding a new model
|
|
579
|
+
* block for the specified table and fields.
|
|
580
|
+
*
|
|
581
|
+
* @param schema The current Prisma schema string.
|
|
582
|
+
* @param operation The schema table create operation to apply.
|
|
583
|
+
* @returns The updated Prisma schema string with the new model block.
|
|
584
|
+
*/
|
|
585
|
+
const applyCreateTableOperation = (schema, operation) => {
|
|
586
|
+
if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
|
|
587
|
+
const block = buildModelBlock(operation);
|
|
588
|
+
return `${schema.trimEnd()}\n\n${block}\n`;
|
|
589
|
+
};
|
|
590
|
+
/**
|
|
591
|
+
* Apply an alter table operation to a Prisma schema string, modifying the model
|
|
592
|
+
* block for the specified table by adding and removing fields as needed.
|
|
593
|
+
*
|
|
594
|
+
* @param schema The current Prisma schema string.
|
|
595
|
+
* @param operation The schema table alter operation to apply.
|
|
596
|
+
* @returns The updated Prisma schema string with the modified model block.
|
|
597
|
+
*/
|
|
598
|
+
const applyAlterTableOperation = (schema, operation) => {
|
|
599
|
+
const model = findModelBlock(schema, operation.table);
|
|
600
|
+
if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
|
|
601
|
+
let block = model.block;
|
|
602
|
+
const bodyLines = block.split("\n");
|
|
603
|
+
operation.dropColumns.forEach((column) => {
|
|
604
|
+
const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
|
|
605
|
+
for (let index = 0; index < bodyLines.length; index += 1) if (columnRegex.test(bodyLines[index])) {
|
|
606
|
+
bodyLines.splice(index, 1);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
operation.addColumns.forEach((column) => {
|
|
611
|
+
const fieldLine = buildFieldLine(column);
|
|
612
|
+
const columnRegex = new RegExp(`^\\s*${escapeRegex(column.name)}\\s+`);
|
|
613
|
+
if (bodyLines.some((line) => columnRegex.test(line))) return;
|
|
614
|
+
const defaultInsertIndex = Math.max(1, bodyLines.length - 1);
|
|
615
|
+
const afterInsertIndex = typeof column.after === "string" && column.after.length > 0 ? bodyLines.findIndex((line) => new RegExp(`^\\s*${escapeRegex(column.after)}\\s+`).test(line)) : -1;
|
|
616
|
+
const insertIndex = afterInsertIndex > 0 ? Math.min(afterInsertIndex + 1, defaultInsertIndex) : defaultInsertIndex;
|
|
617
|
+
bodyLines.splice(insertIndex, 0, fieldLine);
|
|
618
|
+
});
|
|
619
|
+
(operation.addIndexes ?? []).forEach((index) => {
|
|
620
|
+
const indexLine = buildIndexLine(index);
|
|
621
|
+
if (bodyLines.some((line) => line.trim() === indexLine.trim())) return;
|
|
622
|
+
const insertIndex = Math.max(1, bodyLines.length - 1);
|
|
623
|
+
bodyLines.splice(insertIndex, 0, indexLine);
|
|
624
|
+
});
|
|
625
|
+
block = bodyLines.join("\n");
|
|
626
|
+
return `${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`;
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Apply a drop table operation to a Prisma schema string, removing the model block
|
|
630
|
+
* for the specified table.
|
|
631
|
+
*/
|
|
632
|
+
const applyDropTableOperation = (schema, operation) => {
|
|
633
|
+
const model = findModelBlock(schema, operation.table);
|
|
634
|
+
if (!model) return schema;
|
|
635
|
+
const before = schema.slice(0, model.start).trimEnd();
|
|
636
|
+
const after = schema.slice(model.end).trimStart();
|
|
637
|
+
return `${before}${before && after ? "\n\n" : ""}${after}`;
|
|
638
|
+
};
|
|
639
|
+
/**
|
|
640
|
+
* The SchemaBuilder class provides a fluent interface for defining
|
|
641
|
+
* database schema operations in a migration, such as creating, altering, and
|
|
642
|
+
* dropping tables.
|
|
643
|
+
*
|
|
644
|
+
* @param schema The current Prisma schema string.
|
|
645
|
+
* @param operations The list of schema operations to apply.
|
|
646
|
+
* @returns The updated Prisma schema string after applying all operations.
|
|
647
|
+
*/
|
|
648
|
+
const applyOperationsToPrismaSchema = (schema, operations) => {
|
|
649
|
+
return operations.reduce((current, operation) => {
|
|
650
|
+
if (operation.type === "createTable") return applyCreateTableOperation(current, operation);
|
|
651
|
+
if (operation.type === "alterTable") return applyAlterTableOperation(current, operation);
|
|
652
|
+
return applyDropTableOperation(current, operation);
|
|
653
|
+
}, schema);
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Run a Prisma CLI command using npx, capturing and throwing any errors that occur.
|
|
657
|
+
*
|
|
658
|
+
* @param args The arguments to pass to the Prisma CLI command.
|
|
659
|
+
* @param cwd The current working directory to run the command in.
|
|
660
|
+
* @returns void
|
|
661
|
+
*/
|
|
662
|
+
const runPrismaCommand = (args, cwd) => {
|
|
663
|
+
const command = spawnSync("npx", ["prisma", ...args], {
|
|
664
|
+
cwd,
|
|
665
|
+
encoding: "utf-8"
|
|
666
|
+
});
|
|
667
|
+
if (command.status === 0) return;
|
|
668
|
+
const errorOutput = [command.stdout, command.stderr].filter(Boolean).join("\n").trim();
|
|
669
|
+
throw new ArkormException(errorOutput ? `Prisma command failed: prisma ${args.join(" ")}\n${errorOutput}` : `Prisma command failed: prisma ${args.join(" ")}`);
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* Generate a new migration file with a given name and options, including
|
|
673
|
+
* writing the file to disk if specified.
|
|
674
|
+
*
|
|
675
|
+
* @param name
|
|
676
|
+
* @returns
|
|
677
|
+
*/
|
|
678
|
+
const resolveMigrationClassName = (name) => {
|
|
679
|
+
const cleaned = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
680
|
+
if (!cleaned) return "GeneratedMigration";
|
|
681
|
+
return `${cleaned.split(/\s+/g).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}Migration`;
|
|
682
|
+
};
|
|
683
|
+
/**
|
|
684
|
+
* Pad a number with leading zeros to ensure it is at least two digits, for
|
|
685
|
+
* use in migration timestamps.
|
|
686
|
+
*
|
|
687
|
+
* @param value
|
|
688
|
+
* @returns
|
|
689
|
+
*/
|
|
690
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
691
|
+
/**
|
|
692
|
+
* Create a timestamp string in the format YYYYMMDDHHMMSS for use in migration
|
|
693
|
+
* file names, based on the current date and time or a provided date.
|
|
694
|
+
*
|
|
695
|
+
* @param date
|
|
696
|
+
* @returns
|
|
697
|
+
*/
|
|
698
|
+
const createMigrationTimestamp = (date = /* @__PURE__ */ new Date()) => {
|
|
699
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* Convert a migration name to a slug suitable for use in a file name, by
|
|
703
|
+
* lowercasing and replacing non-alphanumeric characters with underscores.
|
|
704
|
+
*
|
|
705
|
+
* @param name
|
|
706
|
+
* @returns
|
|
707
|
+
*/
|
|
708
|
+
const toMigrationFileSlug = (name) => {
|
|
709
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration";
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
712
|
+
* Build the source code for a new migration file based on a given class
|
|
713
|
+
* name, using a template with empty up and down methods.
|
|
714
|
+
*
|
|
715
|
+
* @param className
|
|
716
|
+
* @returns
|
|
717
|
+
*/
|
|
718
|
+
const buildMigrationSource = (className, extension = "ts") => {
|
|
719
|
+
if (extension === "js") return [
|
|
720
|
+
"import { Migration } from 'arkormx'",
|
|
721
|
+
"",
|
|
722
|
+
`export default class ${className} extends Migration {`,
|
|
723
|
+
" /**",
|
|
724
|
+
" * @param {import('arkormx').SchemaBuilder} schema",
|
|
725
|
+
" * @returns {Promise<void>}",
|
|
726
|
+
" */",
|
|
727
|
+
" async up (schema) {",
|
|
728
|
+
" }",
|
|
729
|
+
"",
|
|
730
|
+
" /**",
|
|
731
|
+
" * @param {import('arkormx').SchemaBuilder} schema",
|
|
732
|
+
" * @returns {Promise<void>}",
|
|
733
|
+
" */",
|
|
734
|
+
" async down (schema) {",
|
|
735
|
+
" }",
|
|
736
|
+
"}",
|
|
737
|
+
""
|
|
738
|
+
].join("\n");
|
|
739
|
+
return [
|
|
740
|
+
"import { Migration, SchemaBuilder } from 'arkormx'",
|
|
741
|
+
"",
|
|
742
|
+
`export default class ${className} extends Migration {`,
|
|
743
|
+
" public async up (schema: SchemaBuilder): Promise<void> {",
|
|
744
|
+
" }",
|
|
745
|
+
"",
|
|
746
|
+
" public async down (schema: SchemaBuilder): Promise<void> {",
|
|
747
|
+
" }",
|
|
748
|
+
"}",
|
|
749
|
+
""
|
|
750
|
+
].join("\n");
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* Generate a new migration file with a given name and options, including
|
|
754
|
+
* writing the file to disk if specified, and return the details of the generated file.
|
|
755
|
+
*
|
|
756
|
+
* @param name
|
|
757
|
+
* @param options
|
|
758
|
+
* @returns
|
|
759
|
+
*/
|
|
760
|
+
const generateMigrationFile = (name, options = {}) => {
|
|
761
|
+
const timestamp = createMigrationTimestamp(/* @__PURE__ */ new Date());
|
|
762
|
+
const fileSlug = toMigrationFileSlug(name);
|
|
763
|
+
const className = resolveMigrationClassName(name);
|
|
764
|
+
const extension = options.extension ?? "ts";
|
|
765
|
+
const directory = options.directory ?? join$1(process.cwd(), "database", "migrations");
|
|
766
|
+
const fileName = `${timestamp}_${fileSlug}.${extension}`;
|
|
767
|
+
const filePath = join$1(directory, fileName);
|
|
768
|
+
const content = buildMigrationSource(className, extension);
|
|
769
|
+
if (options.write ?? true) {
|
|
770
|
+
if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
|
|
771
|
+
if (existsSync$1(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
|
|
772
|
+
writeFileSync$1(filePath, content);
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
fileName,
|
|
776
|
+
filePath,
|
|
777
|
+
className,
|
|
778
|
+
content
|
|
779
|
+
};
|
|
780
|
+
};
|
|
781
|
+
/**
|
|
782
|
+
* 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.
|
|
783
|
+
*
|
|
784
|
+
* @param migration The migration class or instance to analyze.
|
|
785
|
+
* @param direction The direction of the migration to plan for ('up' or 'down').
|
|
786
|
+
* @returns A promise that resolves to an array of schema operations that would be performed.
|
|
787
|
+
*/
|
|
788
|
+
const getMigrationPlan = async (migration, direction = "up") => {
|
|
789
|
+
const instance = migration instanceof Migration ? migration : new migration();
|
|
790
|
+
const schema = new SchemaBuilder();
|
|
791
|
+
if (direction === "up") await instance.up(schema);
|
|
792
|
+
else await instance.down(schema);
|
|
793
|
+
return schema.getOperations();
|
|
794
|
+
};
|
|
795
|
+
/**
|
|
796
|
+
* Apply the schema operations defined in a migration to a Prisma schema
|
|
797
|
+
* file, updating the file on disk if specified, and return the updated
|
|
798
|
+
* schema and list of operations applied.
|
|
799
|
+
*
|
|
800
|
+
* @param migration The migration class or instance to apply.
|
|
801
|
+
* @param options Options for applying the migration, including schema path and write flag.
|
|
802
|
+
* @returns A promise that resolves to an object containing the updated schema, schema path, and list of operations applied.
|
|
803
|
+
*/
|
|
804
|
+
const applyMigrationToPrismaSchema = async (migration, options = {}) => {
|
|
805
|
+
const schemaPath = options.schemaPath ?? join$1(process.cwd(), "prisma", "schema.prisma");
|
|
806
|
+
if (!existsSync$1(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
|
|
807
|
+
const source = readFileSync$1(schemaPath, "utf-8");
|
|
808
|
+
const operations = await getMigrationPlan(migration, "up");
|
|
809
|
+
const schema = applyOperationsToPrismaSchema(source, operations);
|
|
810
|
+
if (options.write ?? true) writeFileSync$1(schemaPath, schema);
|
|
811
|
+
return {
|
|
812
|
+
schema,
|
|
813
|
+
schemaPath,
|
|
814
|
+
operations
|
|
815
|
+
};
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/helpers/runtime-config.ts
|
|
820
|
+
const resolveDefaultStubsPath = () => {
|
|
821
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
822
|
+
while (true) {
|
|
823
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
824
|
+
const stubsPath = path.join(current, "stubs");
|
|
825
|
+
if (existsSync(packageJsonPath) && existsSync(stubsPath)) return stubsPath;
|
|
826
|
+
const parent = path.dirname(current);
|
|
827
|
+
if (parent === current) break;
|
|
828
|
+
current = parent;
|
|
829
|
+
}
|
|
830
|
+
return path.join(process.cwd(), "stubs");
|
|
831
|
+
};
|
|
832
|
+
const baseConfig = {
|
|
833
|
+
paths: {
|
|
834
|
+
stubs: resolveDefaultStubsPath(),
|
|
835
|
+
seeders: path.join(process.cwd(), "database", "seeders"),
|
|
836
|
+
models: path.join(process.cwd(), "src", "models"),
|
|
837
|
+
migrations: path.join(process.cwd(), "database", "migrations"),
|
|
838
|
+
factories: path.join(process.cwd(), "database", "factories"),
|
|
839
|
+
buildOutput: path.join(process.cwd(), "dist")
|
|
840
|
+
},
|
|
841
|
+
outputExt: "ts"
|
|
842
|
+
};
|
|
843
|
+
const userConfig = {
|
|
844
|
+
...baseConfig,
|
|
845
|
+
paths: { ...baseConfig.paths ?? {} }
|
|
846
|
+
};
|
|
847
|
+
let runtimeConfigLoaded = false;
|
|
848
|
+
let runtimeConfigLoadingPromise;
|
|
849
|
+
let runtimeClientResolver;
|
|
850
|
+
let runtimePaginationURLDriverFactory;
|
|
851
|
+
const mergePathConfig = (paths) => {
|
|
852
|
+
const defaults = baseConfig.paths ?? {};
|
|
853
|
+
const current = userConfig.paths ?? {};
|
|
854
|
+
const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
|
|
855
|
+
if (typeof value === "string" && value.trim().length > 0) all[key] = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
856
|
+
return all;
|
|
857
|
+
}, {});
|
|
858
|
+
return {
|
|
859
|
+
...defaults,
|
|
860
|
+
...current,
|
|
861
|
+
...incoming
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
/**
|
|
865
|
+
* Get the user-provided ArkORM configuration.
|
|
866
|
+
*
|
|
867
|
+
* @returns The user-provided ArkORM configuration object.
|
|
868
|
+
*/
|
|
869
|
+
const getUserConfig = (key) => {
|
|
870
|
+
if (key) return userConfig[key];
|
|
871
|
+
return userConfig;
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Configure the ArkORM runtime with the provided Prisma client resolver and
|
|
875
|
+
* delegate mapping resolver.
|
|
876
|
+
*
|
|
877
|
+
* @param prisma
|
|
878
|
+
* @param mapping
|
|
879
|
+
*/
|
|
880
|
+
const configureArkormRuntime = (prisma, options = {}) => {
|
|
881
|
+
const nextConfig = {
|
|
882
|
+
...userConfig,
|
|
883
|
+
prisma,
|
|
884
|
+
paths: mergePathConfig(options.paths)
|
|
885
|
+
};
|
|
886
|
+
if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
|
|
887
|
+
if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
|
|
888
|
+
Object.assign(userConfig, { ...nextConfig });
|
|
889
|
+
runtimeClientResolver = prisma;
|
|
890
|
+
runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
|
|
891
|
+
};
|
|
892
|
+
/**
|
|
893
|
+
* Resolve and apply the ArkORM configuration from an imported module.
|
|
894
|
+
* This function checks for a default export and falls back to the module itself, then validates
|
|
895
|
+
* the configuration object and applies it to the runtime if valid.
|
|
896
|
+
*
|
|
897
|
+
* @param imported
|
|
898
|
+
* @returns
|
|
899
|
+
*/
|
|
900
|
+
const resolveAndApplyConfig = (imported) => {
|
|
901
|
+
const config = imported?.default ?? imported;
|
|
902
|
+
if (!config || typeof config !== "object" || !config.prisma) return;
|
|
903
|
+
configureArkormRuntime(config.prisma, {
|
|
904
|
+
pagination: config.pagination,
|
|
905
|
+
paths: config.paths,
|
|
906
|
+
outputExt: config.outputExt
|
|
907
|
+
});
|
|
908
|
+
runtimeConfigLoaded = true;
|
|
909
|
+
};
|
|
910
|
+
/**
|
|
911
|
+
* Dynamically import a configuration file.
|
|
912
|
+
* A cache-busting query parameter is appended to ensure the latest version is loaded.
|
|
913
|
+
*
|
|
914
|
+
* @param configPath
|
|
915
|
+
* @returns A promise that resolves to the imported configuration module.
|
|
916
|
+
*/
|
|
917
|
+
const importConfigFile = (configPath) => {
|
|
918
|
+
return import(`${pathToFileURL(configPath).href}?arkorm_runtime=${Date.now()}`);
|
|
919
|
+
};
|
|
920
|
+
const loadRuntimeConfigSync = () => {
|
|
921
|
+
const require = createRequire(import.meta.url);
|
|
922
|
+
const syncConfigPaths = [path.join(process.cwd(), "arkormx.config.cjs")];
|
|
923
|
+
for (const configPath of syncConfigPaths) {
|
|
924
|
+
if (!existsSync(configPath)) continue;
|
|
925
|
+
try {
|
|
926
|
+
resolveAndApplyConfig(require(configPath));
|
|
927
|
+
return true;
|
|
928
|
+
} catch {
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return false;
|
|
933
|
+
};
|
|
934
|
+
/**
|
|
935
|
+
* Load the ArkORM configuration by searching for configuration files in the
|
|
936
|
+
* current working directory.
|
|
937
|
+
* @returns
|
|
938
|
+
*/
|
|
939
|
+
const loadArkormConfig = async () => {
|
|
940
|
+
if (runtimeConfigLoaded) return;
|
|
941
|
+
if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
|
|
942
|
+
if (loadRuntimeConfigSync()) return;
|
|
943
|
+
runtimeConfigLoadingPromise = (async () => {
|
|
944
|
+
const configPaths = [path.join(process.cwd(), "arkormx.config.js"), path.join(process.cwd(), "arkormx.config.ts")];
|
|
945
|
+
for (const configPath of configPaths) {
|
|
946
|
+
if (!existsSync(configPath)) continue;
|
|
947
|
+
try {
|
|
948
|
+
resolveAndApplyConfig(await importConfigFile(configPath));
|
|
949
|
+
return;
|
|
950
|
+
} catch {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
runtimeConfigLoaded = true;
|
|
955
|
+
})();
|
|
956
|
+
await runtimeConfigLoadingPromise;
|
|
957
|
+
};
|
|
958
|
+
const getDefaultStubsPath = () => {
|
|
959
|
+
return resolveDefaultStubsPath();
|
|
960
|
+
};
|
|
961
|
+
loadArkormConfig();
|
|
962
|
+
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/cli/CliApp.ts
|
|
965
|
+
/**
|
|
966
|
+
* Main application class for the Arkormˣ CLI.
|
|
967
|
+
*
|
|
968
|
+
* @author Legacy (3m1n3nc3)
|
|
969
|
+
* @since 0.1.0
|
|
970
|
+
*/
|
|
971
|
+
var CliApp = class {
|
|
972
|
+
command;
|
|
973
|
+
config = {};
|
|
974
|
+
constructor() {
|
|
975
|
+
this.config = getUserConfig();
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get the current configuration object or a specific configuration value.
|
|
979
|
+
*
|
|
980
|
+
* @param key Optional specific configuration key to retrieve
|
|
981
|
+
* @returns The entire configuration object or the value of the specified key
|
|
982
|
+
*/
|
|
983
|
+
getConfig = getUserConfig;
|
|
984
|
+
/**
|
|
985
|
+
* Utility to ensure directory exists
|
|
986
|
+
*
|
|
987
|
+
* @param filePath
|
|
988
|
+
*/
|
|
989
|
+
ensureDirectory(filePath) {
|
|
990
|
+
const dir = dirname(filePath);
|
|
991
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Convert absolute paths under current working directory into relative display paths.
|
|
995
|
+
*
|
|
996
|
+
* @param filePath
|
|
997
|
+
* @returns
|
|
998
|
+
*/
|
|
999
|
+
formatPathForLog(filePath) {
|
|
1000
|
+
const relPath = relative(process.cwd(), filePath);
|
|
1001
|
+
if (!relPath) return ".";
|
|
1002
|
+
if (relPath.startsWith("..")) return filePath;
|
|
1003
|
+
return relPath;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Utility to format a value for logging, converting absolute paths under current
|
|
1007
|
+
* working directory into relative display paths.
|
|
1008
|
+
*
|
|
1009
|
+
* @param name
|
|
1010
|
+
* @param value
|
|
1011
|
+
* @returns
|
|
1012
|
+
*/
|
|
1013
|
+
splitLogger(name, value) {
|
|
1014
|
+
value = value.includes(process.cwd()) ? this.formatPathForLog(value) : value;
|
|
1015
|
+
return Logger.twoColumnDetail(name + " ", " " + value, false).join("");
|
|
1016
|
+
}
|
|
1017
|
+
hasTypeScriptInstalled() {
|
|
1018
|
+
try {
|
|
1019
|
+
createRequire(import.meta.url).resolve("typescript", { paths: [process.cwd()] });
|
|
1020
|
+
return true;
|
|
1021
|
+
} catch {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
resolveOutputExt() {
|
|
1026
|
+
const preferred = this.getConfig("outputExt") === "js" ? "js" : "ts";
|
|
1027
|
+
if (preferred === "ts" && !this.hasTypeScriptInstalled()) return "js";
|
|
1028
|
+
return preferred;
|
|
1029
|
+
}
|
|
1030
|
+
stripKnownSourceExtension(value) {
|
|
1031
|
+
return value.replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "");
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Resolve a directory path to runtime output when the source path is unavailable.
|
|
1035
|
+
*
|
|
1036
|
+
* @param directoryPath
|
|
1037
|
+
* @returns
|
|
1038
|
+
*/
|
|
1039
|
+
resolveRuntimeDirectoryPath(directoryPath) {
|
|
1040
|
+
if (existsSync(directoryPath)) return directoryPath;
|
|
1041
|
+
const { buildOutput } = this.getConfig("paths") || {};
|
|
1042
|
+
if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directoryPath;
|
|
1043
|
+
const relativeSource = relative(process.cwd(), directoryPath);
|
|
1044
|
+
if (!relativeSource || relativeSource.startsWith("..")) return directoryPath;
|
|
1045
|
+
const mappedDirectory = join(buildOutput, relativeSource);
|
|
1046
|
+
return existsSync(mappedDirectory) ? mappedDirectory : directoryPath;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolve a script file path for runtime execution.
|
|
1050
|
+
* If a .ts file is provided, tries equivalent .js/.cjs/.mjs files first.
|
|
1051
|
+
* Also attempts mapped paths inside paths.buildOutput preserving structure.
|
|
1052
|
+
*
|
|
1053
|
+
* @param filePath
|
|
1054
|
+
* @returns
|
|
1055
|
+
*/
|
|
1056
|
+
resolveRuntimeScriptPath(filePath) {
|
|
1057
|
+
const extension = extname(filePath).toLowerCase();
|
|
1058
|
+
const isTsFile = extension === ".ts" || extension === ".mts" || extension === ".cts";
|
|
1059
|
+
const candidates = [];
|
|
1060
|
+
if (isTsFile) {
|
|
1061
|
+
const base = filePath.slice(0, -extension.length);
|
|
1062
|
+
candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
|
|
1063
|
+
}
|
|
1064
|
+
const { buildOutput } = this.getConfig("paths") ?? {};
|
|
1065
|
+
if (typeof buildOutput === "string" && buildOutput.trim().length > 0) {
|
|
1066
|
+
const relativeSource = relative(process.cwd(), filePath);
|
|
1067
|
+
if (relativeSource && !relativeSource.startsWith("..")) {
|
|
1068
|
+
const mappedFile = join(buildOutput, relativeSource);
|
|
1069
|
+
const mappedExtension = extname(mappedFile).toLowerCase();
|
|
1070
|
+
if (mappedExtension === ".ts" || mappedExtension === ".mts" || mappedExtension === ".cts") {
|
|
1071
|
+
const mappedBase = mappedFile.slice(0, -mappedExtension.length);
|
|
1072
|
+
candidates.push(`${mappedBase}.js`, `${mappedBase}.cjs`, `${mappedBase}.mjs`);
|
|
1073
|
+
} else candidates.push(mappedFile);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const runtimeMatch = candidates.find((path) => existsSync(path));
|
|
1077
|
+
if (runtimeMatch) return runtimeMatch;
|
|
1078
|
+
return filePath;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Utility to generate file from stub
|
|
1082
|
+
*
|
|
1083
|
+
* @param stubPath
|
|
1084
|
+
* @param outputPath
|
|
1085
|
+
* @param replacements
|
|
1086
|
+
*/
|
|
1087
|
+
generateFile(stubPath, outputPath, replacements, options) {
|
|
1088
|
+
if (existsSync(outputPath) && !options?.force) {
|
|
1089
|
+
this.command.error(`Error: ${this.formatPathForLog(outputPath)} already exists.`);
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
} else if (existsSync(outputPath) && options?.force) rmSync(outputPath);
|
|
1092
|
+
let content = readFileSync(stubPath, "utf-8");
|
|
1093
|
+
for (const [key, value] of Object.entries(replacements)) content = content.replace(new RegExp(`{{${key}}}`, "g"), value);
|
|
1094
|
+
this.ensureDirectory(outputPath);
|
|
1095
|
+
writeFileSync(outputPath, content);
|
|
1096
|
+
return outputPath;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Resolve a configuration path with a fallback default
|
|
1100
|
+
*
|
|
1101
|
+
* @param key The configuration key to resolve
|
|
1102
|
+
* @param fallback The fallback value if the configuration key is not set
|
|
1103
|
+
* @returns The resolved configuration path
|
|
1104
|
+
*/
|
|
1105
|
+
resolveConfigPath(key, fallback) {
|
|
1106
|
+
const { [key]: configured } = this.getConfig("paths") ?? {};
|
|
1107
|
+
if (typeof configured === "string" && configured.trim().length > 0) return configured;
|
|
1108
|
+
return fallback;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Resolve the path to a stub file based on configuration
|
|
1112
|
+
*
|
|
1113
|
+
* @param stubName
|
|
1114
|
+
* @returns
|
|
1115
|
+
*/
|
|
1116
|
+
resolveStubPath(stubName) {
|
|
1117
|
+
return join(this.resolveConfigPath("stubs", getDefaultStubsPath()), stubName);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Generate a factory file for a given model name.
|
|
1121
|
+
*
|
|
1122
|
+
* @param name
|
|
1123
|
+
* @param options
|
|
1124
|
+
* @returns
|
|
1125
|
+
*/
|
|
1126
|
+
makeFactory(name, options = {}) {
|
|
1127
|
+
const baseName = str(name.replace(/Factory$/, "")).pascal();
|
|
1128
|
+
const factoryName = `${baseName}Factory`;
|
|
1129
|
+
const modelName = options.modelName ? str(options.modelName).pascal() : baseName;
|
|
1130
|
+
const outputExt = this.resolveOutputExt();
|
|
1131
|
+
const outputPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
|
|
1132
|
+
const modelPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
|
|
1133
|
+
const relativeImport = options.modelImportPath ?? `./${this.stripKnownSourceExtension(relative(dirname(outputPath), modelPath).replace(/\\/g, "/"))}${outputExt === "js" ? ".js" : ""}`;
|
|
1134
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "factory.js.stub" : "factory.stub");
|
|
1135
|
+
return {
|
|
1136
|
+
name: factoryName,
|
|
1137
|
+
path: this.generateFile(stubPath, outputPath, {
|
|
1138
|
+
FactoryName: factoryName,
|
|
1139
|
+
ModelName: modelName.toString(),
|
|
1140
|
+
ModelImportPath: relativeImport.startsWith(".") ? relativeImport : `./${relativeImport}`
|
|
1141
|
+
}, options)
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Generate a seeder file for a given name.
|
|
1146
|
+
*
|
|
1147
|
+
* @param name
|
|
1148
|
+
* @param options
|
|
1149
|
+
* @returns
|
|
1150
|
+
*/
|
|
1151
|
+
makeSeeder(name, options = {}) {
|
|
1152
|
+
const seederName = `${str(name.replace(/Seeder$/, "")).pascal()}Seeder`;
|
|
1153
|
+
const outputExt = this.resolveOutputExt();
|
|
1154
|
+
const outputPath = join(this.resolveConfigPath("seeders", join(process.cwd(), "database", "seeders")), `${seederName}.${outputExt}`);
|
|
1155
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "seeder.js.stub" : "seeder.stub");
|
|
1156
|
+
return {
|
|
1157
|
+
name: seederName,
|
|
1158
|
+
path: this.generateFile(stubPath, outputPath, { SeederName: seederName }, options)
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Generate a migration file for a given name.
|
|
1163
|
+
*
|
|
1164
|
+
* @param name The name of the migration.
|
|
1165
|
+
* @returns An object containing the name and path of the generated migration file.
|
|
1166
|
+
*/
|
|
1167
|
+
makeMigration(name) {
|
|
1168
|
+
const generated = generateMigrationFile(name, {
|
|
1169
|
+
directory: this.resolveConfigPath("migrations", join(process.cwd(), "database", "migrations")),
|
|
1170
|
+
extension: this.resolveOutputExt()
|
|
1171
|
+
});
|
|
1172
|
+
return {
|
|
1173
|
+
name: generated.className,
|
|
1174
|
+
path: generated.filePath
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Generate a model file along with optional factory, seeder, and migration files.
|
|
1179
|
+
*
|
|
1180
|
+
* @param name
|
|
1181
|
+
* @param options
|
|
1182
|
+
* @returns
|
|
1183
|
+
*/
|
|
1184
|
+
makeModel(name, options = {}) {
|
|
1185
|
+
const baseName = str(name.replace(/Model$/, "")).pascal().toString();
|
|
1186
|
+
const modelName = `${baseName}`;
|
|
1187
|
+
const delegateName = str(baseName).camel().plural().toString();
|
|
1188
|
+
const outputExt = this.resolveOutputExt();
|
|
1189
|
+
const outputPath = join(this.resolveConfigPath("models", join(process.cwd(), "src", "models")), `${modelName}.${outputExt}`);
|
|
1190
|
+
const shouldBuildFactory = options.all || options.factory;
|
|
1191
|
+
const shouldBuildSeeder = options.all || options.seeder;
|
|
1192
|
+
const shouldBuildMigration = options.all || options.migration;
|
|
1193
|
+
const factoryName = `${baseName}Factory`;
|
|
1194
|
+
const factoryPath = join(this.resolveConfigPath("factories", join(process.cwd(), "database", "factories")), `${factoryName}.${outputExt}`);
|
|
1195
|
+
const factoryImportPath = `./${relative(dirname(outputPath), factoryPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`;
|
|
1196
|
+
const stubPath = this.resolveStubPath(outputExt === "js" ? "model.js.stub" : "model.stub");
|
|
1197
|
+
const modelPath = this.generateFile(stubPath, outputPath, {
|
|
1198
|
+
ModelName: modelName,
|
|
1199
|
+
DelegateName: delegateName,
|
|
1200
|
+
FactoryImport: shouldBuildFactory ? `import { ${factoryName} } from '${factoryImportPath}'\n` : "",
|
|
1201
|
+
FactoryLink: shouldBuildFactory ? outputExt === "js" ? `\n static factoryClass = ${factoryName}` : `\n protected static override factoryClass = ${factoryName}` : ""
|
|
1202
|
+
}, options);
|
|
1203
|
+
const prisma = this.ensurePrismaModelEntry(modelName, delegateName);
|
|
1204
|
+
const created = {
|
|
1205
|
+
model: {
|
|
1206
|
+
name: modelName,
|
|
1207
|
+
path: modelPath
|
|
1208
|
+
},
|
|
1209
|
+
prisma,
|
|
1210
|
+
factory: void 0,
|
|
1211
|
+
seeder: void 0,
|
|
1212
|
+
migration: void 0
|
|
1213
|
+
};
|
|
1214
|
+
if (shouldBuildFactory) created.factory = this.makeFactory(baseName, {
|
|
1215
|
+
force: options.force,
|
|
1216
|
+
modelName,
|
|
1217
|
+
modelImportPath: `./${relative(dirname(factoryPath), outputPath).replace(/\\/g, "/").replace(/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i, "")}${outputExt === "js" ? ".js" : ""}`
|
|
1218
|
+
});
|
|
1219
|
+
if (shouldBuildSeeder) created.seeder = this.makeSeeder(baseName, { force: options.force });
|
|
1220
|
+
if (shouldBuildMigration) created.migration = this.makeMigration(`create ${delegateName} table`);
|
|
1221
|
+
return created;
|
|
1222
|
+
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Ensure that the Prisma schema has a model entry for the given model
|
|
1225
|
+
* and delegate names.
|
|
1226
|
+
* If the entry does not exist, it will be created with a default `id` field.
|
|
1227
|
+
*
|
|
1228
|
+
* @param modelName The name of the model to ensure in the Prisma schema.
|
|
1229
|
+
* @param delegateName The name of the delegate (table) to ensure in the Prisma schema.
|
|
1230
|
+
*/
|
|
1231
|
+
ensurePrismaModelEntry(modelName, delegateName) {
|
|
1232
|
+
const schemaPath = join(process.cwd(), "prisma", "schema.prisma");
|
|
1233
|
+
if (!existsSync(schemaPath)) return {
|
|
1234
|
+
path: schemaPath,
|
|
1235
|
+
updated: false
|
|
1236
|
+
};
|
|
1237
|
+
const source = readFileSync(schemaPath, "utf-8");
|
|
1238
|
+
const existingByTable = findModelBlock(source, delegateName);
|
|
1239
|
+
const existingByName = new RegExp(`model\\s+${modelName}\\s*\\{`, "m").test(source);
|
|
1240
|
+
if (existingByTable || existingByName) return {
|
|
1241
|
+
path: schemaPath,
|
|
1242
|
+
updated: false
|
|
1243
|
+
};
|
|
1244
|
+
writeFileSync(schemaPath, applyCreateTableOperation(source, {
|
|
1245
|
+
type: "createTable",
|
|
1246
|
+
table: delegateName,
|
|
1247
|
+
columns: [{
|
|
1248
|
+
name: "id",
|
|
1249
|
+
type: "id",
|
|
1250
|
+
primary: true
|
|
1251
|
+
}]
|
|
1252
|
+
}));
|
|
1253
|
+
return {
|
|
1254
|
+
path: schemaPath,
|
|
1255
|
+
updated: true
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Convert a Prisma scalar type to its corresponding TypeScript type.
|
|
1260
|
+
*
|
|
1261
|
+
* @param value The Prisma scalar type.
|
|
1262
|
+
* @returns The corresponding TypeScript type.
|
|
1263
|
+
*/
|
|
1264
|
+
prismaTypeToTs(value) {
|
|
1265
|
+
if (value === "Int" || value === "Float" || value === "Decimal") return "number";
|
|
1266
|
+
if (value === "BigInt") return "bigint";
|
|
1267
|
+
if (value === "String") return "string";
|
|
1268
|
+
if (value === "Boolean") return "boolean";
|
|
1269
|
+
if (value === "DateTime") return "Date";
|
|
1270
|
+
if (value === "Json") return "Record<string, unknown>";
|
|
1271
|
+
if (value === "Bytes") return "Buffer";
|
|
1272
|
+
return "unknown";
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Parse the Prisma schema to extract model definitions and their fields, focusing
|
|
1276
|
+
* on scalar types.
|
|
1277
|
+
*
|
|
1278
|
+
* @param schema The Prisma schema as a string.
|
|
1279
|
+
* @returns An array of model definitions with their fields.
|
|
1280
|
+
*/
|
|
1281
|
+
parsePrismaModels(schema) {
|
|
1282
|
+
const models = [];
|
|
1283
|
+
const modelRegex = /model\s+(\w+)\s*\{([\s\S]*?)\n\}/g;
|
|
1284
|
+
const scalarTypes = new Set([
|
|
1285
|
+
"Int",
|
|
1286
|
+
"Float",
|
|
1287
|
+
"Decimal",
|
|
1288
|
+
"BigInt",
|
|
1289
|
+
"String",
|
|
1290
|
+
"Boolean",
|
|
1291
|
+
"DateTime",
|
|
1292
|
+
"Json",
|
|
1293
|
+
"Bytes"
|
|
1294
|
+
]);
|
|
1295
|
+
for (const match of schema.matchAll(modelRegex)) {
|
|
1296
|
+
const name = match[1];
|
|
1297
|
+
const body = match[2];
|
|
1298
|
+
const table = body.match(/@@map\("([^"]+)"\)/)?.[1] ?? `${name.charAt(0).toLowerCase()}${name.slice(1)}s`;
|
|
1299
|
+
const fields = [];
|
|
1300
|
+
body.split("\n").forEach((rawLine) => {
|
|
1301
|
+
const line = rawLine.trim();
|
|
1302
|
+
if (!line || line.startsWith("@@") || line.startsWith("//")) return;
|
|
1303
|
+
const fieldMatch = line.match(/^(\w+)\s+([A-Za-z]+)(\?)?\b/);
|
|
1304
|
+
if (!fieldMatch) return;
|
|
1305
|
+
const fieldType = fieldMatch[2];
|
|
1306
|
+
if (!scalarTypes.has(fieldType)) return;
|
|
1307
|
+
fields.push({
|
|
1308
|
+
name: fieldMatch[1],
|
|
1309
|
+
type: this.prismaTypeToTs(fieldType),
|
|
1310
|
+
optional: Boolean(fieldMatch[3])
|
|
1311
|
+
});
|
|
1312
|
+
});
|
|
1313
|
+
models.push({
|
|
1314
|
+
name,
|
|
1315
|
+
table,
|
|
1316
|
+
fields
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return models;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Sync model attribute declarations in a model file based on the
|
|
1323
|
+
* provided declarations.
|
|
1324
|
+
* This method takes the source code of a model file and a list of
|
|
1325
|
+
* attribute declarations,
|
|
1326
|
+
*
|
|
1327
|
+
* @param modelSource The source code of the model file.
|
|
1328
|
+
* @param declarations A list of attribute declarations to sync.
|
|
1329
|
+
* @returns An object containing the updated content and a flag indicating if it was updated.
|
|
1330
|
+
*/
|
|
1331
|
+
syncModelDeclarations(modelSource, declarations) {
|
|
1332
|
+
const lines = modelSource.split("\n");
|
|
1333
|
+
const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
|
|
1334
|
+
if (classIndex < 0) return {
|
|
1335
|
+
content: modelSource,
|
|
1336
|
+
updated: false
|
|
1337
|
+
};
|
|
1338
|
+
let classEndIndex = -1;
|
|
1339
|
+
let depth = 0;
|
|
1340
|
+
for (let index = classIndex; index < lines.length; index += 1) {
|
|
1341
|
+
const line = lines[index];
|
|
1342
|
+
depth += (line.match(/\{/g) || []).length;
|
|
1343
|
+
depth -= (line.match(/\}/g) || []).length;
|
|
1344
|
+
if (depth === 0) {
|
|
1345
|
+
classEndIndex = index;
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (classEndIndex < 0) return {
|
|
1350
|
+
content: modelSource,
|
|
1351
|
+
updated: false
|
|
1352
|
+
};
|
|
1353
|
+
const withoutDeclares = lines.slice(classIndex + 1, classEndIndex).filter((line) => !/^\s*declare\s+\w+\??:\s*[^\n]+$/.test(line));
|
|
1354
|
+
const rebuiltClass = [...declarations.map((declaration) => ` ${declaration}`), ...withoutDeclares];
|
|
1355
|
+
const content = [
|
|
1356
|
+
...lines.slice(0, classIndex + 1),
|
|
1357
|
+
...rebuiltClass,
|
|
1358
|
+
...lines.slice(classEndIndex)
|
|
1359
|
+
].join("\n");
|
|
1360
|
+
return {
|
|
1361
|
+
content,
|
|
1362
|
+
updated: content !== modelSource
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Sync model attribute declarations in model files based on the Prisma schema.
|
|
1367
|
+
* This method reads the Prisma schema to extract model definitions and their
|
|
1368
|
+
* scalar fields, then updates the corresponding model files to include `declare`
|
|
1369
|
+
* statements for these fields. It returns an object containing the paths of the
|
|
1370
|
+
* schema and models, the total number of model files processed, and lists of
|
|
1371
|
+
* updated and skipped files.
|
|
1372
|
+
*
|
|
1373
|
+
* @param options Optional parameters to specify custom paths for the Prisma schema and models directory.
|
|
1374
|
+
* @returns An object with details about the synchronization process, including updated and skipped files.
|
|
1375
|
+
*/
|
|
1376
|
+
syncModelsFromPrisma(options = {}) {
|
|
1377
|
+
const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
|
|
1378
|
+
const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", join(process.cwd(), "src", "models"));
|
|
1379
|
+
if (!existsSync(schemaPath)) throw new Error(`Prisma schema file not found: ${schemaPath}`);
|
|
1380
|
+
if (!existsSync(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
|
|
1381
|
+
const schema = readFileSync(schemaPath, "utf-8");
|
|
1382
|
+
const prismaModels = this.parsePrismaModels(schema);
|
|
1383
|
+
const modelFiles = readdirSync(modelsDir).filter((file) => file.endsWith(".ts"));
|
|
1384
|
+
const updated = [];
|
|
1385
|
+
const skipped = [];
|
|
1386
|
+
modelFiles.forEach((file) => {
|
|
1387
|
+
const filePath = join(modelsDir, file);
|
|
1388
|
+
const source = readFileSync(filePath, "utf-8");
|
|
1389
|
+
const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
|
|
1390
|
+
if (!classMatch) {
|
|
1391
|
+
skipped.push(filePath);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const className = classMatch[1];
|
|
1395
|
+
const delegate = classMatch[2];
|
|
1396
|
+
const prismaModel = prismaModels.find((model) => model.table === delegate) ?? prismaModels.find((model) => model.name === className);
|
|
1397
|
+
if (!prismaModel || prismaModel.fields.length === 0) {
|
|
1398
|
+
skipped.push(filePath);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const declarations = prismaModel.fields.map((field) => `declare ${field.name}${field.optional ? "?" : ""}: ${field.type}`);
|
|
1402
|
+
const synced = this.syncModelDeclarations(source, declarations);
|
|
1403
|
+
if (!synced.updated) {
|
|
1404
|
+
skipped.push(filePath);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
writeFileSync(filePath, synced.content);
|
|
1408
|
+
updated.push(filePath);
|
|
1409
|
+
});
|
|
1410
|
+
return {
|
|
1411
|
+
schemaPath,
|
|
1412
|
+
modelsDir,
|
|
1413
|
+
total: modelFiles.length,
|
|
1414
|
+
updated,
|
|
1415
|
+
skipped
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/cli/commands/InitCommand.ts
|
|
1422
|
+
/**
|
|
1423
|
+
* The InitCommand class implements the CLI command for initializing Arkormˣ by creating
|
|
1424
|
+
* a default config file in the current directory.
|
|
1425
|
+
*
|
|
1426
|
+
* @author Legacy (3m1n3nc3)
|
|
1427
|
+
* @since 0.1.0
|
|
1428
|
+
*/
|
|
1429
|
+
var InitCommand = class extends Command {
|
|
1430
|
+
signature = `init
|
|
14
1431
|
{--force : Force overwrite if config file already exists (existing file will be backed up) }
|
|
15
|
-
`;
|
|
1432
|
+
`;
|
|
1433
|
+
description = "Initialize Arkormˣ by creating a default config file in the current directory";
|
|
1434
|
+
/**
|
|
1435
|
+
* Command handler for the init command.
|
|
1436
|
+
*/
|
|
1437
|
+
async handle() {
|
|
1438
|
+
this.app.command = this;
|
|
1439
|
+
const outputDir = join$1(process.cwd(), "arkormx.config.js");
|
|
1440
|
+
const { stubs } = getUserConfig("paths") ?? {};
|
|
1441
|
+
const stubsDir = typeof stubs === "string" && stubs.trim().length > 0 ? stubs : getDefaultStubsPath();
|
|
1442
|
+
const preferredStubPath = join$1(stubsDir, "arkormx.config.stub");
|
|
1443
|
+
const legacyStubPath = join$1(stubsDir, "arkorm.config.stub");
|
|
1444
|
+
const stubPath = existsSync(preferredStubPath) ? preferredStubPath : legacyStubPath;
|
|
1445
|
+
if (existsSync(outputDir) && !this.option("force")) {
|
|
1446
|
+
this.error("Error: Arkormˣ has already been initialized. Use --force to reinitialize.");
|
|
1447
|
+
process.exit(1);
|
|
1448
|
+
}
|
|
1449
|
+
this.app.ensureDirectory(outputDir);
|
|
1450
|
+
if (existsSync(outputDir) && this.option("force")) copyFileSync(outputDir, outputDir.replace(/\.js$/, `.backup.${Date.now()}.js`));
|
|
1451
|
+
if (!existsSync(stubPath)) {
|
|
1452
|
+
this.error(`Error: Missing config stub at ${preferredStubPath} (or ${legacyStubPath})`);
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
writeFileSync(outputDir, readFileSync(stubPath, "utf-8"));
|
|
1456
|
+
this.success("Arkormˣ initialized successfully!");
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/cli/commands/MakeFactoryCommand.ts
|
|
1462
|
+
/**
|
|
1463
|
+
* The MakeFactoryCommand class implements the CLI command for creating new factory classes.
|
|
1464
|
+
*
|
|
1465
|
+
* @author Legacy (3m1n3nc3)
|
|
1466
|
+
* @since 0.1.0
|
|
1467
|
+
*/
|
|
1468
|
+
var MakeFactoryCommand = class extends Command {
|
|
1469
|
+
signature = `make:factory
|
|
16
1470
|
{name : Name of the factory to create}
|
|
17
1471
|
{--f|force : Overwrite existing file}
|
|
18
|
-
`;
|
|
1472
|
+
`;
|
|
1473
|
+
description = "Create a new model factory class";
|
|
1474
|
+
/**
|
|
1475
|
+
* Command handler for the make:factory command.
|
|
1476
|
+
*
|
|
1477
|
+
* @returns
|
|
1478
|
+
*/
|
|
1479
|
+
async handle() {
|
|
1480
|
+
this.app.command = this;
|
|
1481
|
+
const name = this.argument("name");
|
|
1482
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1483
|
+
const created = this.app.makeFactory(name, { force: this.option("force") });
|
|
1484
|
+
this.success(`Created factory: ${this.app.formatPathForLog(created.path)}`);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1488
|
+
//#endregion
|
|
1489
|
+
//#region src/cli/commands/MakeMigrationCommand.ts
|
|
1490
|
+
/**
|
|
1491
|
+
* The MakeMigrationCommand class implements the CLI command for creating new migration classes.
|
|
1492
|
+
*
|
|
1493
|
+
* @author Legacy (3m1n3nc3)
|
|
1494
|
+
* @since 0.1.0
|
|
1495
|
+
*/
|
|
1496
|
+
var MakeMigrationCommand = class extends Command {
|
|
1497
|
+
signature = `make:migration
|
|
19
1498
|
{name : Name of the migration to create}
|
|
20
|
-
`;
|
|
1499
|
+
`;
|
|
1500
|
+
description = "Create a new migration class file";
|
|
1501
|
+
/**
|
|
1502
|
+
* Command handler for the make:migration command.
|
|
1503
|
+
*
|
|
1504
|
+
* @returns
|
|
1505
|
+
*/
|
|
1506
|
+
async handle() {
|
|
1507
|
+
this.app.command = this;
|
|
1508
|
+
const name = this.argument("name");
|
|
1509
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1510
|
+
const created = this.app.makeMigration(name);
|
|
1511
|
+
this.success(`Created migration: ${this.app.formatPathForLog(created.path)}`);
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
//#endregion
|
|
1516
|
+
//#region src/cli/commands/MakeModelCommand.ts
|
|
1517
|
+
/**
|
|
1518
|
+
* The MakeModelCommand class implements the CLI command for creating new model
|
|
1519
|
+
* classes along with optional linked resources such as factories, seeders, and migrations.
|
|
1520
|
+
*
|
|
1521
|
+
* @author Legacy (3m1n3nc3)
|
|
1522
|
+
* @since 0.1.0
|
|
1523
|
+
*/
|
|
1524
|
+
var MakeModelCommand = class extends Command {
|
|
1525
|
+
signature = `make:model
|
|
21
1526
|
{name : Name of the model to create}
|
|
22
1527
|
{--f|force : Overwrite existing files}
|
|
23
1528
|
{--factory : Create and link a factory}
|
|
24
1529
|
{--seeder : Create a seeder}
|
|
25
1530
|
{--migration : Create a migration}
|
|
26
1531
|
{--all : Create and link factory, seeder, and migration}
|
|
27
|
-
`;
|
|
1532
|
+
`;
|
|
1533
|
+
description = "Create a new model and optional linked resources";
|
|
1534
|
+
/**
|
|
1535
|
+
* Command handler for the make:model command.
|
|
1536
|
+
*
|
|
1537
|
+
* @returns
|
|
1538
|
+
*/
|
|
1539
|
+
async handle() {
|
|
1540
|
+
this.app.command = this;
|
|
1541
|
+
const name = this.argument("name");
|
|
1542
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1543
|
+
const created = this.app.makeModel(name, this.options());
|
|
1544
|
+
this.success("Created files:");
|
|
1545
|
+
[
|
|
1546
|
+
["Model", created.model.path],
|
|
1547
|
+
[`Prisma schema ${created.prisma.updated ? "(updated)" : "(already up to date)"}`, created.prisma.path],
|
|
1548
|
+
created.factory ? ["Factory", created.factory.path] : "",
|
|
1549
|
+
created.seeder ? ["Seeder", created.seeder.path] : "",
|
|
1550
|
+
created.migration ? ["Migration", created.migration.path] : ""
|
|
1551
|
+
].filter(Boolean).map(([name, path]) => this.success(this.app.splitLogger(name, path)));
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
//#endregion
|
|
1556
|
+
//#region src/cli/commands/MakeSeederCommand.ts
|
|
1557
|
+
/**
|
|
1558
|
+
* The MakeSeederCommand class implements the CLI command for creating new seeder classes.
|
|
1559
|
+
*
|
|
1560
|
+
* @author Legacy (3m1n3nc3)
|
|
1561
|
+
* @since 0.1.0
|
|
1562
|
+
*/
|
|
1563
|
+
var MakeSeederCommand = class extends Command {
|
|
1564
|
+
signature = `make:seeder
|
|
28
1565
|
{name : Name of the seeder to create}
|
|
29
1566
|
{--f|force : Overwrite existing file}
|
|
30
|
-
`;
|
|
1567
|
+
`;
|
|
1568
|
+
description = "Create a new seeder class";
|
|
1569
|
+
/**
|
|
1570
|
+
* Command handler for the make:seeder command.
|
|
1571
|
+
*/
|
|
1572
|
+
async handle() {
|
|
1573
|
+
this.app.command = this;
|
|
1574
|
+
const name = this.argument("name");
|
|
1575
|
+
if (!name) return void this.error("Error: Name argument is required.");
|
|
1576
|
+
const created = this.app.makeSeeder(name, this.options());
|
|
1577
|
+
this.success(`Created seeder: ${this.app.formatPathForLog(created.path)}`);
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/cli/commands/MigrateCommand.ts
|
|
1583
|
+
/**
|
|
1584
|
+
* The MigrateCommand class implements the CLI command for applying migration
|
|
1585
|
+
* classes to the Prisma schema and running the Prisma workflow.
|
|
1586
|
+
*
|
|
1587
|
+
* @author Legacy (3m1n3nc3)
|
|
1588
|
+
* @since 0.1.0
|
|
1589
|
+
*/
|
|
1590
|
+
var MigrateCommand = class extends Command {
|
|
1591
|
+
signature = `migrate
|
|
31
1592
|
{name? : Migration class or file name}
|
|
32
1593
|
{--all : Run all migrations from the configured migrations directory}
|
|
33
1594
|
{--deploy : Use prisma migrate deploy instead of migrate dev}
|
|
@@ -35,17 +1596,266 @@ import{copyFileSync as e,existsSync as t,mkdirSync as n,readFileSync as r,readdi
|
|
|
35
1596
|
{--skip-migrate : Skip prisma migrate command}
|
|
36
1597
|
{--schema= : Explicit prisma schema path}
|
|
37
1598
|
{--migration-name= : Name for prisma migrate dev}
|
|
38
|
-
`;
|
|
1599
|
+
`;
|
|
1600
|
+
description = "Apply migration classes to schema.prisma and run Prisma workflow";
|
|
1601
|
+
/**
|
|
1602
|
+
* Command handler for the migrate command.
|
|
1603
|
+
* This method is responsible for orchestrating the migration
|
|
1604
|
+
* process, including loading migration classes, applying them to
|
|
1605
|
+
* the Prisma schema, and running the appropriate Prisma commands
|
|
1606
|
+
* based on the provided options.
|
|
1607
|
+
*
|
|
1608
|
+
* @returns
|
|
1609
|
+
*/
|
|
1610
|
+
async handle() {
|
|
1611
|
+
this.app.command = this;
|
|
1612
|
+
const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? join$1(process.cwd(), "database", "migrations");
|
|
1613
|
+
const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
|
|
1614
|
+
if (!existsSync$1(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
|
|
1615
|
+
const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join$1(process.cwd(), "prisma", "schema.prisma");
|
|
1616
|
+
const classes = this.option("all") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
|
|
1617
|
+
if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
|
|
1618
|
+
for (const [MigrationClassItem] of classes) await applyMigrationToPrismaSchema(MigrationClassItem, {
|
|
1619
|
+
schemaPath,
|
|
1620
|
+
write: true
|
|
1621
|
+
});
|
|
1622
|
+
if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
|
|
1623
|
+
if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
|
|
1624
|
+
else runPrismaCommand([
|
|
1625
|
+
"migrate",
|
|
1626
|
+
"dev",
|
|
1627
|
+
"--name",
|
|
1628
|
+
this.option("migration-name") ? String(this.option("migration-name")) : `arkorm_cli_${Date.now()}`
|
|
1629
|
+
], process.cwd());
|
|
1630
|
+
this.success(`Applied ${classes.length} migration(s).`);
|
|
1631
|
+
classes.forEach(([_, file]) => this.success(this.app.splitLogger("Migrated", file)));
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Load all migration classes from the specified directory.
|
|
1635
|
+
*
|
|
1636
|
+
* @param migrationsDir The directory to load migration classes from.
|
|
1637
|
+
*/
|
|
1638
|
+
async loadAllMigrations(migrationsDir) {
|
|
1639
|
+
const files = readdirSync$1(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath(join$1(migrationsDir, file)));
|
|
1640
|
+
return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Load migration classes from a specific file or by class name.
|
|
1644
|
+
*
|
|
1645
|
+
* @param migrationsDir
|
|
1646
|
+
* @param name
|
|
1647
|
+
* @returns
|
|
1648
|
+
*/
|
|
1649
|
+
async loadNamedMigration(migrationsDir, name) {
|
|
1650
|
+
if (!name) return [[void 0, ""]];
|
|
1651
|
+
const base = name.replace(/Migration$/, "");
|
|
1652
|
+
const target = [
|
|
1653
|
+
`${name}.ts`,
|
|
1654
|
+
`${name}.js`,
|
|
1655
|
+
`${name}.mjs`,
|
|
1656
|
+
`${name}.cjs`,
|
|
1657
|
+
`${base}Migration.ts`,
|
|
1658
|
+
`${base}Migration.js`,
|
|
1659
|
+
`${base}Migration.mjs`,
|
|
1660
|
+
`${base}Migration.cjs`
|
|
1661
|
+
].map((file) => join$1(migrationsDir, file)).find((file) => existsSync$1(file));
|
|
1662
|
+
if (!target) return [[void 0, name]];
|
|
1663
|
+
const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
|
|
1664
|
+
return (await this.loadMigrationClassesFromFile(runtimeTarget)).map((cls) => [cls, runtimeTarget]);
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Load migration classes from a given file path.
|
|
1668
|
+
*
|
|
1669
|
+
* @param filePath
|
|
1670
|
+
* @returns
|
|
1671
|
+
*/
|
|
1672
|
+
async loadMigrationClassesFromFile(filePath) {
|
|
1673
|
+
const imported = await import(`${pathToFileURL$1(resolve(filePath)).href}?arkorm_migrate=${Date.now()}`);
|
|
1674
|
+
return Object.values(imported).filter((value) => {
|
|
1675
|
+
if (typeof value !== "function") return false;
|
|
1676
|
+
return value.prototype instanceof Migration;
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
//#endregion
|
|
1682
|
+
//#region src/cli/commands/ModelsSyncCommand.ts
|
|
1683
|
+
var ModelsSyncCommand = class extends Command {
|
|
1684
|
+
signature = `models:sync
|
|
39
1685
|
{--schema= : Path to prisma schema file}
|
|
40
1686
|
{--models= : Path to models directory}
|
|
41
|
-
`;
|
|
1687
|
+
`;
|
|
1688
|
+
description = "Sync model declare attributes from prisma schema for all model files";
|
|
1689
|
+
async handle() {
|
|
1690
|
+
this.app.command = this;
|
|
1691
|
+
const result = this.app.syncModelsFromPrisma({
|
|
1692
|
+
schemaPath: this.option("schema") ? resolve(String(this.option("schema"))) : void 0,
|
|
1693
|
+
modelsDir: this.option("models") ? resolve(String(this.option("models"))) : void 0
|
|
1694
|
+
});
|
|
1695
|
+
const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
|
|
1696
|
+
this.success("SUCCESS: Model sync completed with the following results:");
|
|
1697
|
+
[
|
|
1698
|
+
this.app.splitLogger("Schema", result.schemaPath),
|
|
1699
|
+
this.app.splitLogger("Models", result.modelsDir),
|
|
1700
|
+
this.app.splitLogger("Processed", String(result.total)),
|
|
1701
|
+
...updatedLines,
|
|
1702
|
+
this.app.splitLogger("Skipped", String(result.skipped.length))
|
|
1703
|
+
].map((line) => this.success(line));
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/database/Seeder.ts
|
|
1709
|
+
/**
|
|
1710
|
+
* The Seeder class serves as a base for defining database seeders, which are
|
|
1711
|
+
* used to populate the database with initial or test data.
|
|
1712
|
+
*
|
|
1713
|
+
* @author Legacy (3m1n3nc3)
|
|
1714
|
+
* @since 0.1.0
|
|
1715
|
+
*/
|
|
1716
|
+
var Seeder = class Seeder {
|
|
1717
|
+
/**
|
|
1718
|
+
* Runs one or more seeders.
|
|
1719
|
+
*
|
|
1720
|
+
* @param seeders The seeders to be run.
|
|
1721
|
+
*/
|
|
1722
|
+
async call(...seeders) {
|
|
1723
|
+
await Seeder.runSeeders(...seeders);
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Converts a SeederInput into a Seeder instance.
|
|
1727
|
+
*
|
|
1728
|
+
* @param input The SeederInput to convert.
|
|
1729
|
+
* @returns A Seeder instance.
|
|
1730
|
+
*/
|
|
1731
|
+
static toSeederInstance(input) {
|
|
1732
|
+
if (input instanceof Seeder) return input;
|
|
1733
|
+
return new input();
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Runs the given seeders in sequence.
|
|
1737
|
+
*
|
|
1738
|
+
* @param seeders The seeders to be run.
|
|
1739
|
+
*/
|
|
1740
|
+
static async runSeeders(...seeders) {
|
|
1741
|
+
const queue = seeders.reduce((all, current) => {
|
|
1742
|
+
if (Array.isArray(current)) return [...all, ...current];
|
|
1743
|
+
all.push(current);
|
|
1744
|
+
return all;
|
|
1745
|
+
}, []);
|
|
1746
|
+
for (const seeder of queue) await this.toSeederInstance(seeder).run();
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
//#endregion
|
|
1751
|
+
//#region src/cli/commands/SeedCommand.ts
|
|
1752
|
+
/**
|
|
1753
|
+
* The SeedCommand class implements the CLI command for running seeder classes.
|
|
1754
|
+
*
|
|
1755
|
+
* @author Legacy (3m1n3nc3)
|
|
1756
|
+
* @since 0.1.0
|
|
1757
|
+
*/
|
|
1758
|
+
var SeedCommand = class extends Command {
|
|
1759
|
+
signature = `seed
|
|
42
1760
|
{name? : Seeder class or file name}
|
|
43
1761
|
{--all : Run all seeders in the configured seeders directory}
|
|
44
|
-
`;
|
|
1762
|
+
`;
|
|
1763
|
+
description = "Run one or more seeders";
|
|
1764
|
+
/**
|
|
1765
|
+
* Command handler for the seed command.
|
|
1766
|
+
*
|
|
1767
|
+
* @returns
|
|
1768
|
+
*/
|
|
1769
|
+
async handle() {
|
|
1770
|
+
this.app.command = this;
|
|
1771
|
+
const configuredSeedersDir = this.app.getConfig("paths")?.seeders ?? join$1(process.cwd(), "database", "seeders");
|
|
1772
|
+
const seedersDir = this.app.resolveRuntimeDirectoryPath(configuredSeedersDir);
|
|
1773
|
+
if (!existsSync$1(seedersDir)) return void this.error(`ERROR: Seeders directory not found: ${this.app.formatPathForLog(configuredSeedersDir)}`);
|
|
1774
|
+
const classes = this.option("all") ? await this.loadAllSeeders(seedersDir) : await this.loadNamedSeeder(seedersDir, this.argument("name") ?? "DatabaseSeeder");
|
|
1775
|
+
if (classes.length === 0) return void this.error("ERROR: No seeder classes found to run.");
|
|
1776
|
+
for (const SeederClassItem of classes) await new SeederClassItem().run();
|
|
1777
|
+
this.success("Database seeding completed");
|
|
1778
|
+
classes.forEach((cls) => this.success(this.app.splitLogger("Seeded", cls.name)));
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Load all seeder classes from the specified directory.
|
|
1782
|
+
*
|
|
1783
|
+
* @param seedersDir
|
|
1784
|
+
* @returns
|
|
1785
|
+
*/
|
|
1786
|
+
async loadAllSeeders(seedersDir) {
|
|
1787
|
+
const files = readdirSync$1(seedersDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).map((file) => this.app.resolveRuntimeScriptPath(join$1(seedersDir, file)));
|
|
1788
|
+
return (await Promise.all(files.map(async (file) => await this.loadSeederClassesFromFile(file)))).flat();
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Load seeder classes from a specific file or by class name.
|
|
1792
|
+
*
|
|
1793
|
+
* @param seedersDir
|
|
1794
|
+
* @param name
|
|
1795
|
+
* @returns
|
|
1796
|
+
*/
|
|
1797
|
+
async loadNamedSeeder(seedersDir, name) {
|
|
1798
|
+
const base = name.replace(/Seeder$/, "");
|
|
1799
|
+
const target = [
|
|
1800
|
+
`${name}.ts`,
|
|
1801
|
+
`${name}.js`,
|
|
1802
|
+
`${name}.mjs`,
|
|
1803
|
+
`${name}.cjs`,
|
|
1804
|
+
`${base}Seeder.ts`,
|
|
1805
|
+
`${base}Seeder.js`,
|
|
1806
|
+
`${base}Seeder.mjs`,
|
|
1807
|
+
`${base}Seeder.cjs`
|
|
1808
|
+
].map((file) => join$1(seedersDir, file)).find((file) => existsSync$1(file));
|
|
1809
|
+
if (!target) return [];
|
|
1810
|
+
const runtimeTarget = this.app.resolveRuntimeScriptPath(target);
|
|
1811
|
+
return await this.loadSeederClassesFromFile(runtimeTarget);
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Load seeder classes from a given file path.
|
|
1815
|
+
*
|
|
1816
|
+
* @param filePath The path to the file containing seeder classes.
|
|
1817
|
+
* @returns An array of seeder classes.
|
|
1818
|
+
*/
|
|
1819
|
+
async loadSeederClassesFromFile(filePath) {
|
|
1820
|
+
const imported = await import(`${pathToFileURL$1(resolve(filePath)).href}?arkorm_seed=${Date.now()}`);
|
|
1821
|
+
return Object.values(imported).filter((value) => {
|
|
1822
|
+
if (typeof value !== "function") return false;
|
|
1823
|
+
return value.prototype instanceof Seeder;
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/cli/logo.ts
|
|
1830
|
+
var logo_default = String.raw`
|
|
45
1831
|
__/^^^^^^^^^^^^^^^^\__
|
|
46
1832
|
▄▄▄/ \▄▄
|
|
47
1833
|
▄██▀▀██▄ ▄▄
|
|
48
1834
|
███ ███ ████▄ ██ ▄█▀ ▄███▄ ████▄ ███▄███▄
|
|
49
1835
|
███▀▀███ ██ ▀▀ ████ ██ ██ ██ ▀▀ ██ ██ ██
|
|
50
1836
|
███ ███ ██ ██ ▀█▄ ▀███▀ ██ ██ ██ ██
|
|
51
|
-
`;
|
|
1837
|
+
`;
|
|
1838
|
+
|
|
1839
|
+
//#endregion
|
|
1840
|
+
//#region src/cli/index.ts
|
|
1841
|
+
const app = new CliApp();
|
|
1842
|
+
await Kernel.init(app, {
|
|
1843
|
+
logo: logo_default,
|
|
1844
|
+
name: "Arkormˣ CLI",
|
|
1845
|
+
baseCommands: [
|
|
1846
|
+
InitCommand,
|
|
1847
|
+
MakeModelCommand,
|
|
1848
|
+
MakeFactoryCommand,
|
|
1849
|
+
MakeSeederCommand,
|
|
1850
|
+
MakeMigrationCommand,
|
|
1851
|
+
ModelsSyncCommand,
|
|
1852
|
+
SeedCommand,
|
|
1853
|
+
MigrateCommand
|
|
1854
|
+
],
|
|
1855
|
+
exceptionHandler(exception) {
|
|
1856
|
+
throw exception;
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
//#endregion
|
|
1861
|
+
export { };
|