@stacksjs/database 0.70.87 → 0.70.90

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.
Files changed (46) hide show
  1. package/dist/auth-tables.js +220 -0
  2. package/dist/class-seeder.js +116 -0
  3. package/dist/column.d.ts +17 -0
  4. package/dist/column.js +26 -0
  5. package/dist/custom/audits.js +57 -0
  6. package/dist/custom/errors.js +48 -0
  7. package/dist/custom/index.js +3 -0
  8. package/dist/custom/jobs.js +449 -0
  9. package/dist/database.js +178 -0
  10. package/dist/defaults.js +48 -0
  11. package/dist/driver-config.js +144 -0
  12. package/dist/drivers/defaults/index.js +2 -0
  13. package/dist/drivers/defaults/passwords.js +106 -0
  14. package/dist/drivers/defaults/traits.js +1125 -0
  15. package/dist/drivers/dynamodb.js +607 -0
  16. package/dist/drivers/helpers.js +206 -0
  17. package/dist/drivers/index.js +9 -0
  18. package/dist/drivers/mysql.js +322 -0
  19. package/dist/drivers/postgres.js +411 -0
  20. package/dist/drivers/sqlite.js +397 -0
  21. package/dist/factory.js +51 -0
  22. package/dist/fk-audit.js +181 -0
  23. package/dist/index.js +55 -1263
  24. package/dist/migration-lock.js +143 -0
  25. package/dist/migrations.js +528 -0
  26. package/dist/notification-tables.js +54 -0
  27. package/dist/query-logger.js +213 -0
  28. package/dist/query-parser.js +93 -0
  29. package/dist/rbac-tables.js +84 -0
  30. package/dist/safe-migrations.js +59 -0
  31. package/dist/schema.d.ts +4 -0
  32. package/dist/schema.js +10 -0
  33. package/dist/seed-scaffold.js +144 -0
  34. package/dist/seeder.js +363 -0
  35. package/dist/sql-helpers.js +24 -0
  36. package/dist/table.d.ts +7 -0
  37. package/dist/table.js +26 -0
  38. package/dist/tools/setup.d.ts +1 -0
  39. package/dist/tools/setup.js +6 -0
  40. package/dist/transaction-context.js +62 -0
  41. package/dist/types.js +23 -0
  42. package/dist/unique-audit.js +174 -0
  43. package/dist/utils.js +163 -0
  44. package/dist/uuid-columns.js +68 -0
  45. package/dist/validators.js +122 -0
  46. package/package.json +11 -11
@@ -0,0 +1,397 @@
1
+ import { log } from "@stacksjs/logging";
2
+ function italic(str) {
3
+ return `\x1B[3m${str}\x1B[23m`;
4
+ }
5
+ import { app } from "@stacksjs/config";
6
+ import { db, SQLITE_BOOTSTRAP_PRAGMAS } from "../utils";
7
+ import { ok } from "@stacksjs/error-handling";
8
+ import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
9
+ import { path } from "@stacksjs/path";
10
+ import { fs, globSync } from "@stacksjs/storage";
11
+ import { snakeCase } from "@stacksjs/strings";
12
+ import {
13
+ arrangeColumns,
14
+ checkPivotMigration,
15
+ deleteFrameworkModels,
16
+ deleteMigrationFiles,
17
+ fetchTables,
18
+ findDifferingKeys,
19
+ getLastMigrationFields,
20
+ getLikeableForeignKey,
21
+ getUpvoteTableName,
22
+ isArrayEqual,
23
+ mapFieldTypeToColumnType,
24
+ pluckChanges
25
+ } from "./helpers";
26
+ import { dropCommonTables } from "./defaults/traits";
27
+ export async function resetSqliteDatabase() {
28
+ await deleteFrameworkModels();
29
+ await deleteMigrationFiles();
30
+ await dropSqliteTables();
31
+ return ok("All tables dropped successfully!");
32
+ }
33
+ export async function configureSqlitePragmas() {
34
+ try {
35
+ for (const pragma of SQLITE_BOOTSTRAP_PRAGMAS)
36
+ await db.unsafe(pragma).execute();
37
+ } catch (err) {
38
+ log.debug(`[sqlite] Failed to apply pragmas: ${err.message}`);
39
+ }
40
+ }
41
+ export async function dropSqliteTables() {
42
+ const userModelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 }), tables = await fetchTables(), safeName = /^[a-z_][\w]*$/i;
43
+ await db.unsafe("PRAGMA foreign_keys = OFF").execute();
44
+ try {
45
+ for (const table of tables) {
46
+ if (!safeName.test(table))
47
+ throw Error(`[sqlite] Refusing to drop table with unsafe name: ${table}`);
48
+ await db.unsafe(`DROP TABLE IF EXISTS "${table}"`).execute();
49
+ }
50
+ await db.unsafe('DROP TABLE IF EXISTS "migrations"').execute();
51
+ await dropCommonTables();
52
+ for (const userModel of userModelFiles) {
53
+ const userModelPath = (await import(userModel)).default, pivotTables = await getPivotTables(userModelPath, userModel);
54
+ for (const pivotTable of pivotTables) {
55
+ if (!safeName.test(pivotTable.table))
56
+ throw Error(`[sqlite] Refusing to drop pivot table with unsafe name: ${pivotTable.table}`);
57
+ await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}"`).execute();
58
+ }
59
+ }
60
+ } finally {
61
+ await db.unsafe("PRAGMA foreign_keys = ON").execute();
62
+ }
63
+ }
64
+ export function fetchSqliteFile() {
65
+ if (app.env === "testing")
66
+ return fetchTestSqliteFile();
67
+ return path.userDatabasePath("stacks.sqlite");
68
+ }
69
+ export function fetchTestSqliteFile() {
70
+ return path.userDatabasePath("stacks_testing.sqlite");
71
+ }
72
+ export async function generateSqliteMigration(modelPath) {
73
+ const model = (await import(modelPath)).default, fileName = path.basename(modelPath), tableName = await getTableName(model, modelPath), fieldsString = JSON.stringify(model.attributes, null, 2), copiedModelPath = path.frameworkPath(`cache/models/${fileName}`);
74
+ let haveFieldsChanged = !1;
75
+ if (fs.existsSync(copiedModelPath)) {
76
+ log.debug(`Fields have already been generated for ${tableName}`);
77
+ const previousFields = await getLastMigrationFields(fileName);
78
+ if (JSON.stringify(previousFields, null, 2) === fieldsString) {
79
+ log.debug(`Fields have not changed for ${tableName}`);
80
+ return;
81
+ }
82
+ haveFieldsChanged = !0;
83
+ log.debug(`Fields have changed for ${tableName}`);
84
+ } else
85
+ log.debug(`Fields have not been generated for ${tableName}`);
86
+ await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`;
87
+ const hasBeenMigrated = !1;
88
+ log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);
89
+ if (haveFieldsChanged)
90
+ await createAlterTableMigration(modelPath);
91
+ else
92
+ await createTableMigration(modelPath);
93
+ }
94
+ export async function copyModelFiles(modelPath) {
95
+ const model = (await import(modelPath)).default, fileName = path.basename(modelPath), tableName = await getTableName(model, modelPath), fieldsString = JSON.stringify(model.attributes, null, 2), copiedModelPath = path.frameworkPath(`cache/models/${fileName}`);
96
+ if (fs.existsSync(copiedModelPath)) {
97
+ log.debug(`Fields have already been generated for ${tableName}`);
98
+ const previousFields = await getLastMigrationFields(fileName);
99
+ if (JSON.stringify(previousFields, null, 2) === fieldsString) {
100
+ log.debug(`Fields have not changed for ${tableName}`);
101
+ return;
102
+ }
103
+ }
104
+ await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`;
105
+ }
106
+ async function createTableMigration(modelPath) {
107
+ log.debug("createTableMigration modelPath:", modelPath);
108
+ const model = (await import(modelPath)).default, tableName = getTableName(model, modelPath), modelName = getModelName(model, modelPath), twoFactorEnabled = model.traits?.useAuth && typeof model.traits.useAuth !== "boolean" ? model.traits.useAuth.useTwoFactor : !1;
109
+ await createPivotTableMigration(model, modelPath);
110
+ const otherModelRelations = await fetchOtherModelRelations(modelName), useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? !0, useSocials = model?.traits?.useSocials && Array.isArray(model.traits.useSocials) && model.traits.useSocials.length > 0, useLikeable = Array.isArray(model?.traits?.likeable) ? model.traits.likeable.length > 0 : Boolean(model?.traits?.likeable), useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? !1, usePasskey = (typeof model.traits?.useAuth === "object" && model.traits.useAuth.usePasskey) ?? !1, useBillable = model.traits?.billable || !1, useUuid = model.traits?.useUuid || !1;
111
+ let migrationContent = `import type { Database } from '@stacksjs/database'
112
+ `;
113
+ migrationContent += `import { sql } from '@stacksjs/database'
114
+
115
+ `;
116
+ migrationContent += `export async function up(db: Database<any>) {
117
+ `;
118
+ migrationContent += ` await (db as any).schema
119
+ `;
120
+ migrationContent += ` .createTable('${tableName}')
121
+ `;
122
+ migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
123
+ `;
124
+ if (useUuid)
125
+ migrationContent += ` .addColumn('uuid', 'text')
126
+ `;
127
+ if (useSocials) {
128
+ const socials = model.traits?.useSocials || [];
129
+ if (socials.includes("google"))
130
+ migrationContent += ` .addColumn('google_id', 'text')
131
+ `;
132
+ if (socials.includes("github"))
133
+ migrationContent += ` .addColumn('github_id', 'text')
134
+ `;
135
+ if (socials.includes("apple"))
136
+ migrationContent += ` .addColumn('apple_id', 'text')
137
+ `;
138
+ if (socials.includes("twitter"))
139
+ migrationContent += ` .addColumn('twitter_id', 'text')
140
+ `;
141
+ if (socials.includes("facebook"))
142
+ migrationContent += ` .addColumn('facebook_id', 'text')
143
+ `;
144
+ }
145
+ for (const [fieldName, options] of arrangeColumns(model.attributes)) {
146
+ const fieldOptions = options, fieldNameFormatted = snakeCase(fieldName), columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule, "sqlite");
147
+ migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`;
148
+ const isRequired = fieldOptions.validation?.rule.isRequired;
149
+ if (isRequired || fieldOptions.unique || fieldOptions.default !== void 0) {
150
+ migrationContent += ", col => col";
151
+ if (isRequired)
152
+ migrationContent += ".notNull()";
153
+ if (fieldOptions.unique)
154
+ migrationContent += ".unique()";
155
+ if (fieldOptions.default !== void 0)
156
+ if (typeof fieldOptions.default === "string")
157
+ migrationContent += `.defaultTo('${fieldOptions.default}')`;
158
+ else if (fieldOptions.default === null)
159
+ migrationContent += ".defaultTo(null)";
160
+ else
161
+ migrationContent += `.defaultTo(${fieldOptions.default})`;
162
+ migrationContent += "";
163
+ }
164
+ migrationContent += `)
165
+ `;
166
+ }
167
+ if (twoFactorEnabled !== !1 && twoFactorEnabled)
168
+ migrationContent += ` .addColumn('two_factor_secret', 'text')
169
+ `;
170
+ if (useBillable)
171
+ migrationContent += ` .addColumn('stripe_id', 'text')
172
+ `;
173
+ if (useSoftDeletes)
174
+ migrationContent += ` .addColumn('deleted_at', 'timestamp')
175
+ `;
176
+ if (usePasskey)
177
+ migrationContent += ` .addColumn('public_passkey', 'text')
178
+ `;
179
+ if (otherModelRelations?.length)
180
+ for (const modelRelation of otherModelRelations) {
181
+ if (!modelRelation.foreignKey)
182
+ continue;
183
+ migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
184
+ col.references('${modelRelation.relationTable}.id').onDelete('cascade')
185
+ )
186
+ `;
187
+ }
188
+ if (useTimestamps) {
189
+ migrationContent += " .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";
190
+ migrationContent += ` .addColumn('updated_at', 'timestamp')
191
+ `;
192
+ }
193
+ migrationContent += ` .execute()
194
+
195
+ `;
196
+ if (otherModelRelations?.length)
197
+ for (const modelRelation of otherModelRelations) {
198
+ if (!modelRelation.foreignKey)
199
+ continue;
200
+ migrationContent += generateForeignKeyIndexSQL(tableName, modelRelation.foreignKey);
201
+ }
202
+ if (model.indexes?.length) {
203
+ migrationContent += `
204
+ `;
205
+ for (const index of model.indexes)
206
+ migrationContent += generateIndexCreationSQL(tableName, index);
207
+ }
208
+ migrationContent += generatePrimaryKeyIndexSQL(tableName);
209
+ if (useLikeable) {
210
+ const upvoteTable = getUpvoteTableName(model, tableName);
211
+ if (upvoteTable) {
212
+ const foreignKey = getLikeableForeignKey(model, tableName);
213
+ migrationContent += `
214
+ // Create upvote table
215
+ `;
216
+ migrationContent += ` await (db as any).schema
217
+ `;
218
+ migrationContent += ` .createTable('${upvoteTable}')
219
+ `;
220
+ migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
221
+ `;
222
+ migrationContent += ` .addColumn('${foreignKey}', 'integer', col => col.notNull())
223
+ `;
224
+ migrationContent += ` .addColumn('user_id', 'integer', col => col.notNull())
225
+ `;
226
+ migrationContent += " .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";
227
+ migrationContent += ` .addColumn('updated_at', 'timestamp')
228
+ `;
229
+ migrationContent += ` .execute()
230
+
231
+ `;
232
+ migrationContent += ` // Add indexes for upvote table
233
+ `;
234
+ migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_${foreignKey}_index').on('${upvoteTable}').column('${foreignKey}').execute()
235
+ `;
236
+ migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_user_${foreignKey}_unique').on('${upvoteTable}').columns(['user_id', '${foreignKey}']).unique().execute()
237
+ `;
238
+ migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_id_index').on('${upvoteTable}').column('id').execute()
239
+ `;
240
+ }
241
+ }
242
+ migrationContent += `}
243
+ `;
244
+ const migrationFileName = `${new Date().getTime().toString()}-create-${tableName}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
245
+ await Bun.write(migrationFilePath, migrationContent);
246
+ log.success(`Created migration: ${italic(migrationFileName)}`);
247
+ }
248
+ async function createPivotTableMigration(model, modelPath) {
249
+ const pivotTables = await getPivotTables(model, modelPath);
250
+ if (!pivotTables.length)
251
+ return;
252
+ for (const pivotTable of pivotTables) {
253
+ if (await checkPivotMigration(pivotTable.table))
254
+ return;
255
+ let migrationContent = `import type { Database } from '@stacksjs/database'
256
+ `;
257
+ migrationContent += `import { sql } from '@stacksjs/database'
258
+
259
+ `;
260
+ migrationContent += `export async function up(db: Database<any>) {
261
+ `;
262
+ migrationContent += ` await (db as any).schema
263
+ `;
264
+ migrationContent += ` .createTable('${pivotTable.table}')
265
+ `;
266
+ migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
267
+ `;
268
+ migrationContent += ` .addColumn('${pivotTable.firstForeignKey}', 'integer')
269
+ `;
270
+ migrationContent += ` .addColumn('${pivotTable.secondForeignKey}', 'integer')
271
+ `;
272
+ migrationContent += ` .addColumn('created_at', 'timestamp', col => col.defaultTo(sql.raw('CURRENT_TIMESTAMP')))
273
+ `;
274
+ migrationContent += ` .execute()
275
+ `;
276
+ migrationContent += ` }
277
+ `;
278
+ const migrationFileName = `${new Date().getTime().toString()}-create-${pivotTable.table}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
279
+ await Bun.write(migrationFilePath, migrationContent);
280
+ log.success(`Created pivot migration: ${migrationFileName}`);
281
+ }
282
+ }
283
+ async function createAlterTableMigration(modelPath) {
284
+ const model = (await import(modelPath)).default, modelName = getModelName(model, modelPath), tableName = getTableName(model, modelPath);
285
+ let hasChanged = !1;
286
+ const oldModel = (await import(path.frameworkPath(`cache/models/${modelName}.ts`))).default, lastFields = await getLastMigrationFields(modelName) ?? {}, currentFields = model.attributes, changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields)), fieldsToAdd = changes?.added || [], fieldsToRemove = changes?.removed || [];
287
+ let migrationContent = `import type { Database } from '@stacksjs/database'
288
+ `;
289
+ migrationContent += `import { sql } from '@stacksjs/database'
290
+
291
+ `;
292
+ migrationContent += `export async function up(db: Database<any>) {
293
+ `;
294
+ if (fieldsToAdd.length || fieldsToRemove.length) {
295
+ hasChanged = !0;
296
+ migrationContent += ` await (db as any).schema.alterTable('${tableName}')
297
+ `;
298
+ }
299
+ const fieldValidations = findDifferingKeys(lastFields, currentFields);
300
+ for (const fieldValidation of fieldValidations) {
301
+ hasChanged = !0;
302
+ const fieldNameFormatted = snakeCase(fieldValidation.key);
303
+ migrationContent += `await sql\`
304
+ ALTER TABLE ${tableName}
305
+ MODIFY COLUMN ${fieldNameFormatted} TEXT
306
+ \`.execute(db)
307
+
308
+ `;
309
+ }
310
+ for (const fieldName of fieldsToAdd) {
311
+ const options = currentFields[fieldName], columnType = mapFieldTypeToColumnType(options.validation?.rule, "sqlite"), formattedFieldName = snakeCase(fieldName), isRequired = options.validation?.rule.isRequired;
312
+ migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`;
313
+ if (isRequired || options.unique || options.default !== void 0) {
314
+ migrationContent += ", col => col";
315
+ if (isRequired)
316
+ migrationContent += ".notNull()";
317
+ if (options.unique)
318
+ migrationContent += ".unique()";
319
+ if (options.default !== void 0)
320
+ if (typeof options.default === "string")
321
+ migrationContent += `.defaultTo('${options.default}')`;
322
+ else if (options.default === null)
323
+ migrationContent += ".defaultTo(null)";
324
+ else
325
+ migrationContent += `.defaultTo(${options.default})`;
326
+ migrationContent += "";
327
+ }
328
+ migrationContent += `)
329
+
330
+ `;
331
+ }
332
+ for (const fieldName of fieldsToRemove)
333
+ migrationContent += ` .dropColumn('${fieldName}')
334
+ `;
335
+ if (fieldsToAdd.length || fieldsToRemove.length)
336
+ migrationContent += ` .execute();
337
+ `;
338
+ const lastFieldOrder = Object.values(lastFields).map((attr) => attr.order), currentFieldOrder = Object.values(currentFields).map((attr) => attr.order);
339
+ if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
340
+ hasChanged = !0;
341
+ migrationContent += reArrangeColumns(model.attributes, tableName);
342
+ }
343
+ const oldIndexes = oldModel.indexes || [], newIndexes = model.indexes || [];
344
+ for (const oldIndex of oldIndexes)
345
+ if (!newIndexes.find((newIndex) => newIndex.name === oldIndex.name)) {
346
+ hasChanged = !0;
347
+ migrationContent += ` await (db as any).schema.dropIndex('${oldIndex.name}').execute()
348
+ `;
349
+ }
350
+ for (const newIndex of newIndexes)
351
+ if (!oldIndexes.find((oldIndex) => oldIndex.name === newIndex.name)) {
352
+ hasChanged = !0;
353
+ migrationContent += generateIndexCreationSQL(tableName, newIndex);
354
+ }
355
+ migrationContent += `}
356
+ `;
357
+ const migrationFileName = `${new Date().getTime().toString()}-update-${tableName}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
358
+ if (hasChanged) {
359
+ await Bun.write(migrationFilePath, migrationContent);
360
+ log.success(`Created migration: ${italic(migrationFileName)}`);
361
+ }
362
+ }
363
+ function reArrangeColumns(attributes, tableName) {
364
+ const fields = arrangeColumns(attributes);
365
+ let migrationContent = "", previousField = "";
366
+ for (const [fieldName] of fields) {
367
+ const fieldNameFormatted = snakeCase(fieldName);
368
+ if (previousField)
369
+ migrationContent += `await sql\`
370
+ ALTER TABLE ${tableName}
371
+ MODIFY COLUMN ${fieldNameFormatted} TEXT NOT NULL AFTER ${snakeCase(previousField)};
372
+ \`.execute(db)
373
+
374
+ `;
375
+ previousField = fieldNameFormatted;
376
+ }
377
+ return migrationContent;
378
+ }
379
+ export function generateIndexCreationSQL(tableName, index) {
380
+ if (index.unique || index.where) {
381
+ const unique = index.unique ? "UNIQUE " : "", cols = index.columns.map((col) => snakeCase(col)).join(", "), whereClause = index.where ? ` WHERE ${index.where}` : "";
382
+ return ` await db.unsafe(\`CREATE ${unique}INDEX IF NOT EXISTS "${index.name}" ON "${tableName}" (${cols})${whereClause}\`).execute()
383
+ `;
384
+ }
385
+ const columnsStr = index.columns.map((col) => `\`${snakeCase(col)}\``).join(", ");
386
+ return ` await (db as any).schema.createIndex('${index.name}').on('${tableName}').columns([${columnsStr}]).execute()
387
+ `;
388
+ }
389
+ function generatePrimaryKeyIndexSQL(tableName) {
390
+ return ` await (db as any).schema.createIndex('${tableName}_id_index').on('${tableName}').column('id').execute()
391
+ `;
392
+ }
393
+ function generateForeignKeyIndexSQL(tableName, foreignKey) {
394
+ return ` await (db as any).schema.createIndex('${tableName}_${foreignKey}_index').on('${tableName}').column(\`${foreignKey}\`).execute()
395
+
396
+ `;
397
+ }
@@ -0,0 +1,51 @@
1
+ import { log } from "@stacksjs/logging";
2
+ import { seedModelDirect } from "./seeder";
3
+ function resolveDefinition(input) {
4
+ if (input && typeof input === "object") {
5
+ const obj = input;
6
+ if (obj._definition && typeof obj._definition === "object" && "name" in obj._definition)
7
+ return obj._definition;
8
+ if ("name" in obj && (("attributes" in obj) || ("table" in obj) || ("traits" in obj)))
9
+ return obj;
10
+ }
11
+ throw Error("factory.generate: expected a Stacks model (the default export of app/Models/*.ts, or a defineModel() return value). Got something without a `.name` field.");
12
+ }
13
+ function snakeCase(str) {
14
+ return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/(\d)([A-Za-z])/g, "$1_$2").toLowerCase();
15
+ }
16
+ function snakeCaseKeys(input) {
17
+ const out = {};
18
+ for (const [key, value] of Object.entries(input))
19
+ out[snakeCase(key)] = value;
20
+ return out;
21
+ }
22
+ export function buildSeederPayload(modelInput, options = {}) {
23
+ const def = resolveDefinition(modelInput), name = def.name, attributes = def.attributes ?? {}, useSeederConfig = def.traits?.useSeeder, seederDefault = useSeederConfig && typeof useSeederConfig === "object" ? useSeederConfig : void 0, count = options.count ?? seederDefault?.count ?? 10, globalOverrides = options.with ? snakeCaseKeys(options.with) : void 0, perRow = options.rows ?? seederDefault?.fixtures ?? [], fixtureCount = Math.max(count, perRow.length), fixtures = [];
24
+ for (let i = 0;i < fixtureCount; i++) {
25
+ const row = perRow[i];
26
+ if (!globalOverrides && !row)
27
+ continue;
28
+ fixtures[i] = { ...globalOverrides ?? {}, ...row ? snakeCaseKeys(row) : {} };
29
+ }
30
+ const table = def.table ?? `${snakeCase(name)}s`;
31
+ return { name, table, count: fixtureCount, fixtures, attributes, model: def };
32
+ }
33
+ export async function generate(modelInput, options = {}) {
34
+ const payload = buildSeederPayload(modelInput, options);
35
+ try {
36
+ return await seedModelDirect({
37
+ ...payload,
38
+ filePath: ""
39
+ }, {
40
+ fresh: options.fresh,
41
+ verbose: options.verbose ?? !1
42
+ });
43
+ } catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ log.error(`[factory] generate(${payload.name}) failed: ${message}`);
46
+ throw err;
47
+ }
48
+ }
49
+ export const factory = {
50
+ generate
51
+ };
@@ -0,0 +1,181 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { plural, singular, snakeCase } from "@stacksjs/strings";
4
+ import { path } from "@stacksjs/path";
5
+ import { globSync } from "@stacksjs/storage";
6
+ export function safeGlob(pattern) {
7
+ const metaIdx = pattern.search(/[*?[]/), root = metaIdx === -1 ? dirname(pattern) : dirname(pattern.slice(0, metaIdx));
8
+ if (!existsSync(root))
9
+ return [];
10
+ try {
11
+ return globSync(pattern, { absolute: !0 });
12
+ } catch {
13
+ return [];
14
+ }
15
+ }
16
+ export async function getDeclaredFKs() {
17
+ const modelFiles = [
18
+ ...safeGlob(path.userModelsPath("*.ts")),
19
+ ...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))
20
+ ], declared = [];
21
+ for (const modelFile of modelFiles) {
22
+ let model;
23
+ try {
24
+ model = (await import(modelFile)).default;
25
+ } catch {
26
+ continue;
27
+ }
28
+ if (!model || typeof model !== "object")
29
+ continue;
30
+ const fromTable = model.table || plural(snakeCase(model.name || "")), belongsTo = model.belongsTo;
31
+ if (Array.isArray(belongsTo))
32
+ for (const entry of belongsTo) {
33
+ const related = typeof entry === "string" ? entry : entry?.model ?? "";
34
+ if (!related)
35
+ continue;
36
+ const fromColumn = `${snakeCase(singular(related))}_id`, toTable = plural(snakeCase(related));
37
+ declared.push({
38
+ fromTable,
39
+ fromColumn,
40
+ toTable,
41
+ toColumn: "id",
42
+ model: model.name || ""
43
+ });
44
+ }
45
+ else if (belongsTo && typeof belongsTo === "object")
46
+ for (const related of Object.keys(belongsTo)) {
47
+ const fromColumn = `${snakeCase(singular(related))}_id`, toTable = plural(snakeCase(related));
48
+ declared.push({
49
+ fromTable,
50
+ fromColumn,
51
+ toTable,
52
+ toColumn: "id",
53
+ model: model.name || ""
54
+ });
55
+ }
56
+ }
57
+ return declared;
58
+ }
59
+ export async function getLiveFKs() {
60
+ const { db } = await import("./utils"), dialect = await currentDialect();
61
+ if (dialect === "sqlite")
62
+ return getSqliteLiveFKs(db);
63
+ if (dialect === "mysql")
64
+ return getMysqlLiveFKs(db);
65
+ if (dialect === "postgres")
66
+ return getPostgresLiveFKs(db);
67
+ return [];
68
+ }
69
+ export async function auditForeignKeys() {
70
+ const declared = await getDeclaredFKs(), live = await getLiveFKs(), liveKeys = new Set(live.map((fk) => `${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)), missing = declared.filter((d) => {
71
+ const key = `${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;
72
+ return !liveKeys.has(key);
73
+ });
74
+ return { declared, live, missing };
75
+ }
76
+ export async function findFkOrphans(dialect) {
77
+ if ((dialect ?? await currentDialect()) !== "sqlite")
78
+ return { supported: !1, total: 0, orphans: [] };
79
+ const { db } = await import("./utils"), rows = await db.unsafe("PRAGMA foreign_key_check").execute(), checkRows = Array.isArray(rows) ? rows : [], fkListCache = new Map;
80
+ async function fkListFor(table) {
81
+ if (fkListCache.has(table))
82
+ return fkListCache.get(table);
83
+ if (!/^[a-z_]\w*$/i.test(table)) {
84
+ fkListCache.set(table, []);
85
+ return [];
86
+ }
87
+ const list = await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(), arr = Array.isArray(list) ? list : [];
88
+ fkListCache.set(table, arr);
89
+ return arr;
90
+ }
91
+ const grouped = new Map;
92
+ for (const raw of checkRows) {
93
+ const r = raw, table = String(r.table ?? ""), parent = String(r.parent ?? "");
94
+ if (!table || !parent)
95
+ continue;
96
+ const fkid = Number(r.fkid ?? 0);
97
+ let column = "";
98
+ const fkList = await fkListFor(table);
99
+ for (const fk of fkList) {
100
+ const f = fk;
101
+ if (Number(f.id) === fkid && f.from) {
102
+ column = String(f.from);
103
+ break;
104
+ }
105
+ }
106
+ const key = `${table}\x00${parent}\x00${fkid}`;
107
+ let entry = grouped.get(key);
108
+ if (!entry) {
109
+ entry = { table, column, parent, count: 0, sampleRowids: [] };
110
+ grouped.set(key, entry);
111
+ }
112
+ entry.count++;
113
+ if (entry.sampleRowids.length < 5 && typeof r.rowid === "number")
114
+ entry.sampleRowids.push(r.rowid);
115
+ }
116
+ const orphans = [...grouped.values()];
117
+ return { supported: !0, total: orphans.reduce((sum, o) => sum + o.count, 0), orphans };
118
+ }
119
+ async function currentDialect() {
120
+ const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
121
+ if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
122
+ return driver;
123
+ return "other";
124
+ }
125
+ async function getSqliteLiveFKs(db) {
126
+ const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"), rows = Array.isArray(tables) ? tables : [], fks = [];
127
+ for (const row of rows) {
128
+ const fromTable = row.name;
129
+ if (!fromTable)
130
+ continue;
131
+ if (!/^[a-z_][\w]*$/i.test(fromTable))
132
+ continue;
133
+ const fkRows = await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);
134
+ for (const fk of Array.isArray(fkRows) ? fkRows : []) {
135
+ const r = fk;
136
+ if ((r.seq ?? 0) !== 0)
137
+ continue;
138
+ if (!r.from || !r.to || !r.table)
139
+ continue;
140
+ fks.push({ fromTable, fromColumn: r.from, toTable: r.table, toColumn: r.to });
141
+ }
142
+ }
143
+ return fks;
144
+ }
145
+ async function getMysqlLiveFKs(db) {
146
+ const rows = await db.unsafe(`
147
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
148
+ FROM information_schema.KEY_COLUMN_USAGE
149
+ WHERE TABLE_SCHEMA = DATABASE()
150
+ AND REFERENCED_TABLE_NAME IS NOT NULL
151
+ `);
152
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
153
+ fromTable: String(row.TABLE_NAME ?? row.table_name ?? ""),
154
+ fromColumn: String(row.COLUMN_NAME ?? row.column_name ?? ""),
155
+ toTable: String(row.REFERENCED_TABLE_NAME ?? row.referenced_table_name ?? ""),
156
+ toColumn: String(row.REFERENCED_COLUMN_NAME ?? row.referenced_column_name ?? "")
157
+ })).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
158
+ }
159
+ async function getPostgresLiveFKs(db) {
160
+ const rows = await db.unsafe(`
161
+ SELECT
162
+ kcu.table_name AS from_table,
163
+ kcu.column_name AS from_column,
164
+ ccu.table_name AS to_table,
165
+ ccu.column_name AS to_column
166
+ FROM information_schema.referential_constraints rc
167
+ JOIN information_schema.key_column_usage kcu
168
+ ON kcu.constraint_name = rc.constraint_name
169
+ AND kcu.constraint_schema = rc.constraint_schema
170
+ JOIN information_schema.constraint_column_usage ccu
171
+ ON ccu.constraint_name = rc.constraint_name
172
+ AND ccu.constraint_schema = rc.constraint_schema
173
+ WHERE rc.constraint_schema = 'public'
174
+ `);
175
+ return (Array.isArray(rows) ? rows : []).map((row) => ({
176
+ fromTable: String(row.from_table ?? ""),
177
+ fromColumn: String(row.from_column ?? ""),
178
+ toTable: String(row.to_table ?? ""),
179
+ toColumn: String(row.to_column ?? "")
180
+ })).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
181
+ }