@stacksjs/database 0.70.88 → 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 (83) hide show
  1. package/dist/auth-tables.d.ts +60 -0
  2. package/dist/auth-tables.js +220 -0
  3. package/dist/class-seeder.d.ts +65 -0
  4. package/dist/class-seeder.js +116 -0
  5. package/dist/column.d.ts +17 -0
  6. package/dist/column.js +26 -0
  7. package/dist/custom/audits.d.ts +16 -0
  8. package/dist/custom/audits.js +57 -0
  9. package/dist/custom/errors.d.ts +1 -0
  10. package/dist/custom/errors.js +48 -0
  11. package/dist/custom/index.d.ts +3 -0
  12. package/dist/custom/index.js +3 -0
  13. package/dist/custom/jobs.d.ts +3 -0
  14. package/dist/custom/jobs.js +449 -0
  15. package/dist/database.d.ts +89 -0
  16. package/dist/database.js +178 -0
  17. package/dist/defaults.d.ts +48 -0
  18. package/dist/defaults.js +48 -0
  19. package/dist/driver-config.d.ts +149 -0
  20. package/dist/driver-config.js +144 -0
  21. package/dist/drivers/defaults/index.d.ts +2 -0
  22. package/dist/drivers/defaults/index.js +2 -0
  23. package/dist/drivers/defaults/passwords.d.ts +4 -0
  24. package/dist/drivers/defaults/passwords.js +106 -0
  25. package/dist/drivers/defaults/traits.d.ts +33 -0
  26. package/dist/drivers/defaults/traits.js +1125 -0
  27. package/dist/drivers/dynamodb.d.ts +200 -0
  28. package/dist/drivers/dynamodb.js +607 -0
  29. package/dist/drivers/helpers.d.ts +35 -0
  30. package/dist/drivers/helpers.js +206 -0
  31. package/dist/drivers/index.d.ts +16 -0
  32. package/dist/drivers/index.js +9 -0
  33. package/dist/drivers/mysql.d.ts +7 -0
  34. package/dist/drivers/mysql.js +322 -0
  35. package/dist/drivers/postgres.d.ts +7 -0
  36. package/dist/drivers/postgres.js +411 -0
  37. package/dist/drivers/sqlite.d.ts +20 -0
  38. package/dist/drivers/sqlite.js +397 -0
  39. package/dist/factory.d.ts +41 -0
  40. package/dist/factory.js +51 -0
  41. package/dist/fk-audit.d.ts +101 -0
  42. package/dist/fk-audit.js +181 -0
  43. package/dist/index.d.ts +149 -0
  44. package/dist/index.js +55 -0
  45. package/dist/migration-lock.d.ts +23 -0
  46. package/dist/migration-lock.js +143 -0
  47. package/dist/migrations.d.ts +76 -0
  48. package/dist/migrations.js +528 -0
  49. package/dist/notification-tables.d.ts +20 -0
  50. package/dist/notification-tables.js +54 -0
  51. package/dist/query-logger.d.ts +26 -0
  52. package/dist/query-logger.js +213 -0
  53. package/dist/query-parser.d.ts +4 -0
  54. package/dist/query-parser.js +93 -0
  55. package/dist/rbac-tables.d.ts +17 -0
  56. package/dist/rbac-tables.js +84 -0
  57. package/dist/safe-migrations.d.ts +72 -0
  58. package/dist/safe-migrations.js +59 -0
  59. package/dist/schema.d.ts +4 -0
  60. package/dist/schema.js +10 -0
  61. package/dist/seed-scaffold.d.ts +34 -0
  62. package/dist/seed-scaffold.js +144 -0
  63. package/dist/seeder.d.ts +116 -0
  64. package/dist/seeder.js +363 -0
  65. package/dist/sql-helpers.d.ts +33 -0
  66. package/dist/sql-helpers.js +24 -0
  67. package/dist/table.d.ts +7 -0
  68. package/dist/table.js +26 -0
  69. package/dist/tools/setup.d.ts +1 -0
  70. package/dist/tools/setup.js +6 -0
  71. package/dist/transaction-context.d.ts +52 -0
  72. package/dist/transaction-context.js +62 -0
  73. package/dist/types.d.ts +151 -0
  74. package/dist/types.js +23 -0
  75. package/dist/unique-audit.d.ts +60 -0
  76. package/dist/unique-audit.js +174 -0
  77. package/dist/utils.d.ts +189 -0
  78. package/dist/utils.js +163 -0
  79. package/dist/uuid-columns.d.ts +22 -0
  80. package/dist/uuid-columns.js +68 -0
  81. package/dist/validators.d.ts +26 -0
  82. package/dist/validators.js +122 -0
  83. 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,41 @@
1
+ import type { Attribute, Model } from '@stacksjs/types';
2
+ import type { SeedResult } from './seeder';
3
+ /**
4
+ * Build the internal `SeederModel`-shaped payload that `seedModelDirect`
5
+ * expects from a public-API call. Pure function — exported separately
6
+ * so tests can assert the override-precedence rules without touching
7
+ * the database.
8
+ *
9
+ * Precedence (lowest → highest): per-attribute `factory` output →
10
+ * global `options.with` → per-row `options.rows[i]`. All keys are
11
+ * snake-cased before insert so callers can use the model's camelCase
12
+ * attribute names without thinking about column naming.
13
+ */
14
+ export declare function buildSeederPayload(modelInput: unknown, options?: GenerateOptions): {
15
+ name: string
16
+ table: string
17
+ count: number
18
+ fixtures: Array<Record<string, unknown>>
19
+ attributes: Record<string, Attribute>
20
+ model: Model
21
+ };
22
+ /**
23
+ * Generate factory rows for a model and insert them. Designed to be
24
+ * called from a class seeder.
25
+ *
26
+ * Honours the model's per-attribute `factory: faker => …` declarations
27
+ * — exactly the same code path as the legacy auto-walker — but without
28
+ * the implicit "every model with `useSeeder` fires on every run"
29
+ * coupling. See stacksjs/stacks#1919 for the rationale.
30
+ */
31
+ export declare function generate(modelInput: unknown, options?: GenerateOptions): Promise<SeedResult>;
32
+ export declare const factory: {
33
+ generate: typeof generate
34
+ };
35
+ export declare interface GenerateOptions {
36
+ count?: number
37
+ fresh?: boolean
38
+ verbose?: boolean
39
+ with?: Record<string, unknown>
40
+ rows?: Array<Record<string, unknown>>
41
+ }
@@ -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,101 @@
1
+ /**
2
+ * Glob, but resilient to "directory doesn't exist yet" (the userland
3
+ * `app/Models/` directory in particular often doesn't exist when the
4
+ * audit runs in framework tests or scaffolded apps that haven't
5
+ * created any models yet). Avoids the Bun Glob ENOENT.
6
+ */
7
+ export declare function safeGlob(pattern: string): string[];
8
+ /**
9
+ * Walk every model file (user + framework defaults) and return the
10
+ * full list of declared `belongsTo` foreign keys.
11
+ *
12
+ * Convention: `Comment` with `belongsTo: ['Post']` implies the FK
13
+ * `comments.post_id → posts.id`. Same shape the migration generator
14
+ * uses, so we keep it consistent here. Other relationship types
15
+ * (`hasOne`, `hasMany`, `belongsToMany` through tables) imply FKs in
16
+ * the *other* direction or in pivot tables — we only check
17
+ * `belongsTo` here for the simplest, highest-signal audit.
18
+ */
19
+ export declare function getDeclaredFKs(): Promise<DeclaredFK[]>;
20
+ /**
21
+ * Query the live database for every foreign key constraint, returning
22
+ * a normalised shape. Dialect-aware: PRAGMA on SQLite,
23
+ * information_schema on MySQL / PostgreSQL.
24
+ */
25
+ export declare function getLiveFKs(): Promise<LiveFK[]>;
26
+ /**
27
+ * Diff declared FKs against live FKs. Returns the declared FKs that
28
+ * have no matching row in the live database — these are the silent
29
+ * "FK should be enforcing referential integrity but isn't" cases that
30
+ * motivated this audit.
31
+ *
32
+ * Match key is `fromTable.fromColumn → toTable.toColumn` (case-
33
+ * insensitive). Extra live FKs (present in DB but not in any model)
34
+ * are not reported — those usually come from manual migrations or
35
+ * external tooling, both legitimate.
36
+ */
37
+ export declare function auditForeignKeys(): Promise<FkAuditResult>;
38
+ /**
39
+ * Scan the live database for rows whose foreign key references a
40
+ * parent row that doesn't exist. SQLite's `PRAGMA foreign_key_check`
41
+ * does this regardless of the `foreign_keys` pragma state and excludes
42
+ * NULL-FK rows. The violating column is resolved by joining the
43
+ * reported `fkid` against `PRAGMA foreign_key_list(table)`.
44
+ */
45
+ export declare function findFkOrphans(dialect?: 'sqlite' | 'mysql' | 'postgres' | 'other'): Promise<FkOrphanReport>;
46
+ // stacksjs/stacks#1916 — Foreign-key audit. Compares each model's
47
+ // declared `belongsTo` relationships against the FKs that actually
48
+ // exist in the live database. Drives `buddy doctor`'s FK integrity
49
+ // check, surfaces the "you flipped DB_CONNECTION but the FKs didn't
50
+ // follow" failure mode that motivated #1915 and #1916.
51
+ //
52
+ // Two halves:
53
+ //
54
+ // 1. Declared FKs: walk model files, look at `belongsTo`, compute
55
+ // the implied FK shape `{ fromTable, fromColumn, toTable,
56
+ // toColumn }`. Convention is `<related>_id` → `<related>.id`,
57
+ // same as the migration generator.
58
+ //
59
+ // 2. Live FKs: query the live database. SQLite via
60
+ // `PRAGMA foreign_key_list("…")`, MySQL/Postgres via
61
+ // `information_schema.key_column_usage`.
62
+ export declare interface DeclaredFK {
63
+ fromTable: string
64
+ fromColumn: string
65
+ toTable: string
66
+ toColumn: string
67
+ model: string
68
+ }
69
+ export declare interface LiveFK {
70
+ fromTable: string
71
+ fromColumn: string
72
+ toTable: string
73
+ toColumn: string
74
+ }
75
+ export declare interface FkAuditResult {
76
+ declared: DeclaredFK[]
77
+ live: LiveFK[]
78
+ missing: DeclaredFK[]
79
+ }
80
+ // stacksjs/stacks#1951 — FK orphan detection. FK enforcement flipped
81
+ // ON (utils.ts bootstrap pragmas) against databases that were written
82
+ // while `foreign_keys = OFF`, so legacy rows can reference parents that
83
+ // no longer exist. Those orphans silently turn previously-working
84
+ // deletes/inserts into runtime FK failures. This READ-ONLY scan finds
85
+ // them so `buddy doctor` can report before they bite.
86
+ //
87
+ // SQLite-specific: MySQL/Postgres enforce FKs natively, so the
88
+ // FK-off-legacy-data failure mode can't arise there — we degrade to
89
+ // `supported: false` rather than run an expensive per-FK anti-join.
90
+ export declare interface FkOrphan {
91
+ table: string
92
+ column: string
93
+ parent: string
94
+ count: number
95
+ sampleRowids: number[]
96
+ }
97
+ export declare interface FkOrphanReport {
98
+ supported: boolean
99
+ total: number
100
+ orphans: FkOrphan[]
101
+ }