@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.
package/src/column.ts DELETED
@@ -1,36 +0,0 @@
1
- interface Options {
2
- notNull?: boolean
3
- default?: any
4
- primaryKey?: boolean
5
- autoIncrement?: boolean
6
- }
7
-
8
- type ColumnType = 'integer' | 'varchar' | 'timestamp' | `varchar(${number})`
9
-
10
- export class Column {
11
- constructor(
12
- public name: string,
13
- public type: ColumnType,
14
- public options: Options = {},
15
- ) {}
16
-
17
- notNullable(): this {
18
- this.options.notNull = true
19
- return this
20
- }
21
-
22
- defaultTo(value: any): this {
23
- this.options.default = value
24
- return this
25
- }
26
-
27
- primary(): this {
28
- this.options.primaryKey = true
29
- return this
30
- }
31
-
32
- autoIncrement(): this {
33
- this.options.autoIncrement = true
34
- return this
35
- }
36
- }
@@ -1,258 +0,0 @@
1
- import type { Attribute, Attributes, Model, VineType } from '@stacksjs/types'
2
- import { log } from '@stacksjs/cli'
3
- import { db } from '@stacksjs/database'
4
- import { handleError } from '@stacksjs/error-handling'
5
- import { getTableName } from '@stacksjs/orm'
6
- import { path } from '@stacksjs/path'
7
- import { fs, globSync } from '@stacksjs/storage'
8
- import { plural, snakeCase } from '@stacksjs/strings'
9
-
10
- export * from './mysql'
11
- export * from './postgres'
12
- export * from './sqlite'
13
-
14
- interface Range {
15
- min: number
16
- max: number
17
- }
18
-
19
- export async function getLastMigrationFields(modelName: string): Promise<Attributes> {
20
- const oldModelPath = path.frameworkPath(`database/models/${modelName}`)
21
- const model = (await import(oldModelPath)).default as Model
22
- let fields = {} as Attributes
23
-
24
- if (typeof model.attributes === 'object')
25
- fields = model.attributes
26
- else fields = JSON.parse(model.attributes || '{}') as Attributes
27
-
28
- return fields
29
- }
30
-
31
- export async function modelTableName(model: Model | string): Promise<string> {
32
- if (typeof model === 'string') {
33
- model = (await import(model)).default as Model
34
- }
35
-
36
- return model.table ?? snakeCase(plural(model?.name || ''))
37
- }
38
-
39
- export async function hasTableBeenMigrated(tableName: string): Promise<boolean> {
40
- log.debug(`hasTableBeenMigrated for table: ${tableName}`)
41
-
42
- const results = await getExecutedMigrations()
43
-
44
- return results.some(migration => migration.name.includes(tableName))
45
- }
46
-
47
- export async function getExecutedMigrations(): Promise<{ name: string }[]> {
48
- try {
49
- return await db.selectFrom('migrations').select('name').execute()
50
- }
51
-
52
- catch (error) {
53
- handleError(error, { shouldExitProcess: false })
54
- return []
55
- }
56
- }
57
-
58
- function hasFunction(rule: VineType, functionName: string): boolean {
59
- return typeof rule[functionName] === 'function'
60
- }
61
-
62
- export function mapFieldTypeToColumnType(rule: VineType, driver = 'mysql'): string {
63
- if (hasFunction(rule, 'getChoices')) {
64
- if (driver === 'sqlite') {
65
- return `'text'`
66
- }
67
-
68
- // Condition checker if an attribute is enum, could not think any conditions atm
69
- const enumChoices = rule.getChoices() as string[]
70
-
71
- // Convert each string value to its corresponding string structure
72
- const enumStructure = enumChoices.map(value => `'${value}'`).join(', ')
73
-
74
- // Construct the ENUM definition
75
- const enumDefinition = `sql\`enum(${enumStructure})\``
76
-
77
- return enumDefinition
78
- }
79
-
80
- if (rule[Symbol.for('schema_name')].includes('string'))
81
- // Default column type for strings
82
- return prepareTextColumnType(rule)
83
-
84
- if (rule[Symbol.for('schema_name')].includes('number'))
85
- return `'integer'`
86
- if (rule[Symbol.for('schema_name')].includes('boolean'))
87
- return `'boolean'`
88
- if (rule[Symbol.for('schema_name')].includes('date'))
89
- return `'date'`
90
-
91
- // need to now handle all other types
92
-
93
- // Add cases for other types as needed, similar to the original function
94
- switch (rule) {
95
- case 'integer':
96
- return `'int'`
97
- case 'boolean':
98
- return `'boolean'`
99
- case 'date':
100
- return `'date'`
101
- case 'datetime':
102
- return `'timestamp'`
103
- case 'float':
104
- return `'float'`
105
- case 'decimal':
106
- return `'decimal'`
107
- default:
108
- return `'text'` // Fallback for unknown types
109
- }
110
- }
111
-
112
- export function prepareTextColumnType(rule: VineType) {
113
- let columnType = 'varchar(255)'
114
-
115
- // Find min and max length validations
116
- const minLengthValidation = rule.validations.find((v: any) => v.options?.min !== undefined)
117
- const maxLengthValidation = rule.validations.find((v: any) => v.options?.max !== undefined)
118
-
119
- // If there's a max length validation, adjust the column type accordingly
120
- if (maxLengthValidation) {
121
- const maxLength = maxLengthValidation.options.max
122
-
123
- columnType = `varchar(${maxLength})`
124
- }
125
-
126
- // If there's only a min length validation and no max, consider using text
127
- // This is a simplistic approach; adjust based on your actual requirements
128
- if (minLengthValidation && !maxLengthValidation)
129
- columnType = 'text'
130
-
131
- return `'${columnType}'`
132
- }
133
-
134
- export function findCharacterLength(rule: VineType): { min: number, max: number } | undefined {
135
- const result: any = {}
136
-
137
- // Find min and max length validations
138
- const minLengthValidation = rule.validations.find((v: any) => v.options?.min !== undefined)
139
- const maxLengthValidation = rule.validations.find((v: any) => v.options?.max !== undefined)
140
-
141
- if (minLengthValidation === undefined || maxLengthValidation === undefined) {
142
- return undefined
143
- }
144
-
145
- for (const key of ['min', 'max']) {
146
- if (maxLengthValidation.options[key] === undefined && minLengthValidation.options[key] === undefined)
147
- continue
148
-
149
- result.max = maxLengthValidation.options[key]
150
- result.min = minLengthValidation.options[key]
151
- }
152
-
153
- // if (minLengthValidation.options[key] !== maxLengthValidation.options[key]) {
154
- // result[key] = maxLengthValidation.options[key];
155
- // }
156
- return result
157
- }
158
-
159
- export function compareRanges(range1: Range, range2: Range): boolean {
160
- return range1.min === range2.min && range1.max === range2.max
161
- }
162
-
163
- export async function checkPivotMigration(dynamicPart: string): Promise<boolean> {
164
- const files = await fs.readdir(path.userMigrationsPath())
165
-
166
- return files.some((migrationFile) => {
167
- // Escape special characters in the dynamic part to ensure it's treated as a literal string
168
- const escapedDynamicPart = dynamicPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
169
-
170
- // Construct the regular expression pattern dynamically
171
- const pattern = new RegExp(`(-${escapedDynamicPart}-)`)
172
-
173
- // Test if the input string matches the pattern
174
- return pattern.test(migrationFile)
175
- })
176
- }
177
-
178
- export function pluckChanges(array1: string[], array2: string[]): { added: string[], removed: string[] } | null {
179
- const removed = array1.filter(item => !array2.includes(item))
180
- const added = array2.filter(item => !array1.includes(item))
181
-
182
- if (removed.length === 0 && added.length === 0) {
183
- return null
184
- }
185
-
186
- return { added, removed }
187
- }
188
-
189
- export function arrangeColumns(attributes: Attributes | undefined): Array<[string, Attribute]> {
190
- if (!attributes)
191
- return []
192
-
193
- const entries = Object.entries(attributes)
194
-
195
- // Sort the entries based on the 'order' property
196
-
197
- // eslint-disable-next-line unused-imports/no-unused-vars
198
- entries.sort(([keyA, valueA], [keyB, valueB]) => {
199
- const orderA = valueA.order ?? Number.POSITIVE_INFINITY
200
- const orderB = valueB.order ?? Number.POSITIVE_INFINITY
201
- return orderA - orderB
202
- })
203
-
204
- return entries // Return the sorted key-value pairs
205
- }
206
-
207
- export function isArrayEqual(arr1: (number | undefined)[], arr2: (number | undefined)[]): boolean {
208
- if (!arr1 || !arr2) {
209
- return false
210
- }
211
-
212
- if (arr1.length !== arr2.length)
213
- return false
214
-
215
- for (let i = 0; i < arr1.length; i++) {
216
- if (arr1[i] !== arr2[i])
217
- return false
218
- }
219
-
220
- return true
221
- }
222
-
223
- export function findDifferingKeys(obj1: any, obj2: any): { key: string, max: number, min: number }[] {
224
- const differingKeys: { key: string, max: number, min: number }[] = []
225
-
226
- for (const key in obj1) {
227
- if (Object.prototype.hasOwnProperty.call(obj1, key) && Object.prototype.hasOwnProperty.call(obj2, key)) {
228
- const lastCharacterLength = findCharacterLength(obj1[key].validation.rule)
229
- const latestCharacterLength = findCharacterLength(obj2[key].validation.rule)
230
-
231
- if (lastCharacterLength !== undefined && latestCharacterLength !== undefined) {
232
- if (
233
- lastCharacterLength.max !== latestCharacterLength.max
234
- || lastCharacterLength.min !== latestCharacterLength.min
235
- ) {
236
- differingKeys.push({ key, max: latestCharacterLength.max, min: latestCharacterLength.min })
237
- }
238
- }
239
- }
240
- }
241
-
242
- return differingKeys
243
- }
244
-
245
- export async function fetchTables(): Promise<string[]> {
246
- const modelFiles = globSync(path.userModelsPath('*.ts'), { absolute: true })
247
-
248
- const tables: string[] = []
249
-
250
- for (const modelPath of modelFiles) {
251
- const model = (await import(modelPath)).default as Model
252
- const tableName = getTableName(model, modelPath)
253
-
254
- tables.push(tableName)
255
- }
256
-
257
- return tables
258
- }
@@ -1,389 +0,0 @@
1
- import type { Attribute, Attributes, Model } from '@stacksjs/types'
2
- import { italic, log } from '@stacksjs/cli'
3
- import { db } from '@stacksjs/database'
4
- import { type Ok, ok } from '@stacksjs/error-handling'
5
- import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from '@stacksjs/orm'
6
- import { path } from '@stacksjs/path'
7
-
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 resetMysqlDatabase(): Promise<Ok<string, never>> {
23
- const tables = await fetchTables()
24
-
25
- for (const table of tables) await db.schema.dropTable(table).ifExists().execute()
26
-
27
- await db.schema.dropTable('migrations').ifExists().execute()
28
- await db.schema.dropTable('migration_locks').ifExists().execute()
29
- await db.schema.dropTable('migrations').ifExists().execute()
30
-
31
- const files = await fs.readdir(path.userMigrationsPath())
32
- const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
33
- const userModelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
34
-
35
- for (const userModel of userModelFiles) {
36
- const model = (await import(userModel)).default as Model
37
- const pivotTables = await getPivotTables(model, userModel)
38
-
39
- for (const pivotTable of pivotTables) await db.schema.dropTable(pivotTable.table).ifExists().execute()
40
- }
41
-
42
- if (modelFiles.length) {
43
- for (const modelFile of modelFiles) {
44
- if (modelFile.endsWith('.ts')) {
45
- const modelPath = path.frameworkPath(`database/models/${modelFile}`)
46
-
47
- if (fs.existsSync(modelPath))
48
- await Bun.$`rm ${modelPath}`
49
- }
50
- }
51
- }
52
-
53
- if (files.length) {
54
- for (const file of files) {
55
- if (file.endsWith('.ts')) {
56
- const migrationPath = path.userMigrationsPath(`${file}`)
57
-
58
- if (fs.existsSync(migrationPath))
59
- await Bun.$`rm ${migrationPath}`
60
- }
61
- }
62
- }
63
-
64
- return ok('All tables dropped successfully!')
65
- }
66
-
67
- export async function generateMysqlMigration(modelPath: string): Promise<void> {
68
- // check if any files are in the database folder
69
- // const files = await fs.readdir(path.userMigrationsPath())
70
-
71
- // if (files.length === 0) {
72
- // log.debug('No migrations found in the database folder, deleting all framework/database/*.json files...')
73
-
74
- // // delete the *.ts files in the database/models folder
75
- // const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
76
-
77
- // if (modelFiles.length) {
78
- // log.debug('No existing model files in framework path...')
79
-
80
- // for (const file of modelFiles) {
81
- // if (file.endsWith('.ts')) await fs.unlink(path.frameworkPath(`database/models/${file}`))
82
- // }
83
- // }
84
- // }
85
-
86
- const model = (await import(modelPath)).default as Model
87
- const fileName = path.basename(modelPath)
88
- const tableName = getTableName(model, modelPath)
89
-
90
- const fieldsString = JSON.stringify(model.attributes, null, 2) // Pretty print the JSON
91
- const copiedModelPath = path.frameworkPath(`database/models/${fileName}`)
92
-
93
- let haveFieldsChanged = false
94
-
95
- // if the file exists, we need to check if the fields have changed
96
- if (fs.existsSync(copiedModelPath)) {
97
- log.info(`Fields have already been generated for ${tableName}`)
98
-
99
- const previousFields = await getLastMigrationFields(fileName)
100
- const previousFieldsString = JSON.stringify(previousFields, null, 2) // Convert to string for comparison
101
-
102
- if (previousFieldsString === fieldsString) {
103
- log.debug(`Fields have not changed for ${tableName}`)
104
- return
105
- }
106
-
107
- haveFieldsChanged = true
108
- log.debug(`Fields have changed for ${tableName}`)
109
- }
110
- else {
111
- log.debug(`Fields have not been generated for ${tableName}`)
112
- }
113
-
114
- // store the fields of the model to a file
115
- await Bun.$`cp ${modelPath} ${copiedModelPath}`
116
-
117
- // if the fields have changed, we need to create a new update migration
118
- // if the fields have not changed, we need to migrate the table
119
-
120
- // we need to check if this tableName has already been migrated
121
- const hasBeenMigrated = await hasTableBeenMigrated(tableName as string)
122
-
123
- log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`)
124
-
125
- if (haveFieldsChanged)
126
- await createAlterTableMigration(modelPath)
127
- else await createTableMigration(modelPath)
128
- }
129
-
130
- async function createTableMigration(modelPath: string): Promise<void> {
131
- log.debug('createTableMigration modelPath:', modelPath)
132
-
133
- const model = (await import(modelPath)).default as Model
134
- const tableName = getTableName(model, modelPath)
135
-
136
- const twoFactorEnabled
137
- = model.traits?.useAuth && typeof model.traits.useAuth !== 'boolean' ? model.traits.useAuth.useTwoFactor : false
138
-
139
- await createPivotTableMigration(model, modelPath)
140
- const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
141
-
142
- const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
143
- const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
144
-
145
- const usePasskey = (typeof model.traits?.useAuth === 'object' && model.traits.useAuth.usePasskey) ?? false
146
-
147
- if (usePasskey)
148
- await createPasskeyMigration()
149
-
150
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
151
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
152
- migrationContent += `export async function up(db: Database<any>) {\n`
153
- migrationContent += ` await db.schema\n`
154
- migrationContent += ` .createTable('${tableName}')\n`
155
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
156
-
157
- for (const [fieldName, options] of arrangeColumns(model.attributes)) {
158
- const fieldOptions = options as Attribute
159
- const fieldNameFormatted = snakeCase(fieldName)
160
- const columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule)
161
- migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`
162
-
163
- // Check if there are configurations that require the lambda function
164
- if (fieldOptions.unique || fieldOptions?.required) {
165
- migrationContent += `, col => col`
166
- if (fieldOptions.unique)
167
- migrationContent += `.unique()`
168
- if (fieldOptions?.required)
169
- migrationContent += `.notNull()`
170
- migrationContent += ``
171
- }
172
-
173
- migrationContent += `)\n`
174
- }
175
-
176
- if (twoFactorEnabled !== false && twoFactorEnabled) {
177
- migrationContent += ` .addColumn('two_factor_secret', 'varchar(255)')\n`
178
- }
179
-
180
- if (otherModelRelations?.length) {
181
- for (const modelRelation of otherModelRelations) {
182
- migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
183
- col.references('${modelRelation.relationTable}.id').onDelete('cascade')
184
- ) \n`
185
- }
186
- }
187
-
188
- if (usePasskey)
189
- migrationContent += ` .addColumn('public_passkey', 'text')\n`
190
-
191
- // Append created_at and updated_at columns if useTimestamps is true
192
- if (useTimestamps) {
193
- migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))\n`
194
- migrationContent += ` .addColumn('updated_at', 'timestamp')\n`
195
- }
196
-
197
- // Append deleted_at column if useSoftDeletes is true
198
- if (useSoftDeletes)
199
- migrationContent += ` .addColumn('deleted_at', 'timestamp')\n`
200
-
201
- migrationContent += ` .execute()\n`
202
- migrationContent += `}\n`
203
-
204
- const timestamp = new Date().getTime().toString()
205
- const migrationFileName = `${timestamp}-create-${tableName}-table.ts`
206
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
207
-
208
- // eslint-disable-next-line no-console
209
- console.log(migrationFilePath)
210
-
211
- Bun.write(migrationFilePath, migrationContent)
212
-
213
- log.success(`Created migration: ${italic(migrationFileName)}`)
214
- }
215
-
216
- async function createPasskeyMigration() {
217
- const hasBeenMigrated = await hasTableBeenMigrated('users')
218
-
219
- if (hasBeenMigrated)
220
- return
221
-
222
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
223
- migrationContent += `export async function up(db: Database<any>) {\n`
224
- migrationContent += ` await db.schema\n`
225
- migrationContent += ` .createTable('passkeys')\n`
226
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
227
- migrationContent += ` .addColumn('cred_public_key', 'text')\n`
228
- migrationContent += ` .addColumn('user_id', 'integer')\n`
229
- migrationContent += ` .addColumn('webauthn_user_id', 'varchar(255)')\n`
230
- migrationContent += ` .addColumn('counter', 'integer')\n`
231
- migrationContent += ` .addColumn('device_type', 'varchar(255)')\n`
232
- migrationContent += ` .addColumn('backup_eligible', 'boolean')\n`
233
- migrationContent += ` .addColumn('backup_status', 'boolean')\n`
234
- migrationContent += ` .addColumn('transports', 'varchar(255)')\n`
235
- migrationContent += ` .addColumn('last_used_at', 'text')\n`
236
- migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))\n`
237
- migrationContent += ` .execute()\n`
238
- migrationContent += ` }\n`
239
-
240
- const timestamp = new Date().getTime().toString()
241
- const migrationFileName = `${timestamp}-create-passkeys-table.ts`
242
-
243
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
244
-
245
- Bun.write(migrationFilePath, migrationContent)
246
-
247
- log.success(`Created pivot migration: ${migrationFileName}`)
248
- }
249
-
250
- async function createPivotTableMigration(model: Model, modelPath: string): Promise<void> {
251
- const pivotTables = await getPivotTables(model, modelPath)
252
-
253
- if (!pivotTables.length)
254
- return
255
-
256
- for (const pivotTable of pivotTables) {
257
- const hasBeenMigrated = await checkPivotMigration(pivotTable.table)
258
-
259
- if (hasBeenMigrated)
260
- return
261
-
262
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
263
- migrationContent += `export async function up(db: Database<any>) {\n`
264
- migrationContent += ` await db.schema\n`
265
- migrationContent += ` .createTable('${pivotTable.table}')\n`
266
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
267
- migrationContent += ` .addColumn('${pivotTable.firstForeignKey}', 'integer')\n`
268
- migrationContent += ` .addColumn('${pivotTable.secondForeignKey}', 'integer')\n`
269
- migrationContent += ` .execute()\n`
270
- migrationContent += ` }\n`
271
-
272
- const timestamp = new Date().getTime().toString()
273
- const migrationFileName = `${timestamp}-create-${pivotTable.table}-table.ts`
274
-
275
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
276
-
277
- Bun.write(migrationFilePath, migrationContent)
278
-
279
- log.success(`Created pivot migration: ${migrationFileName}`)
280
- }
281
- }
282
-
283
- export async function createAlterTableMigration(modelPath: string): Promise<void> {
284
- const model = (await import(modelPath)).default as Model
285
- const modelName = getModelName(model, modelPath)
286
- const tableName = getTableName(model, modelPath)
287
- let hasChanged = false
288
-
289
- // Assuming you have a function to get the fields from the last migration
290
- // For simplicity, this is not implemented here
291
- const lastMigrationFields = await getLastMigrationFields(modelName)
292
- const lastFields = lastMigrationFields ?? {}
293
- const currentFields = model.attributes as Attributes
294
-
295
- // Determine fields to add and remove
296
-
297
- const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
298
-
299
- const fieldsToAdd = changes?.added || []
300
-
301
- const fieldsToRemove = changes?.removed || []
302
-
303
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
304
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
305
- migrationContent += `export async function up(db: Database<any>) {\n`
306
-
307
- if (fieldsToAdd.length || fieldsToRemove.length) {
308
- hasChanged = true
309
- migrationContent += ` await db.schema.alterTable('${tableName}')\n`
310
- }
311
-
312
- const fieldValidations = findDifferingKeys(lastFields, currentFields)
313
- for (const fieldValidation of fieldValidations) {
314
- hasChanged = true
315
- const fieldNameFormatted = snakeCase(fieldValidation.key)
316
- migrationContent += `await sql\`
317
- ALTER TABLE ${tableName}
318
- MODIFY COLUMN ${fieldNameFormatted} VARCHAR(${fieldValidation.max})
319
- \`.execute(db)\n\n`
320
- }
321
-
322
- // Add new fields
323
- for (const fieldName of fieldsToAdd) {
324
- const options = currentFields[fieldName] as Attribute
325
- const columnType = mapFieldTypeToColumnType(options.validation?.rule)
326
- const formattedFieldName = snakeCase(fieldName)
327
-
328
- migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`
329
-
330
- // Check if there are configurations that require the lambda function
331
- if (options.unique || options?.required) {
332
- migrationContent += `, col => col`
333
- if (options.unique)
334
- migrationContent += `.unique()`
335
- if (options?.required)
336
- migrationContent += `.notNull()`
337
- migrationContent += ``
338
- }
339
-
340
- migrationContent += `)\n\n`
341
- }
342
-
343
- // Remove fields that no longer exist
344
- for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
345
-
346
- if (fieldsToAdd.length || fieldsToRemove.length)
347
- migrationContent += ` .execute();\n`
348
-
349
- const lastFieldOrder = Object.values(lastFields).map(attr => attr.order)
350
- const currentFieldOrder = Object.values(currentFields).map(attr => attr.order)
351
-
352
- if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
353
- hasChanged = true
354
- migrationContent += reArrangeColumns(model.attributes, tableName)
355
- }
356
-
357
- migrationContent += `}\n`
358
-
359
- const timestamp = new Date().getTime().toString()
360
- const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
361
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
362
-
363
- if (hasChanged) {
364
- Bun.write(migrationFilePath, migrationContent)
365
-
366
- log.success(`Created migration: ${italic(migrationFileName)}`)
367
- }
368
- }
369
-
370
- function reArrangeColumns(attributes: Attributes | undefined, tableName: string): string {
371
- const fields = arrangeColumns(attributes)
372
- let migrationContent = ''
373
-
374
- let previousField = ''
375
- for (const [fieldName] of fields) {
376
- const fieldNameFormatted = snakeCase(fieldName)
377
-
378
- if (previousField) {
379
- migrationContent += `await sql\`
380
- ALTER TABLE ${tableName}
381
- MODIFY COLUMN ${fieldNameFormatted} VARCHAR(255) NOT NULL AFTER ${snakeCase(previousField)};
382
- \`.execute(db)\n\n`
383
- }
384
-
385
- previousField = fieldNameFormatted
386
- }
387
-
388
- return migrationContent
389
- }