@stacksjs/database 0.66.0 → 0.68.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/dist/index.js +53 -56
- package/package.json +11 -13
- package/dist/index.js.map +0 -605
- package/src/column.ts +0 -36
- package/src/drivers/index.ts +0 -258
- package/src/drivers/mysql.ts +0 -389
- package/src/drivers/postgres.ts +0 -284
- package/src/drivers/sqlite.ts +0 -433
- package/src/index.ts +0 -6
- package/src/migrations.ts +0 -150
- package/src/schema.ts +0 -14
- package/src/seeder.ts +0 -166
- package/src/table.ts +0 -32
- package/src/types.ts +0 -5
- package/src/utils.ts +0 -62
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
|
-
}
|
package/src/drivers/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/drivers/mysql.ts
DELETED
|
@@ -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
|
-
}
|