@stacksjs/database 0.66.0 → 0.67.0

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.
@@ -1,433 +0,0 @@
1
- import type { Attribute, Attributes, Model } from '@stacksjs/types'
2
- import { italic, log } from '@stacksjs/cli'
3
- import { app } from '@stacksjs/config'
4
- import { db } from '@stacksjs/database'
5
- import { type Ok, ok } from '@stacksjs/error-handling'
6
- import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from '@stacksjs/orm'
7
- import { path } from '@stacksjs/path'
8
- import { fs, globSync } from '@stacksjs/storage'
9
- import { snakeCase } from '@stacksjs/strings'
10
- import {
11
- arrangeColumns,
12
- checkPivotMigration,
13
- fetchTables,
14
- findDifferingKeys,
15
- getLastMigrationFields,
16
- hasTableBeenMigrated,
17
- isArrayEqual,
18
- mapFieldTypeToColumnType,
19
- pluckChanges,
20
- } from '.'
21
-
22
- export async function resetSqliteDatabase(): Promise<Ok<string, never>> {
23
- await deleteFrameworkModels()
24
- await deleteMigrationFiles()
25
- await dropSqliteTables()
26
-
27
- return ok('All tables dropped successfully!')
28
- }
29
-
30
- export async function deleteMigrationFiles(): Promise<void> {
31
- const files = await fs.readdir(path.userMigrationsPath())
32
-
33
- if (files.length) {
34
- for (const file of files) {
35
- if (file.endsWith('.ts')) {
36
- const migrationPath = path.userMigrationsPath(`${file}`)
37
-
38
- if (fs.existsSync(migrationPath))
39
- await Bun.$`rm ${migrationPath}`
40
- }
41
- }
42
- }
43
- }
44
-
45
- export async function deleteFrameworkModels(): Promise<void> {
46
- const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
47
-
48
- if (modelFiles.length) {
49
- for (const modelFile of modelFiles) {
50
- if (modelFile.endsWith('.ts')) {
51
- const modelPath = path.frameworkPath(`database/models/${modelFile}`)
52
-
53
- if (fs.existsSync(modelPath))
54
- await Bun.$`rm ${modelPath}`
55
- }
56
- }
57
- }
58
- }
59
-
60
- export async function dropSqliteTables(): Promise<void> {
61
- const userModelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
62
- const tables = await fetchTables()
63
-
64
- for (const table of tables) await db.schema.dropTable(table).ifExists().execute()
65
- await db.schema.dropTable('migrations').ifExists().execute()
66
- await db.schema.dropTable('migration_locks').ifExists().execute()
67
- await db.schema.dropTable('passkeys').ifExists().execute()
68
-
69
- for (const userModel of userModelFiles) {
70
- const userModelPath = (await import(userModel)).default
71
- const pivotTables = await getPivotTables(userModelPath, userModel)
72
-
73
- for (const pivotTable of pivotTables) await db.schema.dropTable(pivotTable.table).ifExists().execute()
74
- }
75
- }
76
-
77
- export function fetchSqliteFile(): string {
78
- if (app.env === 'testing') {
79
- return fetchTestSqliteFile()
80
- }
81
-
82
- return path.userDatabasePath('stacks.sqlite')
83
- }
84
-
85
- export function fetchTestSqliteFile(): string {
86
- return path.userDatabasePath('stacks_testing.sqlite')
87
- }
88
-
89
- export async function generateSqliteMigration(modelPath: string): Promise<void> {
90
- // check if any files are in the database folder
91
- // const files = await fs.readdir(path.userMigrationsPath())
92
-
93
- // if (files.length === 0) {
94
- // log.debug('No migrations found in the database folder, deleting all framework/database/*.json files...')
95
-
96
- // // delete the *.ts files in the database/models folder
97
- // const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
98
-
99
- // if (modelFiles.length) {
100
- // log.debug('No existing model files in framework path...')
101
-
102
- // for (const file of modelFiles)
103
- // if (file.endsWith('.ts')) await fs.unlink(path.frameworkPath(`database/models/${file}`))
104
- // }
105
- // }
106
-
107
- const model = (await import(modelPath)).default as Model
108
- const fileName = path.basename(modelPath)
109
- const tableName = await getTableName(model, modelPath)
110
-
111
- const fieldsString = JSON.stringify(model.attributes, null, 2) // Pretty print the JSON
112
- const copiedModelPath = path.frameworkPath(`database/models/${fileName}`)
113
-
114
- let haveFieldsChanged = false
115
-
116
- // if the file exists, we need to check if the fields have changed
117
- if (fs.existsSync(copiedModelPath)) {
118
- log.debug(`Fields have already been generated for ${tableName}`)
119
-
120
- const previousFields = await getLastMigrationFields(fileName)
121
- const previousFieldsString = JSON.stringify(previousFields, null, 2) // Convert to string for comparison
122
-
123
- if (previousFieldsString === fieldsString) {
124
- log.debug(`Fields have not changed for ${tableName}`)
125
- return
126
- }
127
-
128
- haveFieldsChanged = true
129
- log.debug(`Fields have changed for ${tableName}`)
130
- }
131
- else {
132
- log.debug(`Fields have not been generated for ${tableName}`)
133
- }
134
-
135
- // store the fields of the model to a file
136
- await Bun.$`cp ${modelPath} ${copiedModelPath}`
137
-
138
- // if the fields have changed, we need to create a new update migration
139
- // if the fields have not changed, we need to migrate the table
140
-
141
- // we need to check if this tableName has already been migrated
142
- const hasBeenMigrated = await hasTableBeenMigrated(tableName)
143
-
144
- log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`)
145
-
146
- if (haveFieldsChanged)
147
- await createAlterTableMigration(modelPath)
148
- else await createTableMigration(modelPath)
149
- }
150
-
151
- export async function copyModelFiles(modelPath: string): Promise<void> {
152
- const model = (await import(modelPath)).default as Model
153
- const fileName = path.basename(modelPath)
154
- const tableName = await getTableName(model, modelPath)
155
-
156
- const fieldsString = JSON.stringify(model.attributes, null, 2) // Pretty print the JSON
157
- const copiedModelPath = path.frameworkPath(`database/models/${fileName}`)
158
-
159
- // if the file exists, we need to check if the fields have changed
160
- if (fs.existsSync(copiedModelPath)) {
161
- log.debug(`Fields have already been generated for ${tableName}`)
162
-
163
- const previousFields = await getLastMigrationFields(fileName)
164
- const previousFieldsString = JSON.stringify(previousFields, null, 2) // Convert to string for comparison
165
-
166
- if (previousFieldsString === fieldsString) {
167
- log.debug(`Fields have not changed for ${tableName}`)
168
- return
169
- }
170
- }
171
-
172
- // store the fields of the model to a file
173
- await Bun.$`cp ${modelPath} ${copiedModelPath}`
174
- }
175
- async function createTableMigration(modelPath: string) {
176
- log.debug('createTableMigration modelPath:', modelPath)
177
-
178
- const model = (await import(modelPath)).default as Model
179
- const tableName = getTableName(model, modelPath)
180
-
181
- const twoFactorEnabled
182
- = model.traits?.useAuth && typeof model.traits.useAuth !== 'boolean' ? model.traits.useAuth.useTwoFactor : false
183
-
184
- await createPivotTableMigration(model, modelPath)
185
- const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
186
-
187
- const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
188
- const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
189
-
190
- const usePasskey = (typeof model.traits?.useAuth === 'object' && model.traits.useAuth.usePasskey) ?? false
191
-
192
- if (usePasskey)
193
- await createPasskeyMigration()
194
-
195
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
196
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
197
- migrationContent += `export async function up(db: Database<any>) {\n`
198
- migrationContent += ` await db.schema\n`
199
- migrationContent += ` .createTable('${tableName}')\n`
200
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
201
-
202
- for (const [fieldName, options] of arrangeColumns(model.attributes)) {
203
- const fieldOptions = options as Attribute
204
- const fieldNameFormatted = snakeCase(fieldName)
205
- const columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule, 'sqlite')
206
- migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`
207
-
208
- // Check if there are configurations that require the lambda function
209
- if (fieldOptions.unique || fieldOptions?.required) {
210
- migrationContent += `, col => col`
211
- if (fieldOptions.unique)
212
- migrationContent += `.unique()`
213
- if (fieldOptions?.required)
214
- migrationContent += `.notNull()`
215
- migrationContent += ``
216
- }
217
-
218
- migrationContent += `)\n`
219
- }
220
-
221
- if (twoFactorEnabled !== false && twoFactorEnabled) {
222
- migrationContent += ` .addColumn('two_factor_secret', 'varchar(255)')\n`
223
- }
224
-
225
- if (otherModelRelations?.length) {
226
- for (const modelRelation of otherModelRelations) {
227
- migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
228
- col.references('${modelRelation.relationTable}.id').onDelete('cascade')
229
- ) \n`
230
- }
231
- }
232
-
233
- if (usePasskey)
234
- migrationContent += ` .addColumn('public_passkey', 'text')\n`
235
-
236
- // Append created_at and updated_at columns if useTimestamps is true
237
- if (useTimestamps) {
238
- migrationContent
239
- += ' .addColumn(\'created_at\', \'timestamp\', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n'
240
- migrationContent += ' .addColumn(\'updated_at\', \'timestamp\')\n'
241
- }
242
-
243
- if (useSoftDeletes)
244
- migrationContent += ` .addColumn('deleted_at', 'text')\n`
245
-
246
- migrationContent += ` .execute()\n`
247
- migrationContent += `}\n`
248
-
249
- const timestamp = new Date().getTime().toString()
250
- const migrationFileName = `${timestamp}-create-${tableName}-table.ts`
251
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
252
-
253
- Bun.write(migrationFilePath, migrationContent)
254
-
255
- log.success(`Created migration: ${italic(migrationFileName)}`)
256
- }
257
-
258
- async function createPivotTableMigration(model: Model, modelPath: string) {
259
- const pivotTables = await getPivotTables(model, modelPath)
260
-
261
- if (!pivotTables.length)
262
- return
263
-
264
- for (const pivotTable of pivotTables) {
265
- const hasBeenMigrated = await checkPivotMigration(pivotTable.table)
266
-
267
- if (hasBeenMigrated)
268
- return
269
-
270
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
271
- migrationContent += `export async function up(db: Database<any>) {\n`
272
- migrationContent += ` await db.schema\n`
273
- migrationContent += ` .createTable('${pivotTable.table}')\n`
274
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
275
- migrationContent += ` .addColumn('${pivotTable.firstForeignKey}', 'integer')\n`
276
- migrationContent += ` .addColumn('${pivotTable.secondForeignKey}', 'integer')\n`
277
- migrationContent += ` .execute()\n`
278
- migrationContent += ` }\n`
279
-
280
- const timestamp = new Date().getTime().toString()
281
- const migrationFileName = `${timestamp}-create-${pivotTable.table}-table.ts`
282
-
283
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
284
-
285
- Bun.write(migrationFilePath, migrationContent)
286
-
287
- log.success(`Created pivot migration: ${migrationFileName}`)
288
- }
289
- }
290
-
291
- async function createPasskeyMigration() {
292
- const hasBeenMigrated = await hasTableBeenMigrated('users')
293
-
294
- if (hasBeenMigrated)
295
- return
296
-
297
- let migrationContent = `import type { Database } from '@stacksjs/database'\n import { sql } from '@stacksjs/database'\n\n`
298
- migrationContent += `export async function up(db: Database<any>) {\n`
299
- migrationContent += ` await db.schema\n`
300
- migrationContent += ` .createTable('passkeys')\n`
301
- migrationContent += ` .addColumn('id', 'text')\n`
302
- migrationContent += ` .addColumn('cred_public_key', 'text')\n`
303
- migrationContent += ` .addColumn('user_id', 'integer')\n`
304
- migrationContent += ` .addColumn('webauthn_user_id', 'varchar(255)')\n`
305
- migrationContent += ` .addColumn('counter', 'integer')\n`
306
- migrationContent += ` .addColumn('device_type', 'varchar(255)')\n`
307
- migrationContent += ` .addColumn('credential_type', 'varchar(255)')\n`
308
- migrationContent += ` .addColumn('backup_eligible', 'boolean')\n`
309
- migrationContent += ` .addColumn('backup_status', 'boolean')\n`
310
- migrationContent += ` .addColumn('transports', 'varchar(255)')\n`
311
- migrationContent += ` .addColumn('last_used_at', 'text')\n`
312
- migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))\n`
313
- migrationContent += ` .execute()\n`
314
- migrationContent += ` }\n`
315
-
316
- const timestamp = new Date().getTime().toString()
317
- const migrationFileName = `${timestamp}-create-passkeys-table.ts`
318
-
319
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
320
-
321
- Bun.write(migrationFilePath, migrationContent)
322
-
323
- log.success(`Created pivot migration: ${migrationFileName}`)
324
- }
325
-
326
- async function createAlterTableMigration(modelPath: string) {
327
- const model = (await import(modelPath)).default as Model
328
- const modelName = getModelName(model, modelPath)
329
- const tableName = getTableName(model, modelPath)
330
- let hasChanged = false
331
-
332
- // Assuming you have a function to get the fields from the last migration
333
- // For simplicity, this is not implemented here
334
- const lastMigrationFields = await getLastMigrationFields(modelName)
335
- const lastFields = lastMigrationFields ?? {}
336
- const currentFields = model.attributes as Attributes
337
-
338
- // Determine fields to add and remove
339
-
340
- const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
341
-
342
- const fieldsToAdd = changes?.added || []
343
-
344
- const fieldsToRemove = changes?.removed || []
345
-
346
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
347
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
348
- migrationContent += `export async function up(db: Database<any>) {\n`
349
-
350
- if (fieldsToAdd.length || fieldsToRemove.length) {
351
- hasChanged = true
352
- migrationContent += ` await db.schema.alterTable('${tableName}')\n`
353
- }
354
-
355
- const fieldValidations = findDifferingKeys(lastFields, currentFields)
356
-
357
- for (const fieldValidation of fieldValidations) {
358
- hasChanged = true
359
- const fieldNameFormatted = snakeCase(fieldValidation.key)
360
- migrationContent += `await sql\`
361
- ALTER TABLE ${tableName}
362
- MODIFY COLUMN ${fieldNameFormatted} VARCHAR(${fieldValidation.max})
363
- \`.execute(db)\n\n`
364
- }
365
-
366
- // Add new fields
367
- for (const fieldName of fieldsToAdd) {
368
- const options = currentFields[fieldName] as Attribute
369
- const columnType = mapFieldTypeToColumnType(options.validation?.rule, 'sqlite')
370
- const formattedFieldName = snakeCase(fieldName)
371
-
372
- migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`
373
-
374
- // Check if there are configurations that require the lambda function
375
- if (options.unique || options?.required) {
376
- migrationContent += `, col => col`
377
- if (options.unique)
378
- migrationContent += `.unique()`
379
- if (options?.required)
380
- migrationContent += `.notNull()`
381
- migrationContent += ``
382
- }
383
-
384
- migrationContent += `)\n\n`
385
- }
386
-
387
- // Remove fields that no longer exist
388
- for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
389
-
390
- if (fieldsToAdd.length || fieldsToRemove.length)
391
- migrationContent += ` .execute();\n`
392
-
393
- const lastFieldOrder = Object.values(lastFields).map(attr => attr.order)
394
- const currentFieldOrder = Object.values(currentFields).map(attr => attr.order)
395
-
396
- if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
397
- hasChanged = true
398
- migrationContent += reArrangeColumns(model.attributes, tableName)
399
- }
400
-
401
- migrationContent += `}\n`
402
-
403
- const timestamp = new Date().getTime().toString()
404
- const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
405
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
406
-
407
- if (hasChanged) {
408
- Bun.write(migrationFilePath, migrationContent)
409
-
410
- log.success(`Created migration: ${italic(migrationFileName)}`)
411
- }
412
- }
413
-
414
- function reArrangeColumns(attributes: Attributes | undefined, tableName: string): string {
415
- const fields = arrangeColumns(attributes)
416
- let migrationContent = ''
417
-
418
- let previousField = ''
419
- for (const [fieldName] of fields) {
420
- const fieldNameFormatted = snakeCase(fieldName)
421
-
422
- if (previousField) {
423
- migrationContent += `await sql\`
424
- ALTER TABLE ${tableName}
425
- MODIFY COLUMN ${fieldNameFormatted} VARCHAR(255) NOT NULL AFTER ${snakeCase(previousField)};
426
- \`.execute(db)\n\n`
427
- }
428
-
429
- previousField = fieldNameFormatted
430
- }
431
-
432
- return migrationContent
433
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './drivers'
2
- export * from './migrations'
3
- export * from './schema'
4
- export * from './seeder'
5
- export * from './types'
6
- export * from './utils'
package/src/migrations.ts DELETED
@@ -1,150 +0,0 @@
1
- import { $ } from 'bun'
2
- import { log } from '@stacksjs/cli'
3
- import { database } from '@stacksjs/config'
4
- import { type Err, err, handleError, type Ok, ok, type Result } from '@stacksjs/error-handling'
5
- import { path } from '@stacksjs/path'
6
- import { fs, globSync } from '@stacksjs/storage'
7
- import { FileMigrationProvider, type MigrationResult, Migrator } from 'kysely'
8
- import { generateMysqlMigration, generatePostgresMigration, generateSqliteMigration, resetMysqlDatabase, resetPostgresDatabase, resetSqliteDatabase } from './drivers'
9
- import { db } from './utils'
10
-
11
- const driver = database.default || ''
12
-
13
- export const migrator: Migrator = new Migrator({
14
- db,
15
-
16
- provider: new FileMigrationProvider({
17
- fs,
18
- path,
19
- // This needs to be an absolute path.
20
- migrationFolder: path.userMigrationsPath(),
21
- }),
22
-
23
- migrationTableName: database.migrations,
24
- migrationLockTableName: database.migrationLocks,
25
- })
26
-
27
- // const migratorForeign = new Migrator({
28
- // db,
29
-
30
- // provider: new FileMigrationProvider({
31
- // fs,
32
- // path,
33
- // // This needs to be an absolute path.
34
- // migrationFolder: path.userMigrationsPath('foreign'),
35
- // }),
36
- // })
37
-
38
- export async function runDatabaseMigration(): Promise<Result<MigrationResult[] | string, Error>> {
39
- try {
40
- log.info('Migrating database...')
41
-
42
- const { error, results } = await migrator.migrateToLatest()
43
-
44
- if (error) {
45
- return err(handleError(error))
46
- }
47
-
48
- if (results?.length === 0) {
49
- log.success('No new migrations were executed')
50
- return ok('No new migrations were executed')
51
- }
52
-
53
- if (results)
54
- return ok(results)
55
-
56
- log.success('Database migration completed with no new migrations.')
57
- return ok('Database migration completed with no new migrations.')
58
- }
59
- catch (error) {
60
- return err(handleError('Migration failed', error))
61
- }
62
- }
63
-
64
- export interface MigrationOptions {
65
- name: string
66
- up: string
67
- }
68
-
69
- export async function resetDatabase(): Promise<Ok<string, never>> {
70
- if (driver === 'sqlite')
71
- return await resetSqliteDatabase()
72
- if (driver === 'mysql')
73
- return await resetMysqlDatabase()
74
- if (driver === 'postgres')
75
- return await resetPostgresDatabase()
76
-
77
- throw new Error('Unsupported database driver in resetDatabase')
78
- }
79
-
80
- export async function generateMigrations(): Promise<Ok<string, never> | Err<string, any>> {
81
- try {
82
- log.info('Generating migrations...')
83
-
84
- const modelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
85
-
86
- for (const file of modelFiles) {
87
- log.debug('Generating migration for:', file)
88
-
89
- await generateMigration(file)
90
- }
91
-
92
- log.success('Migrations generated')
93
- return ok('Migrations generated')
94
- }
95
- catch (error) {
96
- return err(error)
97
- }
98
- }
99
-
100
- export async function generateMigration(modelPath: string): Promise<void> {
101
- if (driver === 'sqlite')
102
- await generateSqliteMigration(modelPath)
103
-
104
- if (driver === 'mysql')
105
- await generateMysqlMigration(modelPath)
106
-
107
- if (driver === 'postgres')
108
- await generatePostgresMigration(modelPath)
109
- }
110
-
111
- export async function haveModelFieldsChangedSinceLastMigration(modelPath: string): Promise<boolean> {
112
- log.debug(`haveModelFieldsChangedSinceLastMigration for model: ${modelPath}`)
113
-
114
- // const model = await import(modelPath)
115
- // const tableName = model.default.table
116
- // const lastMigration = await lastMigrationDate()
117
-
118
- // now that we know the date, we need to check the git history for changes to the model file since that date
119
- const cmd = ``
120
- const gitHistory = await $`${cmd}`.text()
121
-
122
- // if there are updates, then we need to check whether
123
- // the updates include the any updates to the model
124
- // fields that would require a migration
125
-
126
- return !!gitHistory
127
- }
128
-
129
- export async function lastMigration(): Promise<any> {
130
- try {
131
- return await db.selectFrom('migrations').selectAll().orderBy('timestamp', 'desc').limit(1).execute()
132
- }
133
- catch (error) {
134
- console.error('Failed to get last migration:', error)
135
- return { error }
136
- }
137
- }
138
-
139
- export async function lastMigrationDate(): Promise<string | undefined> {
140
- try {
141
- return (await db.selectFrom('migrations').select('timestamp').orderBy('timestamp', 'desc').limit(1).execute())[0]
142
- .timestamp
143
- }
144
- catch (error) {
145
- console.error('Failed to get last migration date:', error)
146
- return undefined
147
- }
148
- }
149
-
150
- export type { MigrationResult }
package/src/schema.ts DELETED
@@ -1,14 +0,0 @@
1
- import { log } from '@stacksjs/logging'
2
- import { Table } from './table'
3
-
4
- export const Schema = {
5
- async createTable(tableName: string, callback: (table: Table) => void): Promise<void> {
6
- const table = new Table()
7
-
8
- callback(table)
9
-
10
- table.execute() // Simulate the execution of the table creation
11
-
12
- log.success(`Table "${tableName}" created.`)
13
- },
14
- }