@stacksjs/database 0.61.24 → 0.62.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 +100452 -11921
- package/package.json +5 -5
- package/src/drivers/index.ts +137 -12
- package/src/drivers/mysql.ts +91 -19
- package/src/drivers/postgres.ts +17 -12
- package/src/drivers/sqlite.ts +82 -16
- package/src/seeder.ts +22 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.62.0",
|
|
5
5
|
"description": "The Stacks database integration.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"license": "MIT",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"@stacksjs/storage": "latest",
|
|
58
58
|
"@stacksjs/strings": "latest",
|
|
59
59
|
"@stacksjs/utils": "latest",
|
|
60
|
-
"kysely-bun-worker": "^0.6.
|
|
60
|
+
"kysely-bun-worker": "^0.6.2"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@stacksjs/cli": "latest",
|
|
@@ -68,10 +68,10 @@
|
|
|
68
68
|
"@stacksjs/storage": "latest",
|
|
69
69
|
"@stacksjs/strings": "latest",
|
|
70
70
|
"@stacksjs/utils": "latest",
|
|
71
|
-
"kysely-bun-worker": "^0.6.
|
|
71
|
+
"kysely-bun-worker": "^0.6.2"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
|
-
"mysql2": "^3.10.
|
|
74
|
+
"mysql2": "^3.10.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
77
|
"@stacksjs/development": "latest",
|
|
@@ -79,6 +79,6 @@
|
|
|
79
79
|
"debug": "^4.3.5",
|
|
80
80
|
"mkdirp": "^3.0.1",
|
|
81
81
|
"q": "^1.5.1",
|
|
82
|
-
"tar": "^7.
|
|
82
|
+
"tar": "^7.4.0"
|
|
83
83
|
}
|
|
84
84
|
}
|
package/src/drivers/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { db } from '@stacksjs/database'
|
|
|
3
3
|
import { getModelName, getTableName } from '@stacksjs/orm'
|
|
4
4
|
import { path } from '@stacksjs/path'
|
|
5
5
|
import { fs, glob } from '@stacksjs/storage'
|
|
6
|
-
import { plural, snakeCase } from '@stacksjs/strings'
|
|
6
|
+
import { plural, singular, snakeCase } from '@stacksjs/strings'
|
|
7
7
|
import type { Attributes, Model, RelationConfig, VineType } from '@stacksjs/types'
|
|
8
8
|
import { isString } from '@stacksjs/validation'
|
|
9
9
|
|
|
@@ -11,6 +11,11 @@ export * from './mysql'
|
|
|
11
11
|
export * from './postgres'
|
|
12
12
|
export * from './sqlite'
|
|
13
13
|
|
|
14
|
+
interface Range {
|
|
15
|
+
min: number
|
|
16
|
+
max: number
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export async function getLastMigrationFields(modelName: string): Promise<Attributes> {
|
|
15
20
|
const oldModelPath = path.frameworkPath(`database/models/${modelName}`)
|
|
16
21
|
const model = (await import(oldModelPath)).default as Model
|
|
@@ -116,6 +121,34 @@ export function prepareTextColumnType(rule: VineType) {
|
|
|
116
121
|
return `'${columnType}'`
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
export function findCharacterLength(rule: VineType): { min: number; max: number } | undefined {
|
|
125
|
+
const result: any = {}
|
|
126
|
+
|
|
127
|
+
// Find min and max length validations
|
|
128
|
+
const minLengthValidation = rule.validations.find((v: any) => v.options?.min !== undefined)
|
|
129
|
+
const maxLengthValidation = rule.validations.find((v: any) => v.options?.max !== undefined)
|
|
130
|
+
|
|
131
|
+
if (minLengthValidation === undefined || maxLengthValidation === undefined) {
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const key of ['min', 'max']) {
|
|
136
|
+
if (maxLengthValidation.options[key] === undefined && minLengthValidation.options[key] === undefined) continue
|
|
137
|
+
|
|
138
|
+
result.max = maxLengthValidation.options[key]
|
|
139
|
+
result.min = minLengthValidation.options[key]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// if (minLengthValidation.options[key] !== maxLengthValidation.options[key]) {
|
|
143
|
+
// result[key] = maxLengthValidation.options[key];
|
|
144
|
+
// }
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function compareRanges(range1: Range, range2: Range): boolean {
|
|
149
|
+
return range1.min === range2.min && range1.max === range2.max
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
export async function checkPivotMigration(dynamicPart: string): Promise<boolean> {
|
|
120
153
|
const files = await fs.readdir(path.userMigrationsPath())
|
|
121
154
|
|
|
@@ -147,7 +180,6 @@ export async function getRelations(model: Model, modelPath: string): Promise<Rel
|
|
|
147
180
|
const modelRelationPath = path.userModelsPath(`${relationModel}.ts`)
|
|
148
181
|
const modelRelation = (await import(modelRelationPath)).default
|
|
149
182
|
|
|
150
|
-
|
|
151
183
|
const modelName = getModelName(model, modelPath)
|
|
152
184
|
const formattedModelName = modelName.toLowerCase()
|
|
153
185
|
const tableName = getTableName(model, modelPath)
|
|
@@ -164,7 +196,7 @@ export async function getRelations(model: Model, modelPath: string): Promise<Rel
|
|
|
164
196
|
relationName: relationInstance.relationName || '',
|
|
165
197
|
throughModel: relationInstance.through || '',
|
|
166
198
|
throughForeignKey: relationInstance.throughForeignKey || '',
|
|
167
|
-
pivotTable: relationInstance?.pivotTable ||
|
|
199
|
+
pivotTable: relationInstance?.pivotTable || getPivotTableName(formattedModelName, modelRelationTable),
|
|
168
200
|
})
|
|
169
201
|
}
|
|
170
202
|
}
|
|
@@ -204,31 +236,48 @@ export async function fetchOtherModelRelations(model: Model, modelPath: string):
|
|
|
204
236
|
|
|
205
237
|
export async function getPivotTables(
|
|
206
238
|
model: Model,
|
|
207
|
-
modelPath: string
|
|
239
|
+
modelPath: string,
|
|
208
240
|
): Promise<{ table: string; firstForeignKey: string | undefined; secondForeignKey: string | undefined }[]> {
|
|
209
241
|
const pivotTable = []
|
|
210
242
|
|
|
211
243
|
const modelName = getTableName(model, modelPath)
|
|
244
|
+
let firstForeignKey = ''
|
|
245
|
+
let secondForeignKey = ''
|
|
246
|
+
let table = ''
|
|
247
|
+
let modelRelationPath = ''
|
|
212
248
|
|
|
213
249
|
if (model.belongsToMany) {
|
|
214
250
|
if ('belongsToMany' in model) {
|
|
215
251
|
for (const belongsToManyRelation of model.belongsToMany) {
|
|
216
|
-
|
|
252
|
+
if (typeof belongsToManyRelation === 'string') {
|
|
253
|
+
modelRelationPath = path.userModelsPath(`${belongsToManyRelation}.ts`)
|
|
254
|
+
} else {
|
|
255
|
+
modelRelationPath = path.userModelsPath(`${belongsToManyRelation.model}.ts`)
|
|
256
|
+
}
|
|
257
|
+
|
|
217
258
|
const modelRelation = (await import(modelRelationPath)).default
|
|
218
259
|
const formattedModelName = modelName.toLowerCase()
|
|
219
260
|
|
|
220
261
|
const modelRelationTable = getTableName(modelRelation, modelRelationPath)
|
|
221
262
|
const modelRelationModelName = getModelName(modelRelation, modelRelationPath)
|
|
222
263
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
if (typeof belongsToManyRelation === 'string') {
|
|
265
|
+
firstForeignKey = `${singular(modelName.toLowerCase())}_${model.primaryKey}`
|
|
266
|
+
secondForeignKey = `${singular(modelRelationModelName.toLowerCase())}_${model.primaryKey}`
|
|
267
|
+
table = getPivotTableName(formattedModelName, modelRelationTable)
|
|
268
|
+
} else {
|
|
269
|
+
firstForeignKey =
|
|
270
|
+
belongsToManyRelation.firstForeignKey || `${singular(modelName.toLowerCase())}_${model.primaryKey}`
|
|
271
|
+
secondForeignKey =
|
|
272
|
+
belongsToManyRelation.secondForeignKey ||
|
|
273
|
+
`${singular(modelRelationModelName.toLowerCase())}_${model.primaryKey}`
|
|
274
|
+
table = belongsToManyRelation?.pivotTable || getPivotTableName(formattedModelName, modelRelationTable)
|
|
275
|
+
}
|
|
227
276
|
|
|
228
277
|
pivotTable.push({
|
|
229
|
-
table
|
|
230
|
-
firstForeignKey
|
|
231
|
-
secondForeignKey
|
|
278
|
+
table,
|
|
279
|
+
firstForeignKey,
|
|
280
|
+
secondForeignKey,
|
|
232
281
|
})
|
|
233
282
|
}
|
|
234
283
|
|
|
@@ -239,6 +288,82 @@ export async function getPivotTables(
|
|
|
239
288
|
return []
|
|
240
289
|
}
|
|
241
290
|
|
|
291
|
+
function getPivotTableName(formattedModelName: string, modelRelationTable: string): string {
|
|
292
|
+
// Create an array of the model names
|
|
293
|
+
const models = [formattedModelName, modelRelationTable]
|
|
294
|
+
|
|
295
|
+
// Sort the array alphabetically
|
|
296
|
+
models.sort()
|
|
297
|
+
|
|
298
|
+
models[0] = singular(models[0] || '')
|
|
299
|
+
|
|
300
|
+
// Join the sorted array with an underscore
|
|
301
|
+
const pivotTableName = models.join('_')
|
|
302
|
+
|
|
303
|
+
return pivotTableName
|
|
304
|
+
}
|
|
305
|
+
|
|
242
306
|
function hasRelations(obj: any, key: string): boolean {
|
|
243
307
|
return key in obj
|
|
244
308
|
}
|
|
309
|
+
|
|
310
|
+
export function pluckChanges(array1: string[], array2: string[]): { added: string[]; removed: string[] } | null {
|
|
311
|
+
const removed = array1.filter((item) => !array2.includes(item))
|
|
312
|
+
const added = array2.filter((item) => !array1.includes(item))
|
|
313
|
+
|
|
314
|
+
if (removed.length === 0 && added.length === 0) {
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { added, removed }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function arrangeColumns(attributes: Attributes | undefined) {
|
|
322
|
+
if (!attributes) return []
|
|
323
|
+
|
|
324
|
+
const entries = Object.entries(attributes)
|
|
325
|
+
|
|
326
|
+
entries.sort(([keyA, valueA], [keyB, valueB]) => {
|
|
327
|
+
const orderA = valueA.order ?? Number.POSITIVE_INFINITY
|
|
328
|
+
const orderB = valueB.order ?? Number.POSITIVE_INFINITY
|
|
329
|
+
return orderA - orderB
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
return entries
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function isArrayEqual(arr1: (number | undefined)[], arr2: (number | undefined)[]): boolean {
|
|
336
|
+
if (!arr1 || !arr2) {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (arr1.length !== arr2.length) return false
|
|
341
|
+
|
|
342
|
+
for (let i = 0; i < arr1.length; i++) {
|
|
343
|
+
if (arr1[i] !== arr2[i]) return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return true
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function findDifferingKeys(obj1: any, obj2: any): { key: string; max: number; min: number }[] {
|
|
350
|
+
const differingKeys: { key: string; max: number; min: number }[] = []
|
|
351
|
+
|
|
352
|
+
for (const key in obj1) {
|
|
353
|
+
if (obj1.hasOwnProperty(key) && obj2.hasOwnProperty(key)) {
|
|
354
|
+
const lastCharacterLength = findCharacterLength(obj1[key].validation.rule)
|
|
355
|
+
const latestCharacterLength = findCharacterLength(obj2[key].validation.rule)
|
|
356
|
+
|
|
357
|
+
if (lastCharacterLength !== undefined && latestCharacterLength !== undefined) {
|
|
358
|
+
if (
|
|
359
|
+
lastCharacterLength.max !== latestCharacterLength.max ||
|
|
360
|
+
lastCharacterLength.min !== latestCharacterLength.min
|
|
361
|
+
) {
|
|
362
|
+
differingKeys.push({ key, max: latestCharacterLength.max, min: latestCharacterLength.min })
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return differingKeys
|
|
369
|
+
}
|
package/src/drivers/mysql.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { italic, log } from '@stacksjs/cli'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
3
|
import { ok } from '@stacksjs/error-handling'
|
|
4
|
+
import { getModelName, getTableName } from '@stacksjs/orm'
|
|
4
5
|
import { path } from '@stacksjs/path'
|
|
5
6
|
import { fs, glob } from '@stacksjs/storage'
|
|
7
|
+
import { snakeCase } from '@stacksjs/strings'
|
|
6
8
|
import type { Attribute, Attributes, Model } from '@stacksjs/types'
|
|
7
9
|
import {
|
|
10
|
+
arrangeColumns,
|
|
8
11
|
checkPivotMigration,
|
|
9
12
|
fetchOtherModelRelations,
|
|
13
|
+
findCharacterLength,
|
|
14
|
+
findDifferingKeys,
|
|
10
15
|
getLastMigrationFields,
|
|
11
16
|
getPivotTables,
|
|
12
17
|
hasTableBeenMigrated,
|
|
18
|
+
isArrayEqual,
|
|
13
19
|
mapFieldTypeToColumnType,
|
|
20
|
+
pluckChanges,
|
|
14
21
|
} from '.'
|
|
15
|
-
import { getModelName, getTableName } from '@stacksjs/orm'
|
|
16
22
|
|
|
17
23
|
export async function resetMysqlDatabase() {
|
|
18
24
|
const tables = await fetchMysqlTables()
|
|
@@ -124,11 +130,11 @@ async function createTableMigration(modelPath: string) {
|
|
|
124
130
|
const model = (await import(modelPath)).default as Model
|
|
125
131
|
const tableName = await getTableName(model, modelPath)
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
const twoFactorEnabled = model.traits?.useAuth?.useTwoFactor
|
|
128
134
|
|
|
135
|
+
await createPivotTableMigration(model, modelPath)
|
|
129
136
|
const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
|
|
130
137
|
|
|
131
|
-
const fields = model.attributes
|
|
132
138
|
const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
|
|
133
139
|
const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
|
|
134
140
|
|
|
@@ -139,22 +145,27 @@ async function createTableMigration(modelPath: string) {
|
|
|
139
145
|
migrationContent += ` .createTable('${tableName}')\n`
|
|
140
146
|
migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
|
|
141
147
|
|
|
142
|
-
for (const [fieldName, options] of
|
|
148
|
+
for (const [fieldName, options] of arrangeColumns(model.attributes)) {
|
|
143
149
|
const fieldOptions = options as Attribute
|
|
144
|
-
const
|
|
145
|
-
|
|
150
|
+
const fieldNameFormatted = snakeCase(fieldName)
|
|
151
|
+
const columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule)
|
|
152
|
+
migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`
|
|
146
153
|
|
|
147
154
|
// Check if there are configurations that require the lambda function
|
|
148
|
-
if (fieldOptions.unique || fieldOptions
|
|
155
|
+
if (fieldOptions.unique || fieldOptions?.required) {
|
|
149
156
|
migrationContent += `, col => col`
|
|
150
157
|
if (fieldOptions.unique) migrationContent += `.unique()`
|
|
151
|
-
if (fieldOptions
|
|
158
|
+
if (fieldOptions?.required) migrationContent += `.notNull()`
|
|
152
159
|
migrationContent += ``
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
migrationContent += `)\n`
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
if (false !== twoFactorEnabled && twoFactorEnabled) {
|
|
166
|
+
migrationContent += ` .addColumn('two_factor_secret', 'varchar(255)')\n`
|
|
167
|
+
}
|
|
168
|
+
|
|
158
169
|
if (otherModelRelations?.length) {
|
|
159
170
|
for (const modelRelation of otherModelRelations) {
|
|
160
171
|
migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
|
|
@@ -179,7 +190,6 @@ async function createTableMigration(modelPath: string) {
|
|
|
179
190
|
const migrationFileName = `${timestamp}-create-${tableName}-table.ts`
|
|
180
191
|
const migrationFilePath = path.userMigrationsPath(migrationFileName)
|
|
181
192
|
|
|
182
|
-
// Assuming fs.writeFileSync is available or use an equivalent method
|
|
183
193
|
Bun.write(migrationFilePath, migrationContent)
|
|
184
194
|
|
|
185
195
|
log.success(`Created migration: ${italic(migrationFileName)}`)
|
|
@@ -209,7 +219,6 @@ async function createPivotTableMigration(model: Model, modelPath: string) {
|
|
|
209
219
|
const migrationFileName = `${timestamp}-create-${pivotTable.table}-table.ts`
|
|
210
220
|
const migrationFilePath = path.userMigrationsPath(migrationFileName)
|
|
211
221
|
|
|
212
|
-
// Assuming fs.writeFileSync is available or use an equivalent method
|
|
213
222
|
Bun.write(migrationFilePath, migrationContent)
|
|
214
223
|
|
|
215
224
|
log.success(`Created pivot migration: ${migrationFileName}`)
|
|
@@ -222,6 +231,7 @@ export async function createAlterTableMigration(modelPath: string) {
|
|
|
222
231
|
const model = (await import(modelPath)).default as Model
|
|
223
232
|
const modelName = getModelName(model, modelPath)
|
|
224
233
|
const tableName = await getTableName(model, modelPath)
|
|
234
|
+
let hasChanged = false
|
|
225
235
|
|
|
226
236
|
// Assuming you have a function to get the fields from the last migration
|
|
227
237
|
// For simplicity, this is not implemented here
|
|
@@ -230,35 +240,97 @@ export async function createAlterTableMigration(modelPath: string) {
|
|
|
230
240
|
const currentFields = model.attributes as Attributes
|
|
231
241
|
|
|
232
242
|
// Determine fields to add and remove
|
|
233
|
-
|
|
234
|
-
const
|
|
243
|
+
|
|
244
|
+
const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
|
|
245
|
+
|
|
246
|
+
const fieldsToAdd = changes?.added || []
|
|
247
|
+
|
|
248
|
+
const fieldsToRemove = changes?.removed || []
|
|
235
249
|
|
|
236
250
|
let migrationContent = `import type { Database } from '@stacksjs/database'\n`
|
|
237
251
|
migrationContent += `import { sql } from '@stacksjs/database'\n\n`
|
|
238
252
|
migrationContent += `export async function up(db: Database<any>) {\n`
|
|
239
|
-
|
|
253
|
+
|
|
254
|
+
if (fieldsToAdd.length || fieldsToRemove.length) {
|
|
255
|
+
hasChanged = true
|
|
256
|
+
migrationContent += ` await db.schema.alterTable('${tableName}')\n`
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const fieldValidations = findDifferingKeys(lastFields, currentFields)
|
|
260
|
+
|
|
261
|
+
for (const fieldValidation of fieldValidations) {
|
|
262
|
+
hasChanged = true
|
|
263
|
+
const fieldNameFormatted = snakeCase(fieldValidation.key)
|
|
264
|
+
migrationContent += `await sql\`
|
|
265
|
+
ALTER TABLE ${tableName}
|
|
266
|
+
MODIFY COLUMN ${fieldNameFormatted} VARCHAR(${fieldValidation.max})
|
|
267
|
+
\`.execute(db)\n\n`
|
|
268
|
+
}
|
|
240
269
|
|
|
241
270
|
// Add new fields
|
|
242
271
|
for (const fieldName of fieldsToAdd) {
|
|
243
272
|
const options = currentFields[fieldName] as Attribute
|
|
244
|
-
const columnType = mapFieldTypeToColumnType(options.
|
|
245
|
-
|
|
273
|
+
const columnType = mapFieldTypeToColumnType(options.validation?.rule)
|
|
274
|
+
const formattedFieldName = snakeCase(fieldName)
|
|
275
|
+
|
|
276
|
+
migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`
|
|
277
|
+
|
|
278
|
+
// Check if there are configurations that require the lambda function
|
|
279
|
+
if (options.unique || options?.required) {
|
|
280
|
+
migrationContent += `, col => col`
|
|
281
|
+
if (options.unique) migrationContent += `.unique()`
|
|
282
|
+
if (options?.required) migrationContent += `.notNull()`
|
|
283
|
+
migrationContent += ``
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
migrationContent += `)\n\n`
|
|
246
287
|
}
|
|
247
288
|
|
|
248
289
|
// Remove fields that no longer exist
|
|
249
290
|
for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
|
|
250
291
|
|
|
251
|
-
migrationContent += ` .execute();\n`
|
|
292
|
+
if (fieldsToAdd.length || fieldsToRemove.length) migrationContent += ` .execute();\n`
|
|
293
|
+
|
|
294
|
+
const lastFieldOrder = Object.values(lastFields).map((attr) => attr.order)
|
|
295
|
+
const currentFieldOrder = Object.values(currentFields).map((attr) => attr.order)
|
|
296
|
+
|
|
297
|
+
if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
|
|
298
|
+
hasChanged = true
|
|
299
|
+
migrationContent += reArrangeColumns(model.attributes, tableName)
|
|
300
|
+
}
|
|
301
|
+
|
|
252
302
|
migrationContent += `}\n`
|
|
253
303
|
|
|
254
304
|
const timestamp = new Date().getTime().toString()
|
|
255
305
|
const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
|
|
256
306
|
const migrationFilePath = path.userMigrationsPath(migrationFileName)
|
|
257
307
|
|
|
258
|
-
|
|
259
|
-
|
|
308
|
+
if (hasChanged) {
|
|
309
|
+
Bun.write(migrationFilePath, migrationContent)
|
|
260
310
|
|
|
261
|
-
|
|
311
|
+
log.success(`Created migration: ${italic(migrationFileName)}`)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function reArrangeColumns(attributes: Attributes | undefined, tableName: string): string {
|
|
316
|
+
const fields = arrangeColumns(attributes)
|
|
317
|
+
let migrationContent = ''
|
|
318
|
+
|
|
319
|
+
let previousField = ''
|
|
320
|
+
for (const [fieldName, options] of fields) {
|
|
321
|
+
const fieldNameFormatted = snakeCase(fieldName)
|
|
322
|
+
|
|
323
|
+
if (previousField) {
|
|
324
|
+
migrationContent += `await sql\`
|
|
325
|
+
ALTER TABLE ${tableName}
|
|
326
|
+
MODIFY COLUMN ${fieldNameFormatted} VARCHAR(255) NOT NULL AFTER ${snakeCase(previousField)};
|
|
327
|
+
\`.execute(db)\n\n`
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
previousField = fieldNameFormatted
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return migrationContent
|
|
262
334
|
}
|
|
263
335
|
|
|
264
336
|
export async function fetchMysqlTables(): Promise<string[]> {
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -4,14 +4,17 @@ import { ok } from '@stacksjs/error-handling'
|
|
|
4
4
|
import { getTableName } from '@stacksjs/orm'
|
|
5
5
|
import { path } from '@stacksjs/path'
|
|
6
6
|
import { fs, glob } from '@stacksjs/storage'
|
|
7
|
-
import
|
|
7
|
+
import { snakeCase } from '@stacksjs/strings'
|
|
8
|
+
import type { Attribute, Attributes, Model } from '@stacksjs/types'
|
|
8
9
|
import {
|
|
10
|
+
arrangeColumns,
|
|
9
11
|
checkPivotMigration,
|
|
10
12
|
fetchOtherModelRelations,
|
|
11
13
|
getLastMigrationFields,
|
|
12
14
|
getPivotTables,
|
|
13
15
|
hasTableBeenMigrated,
|
|
14
16
|
mapFieldTypeToColumnType,
|
|
17
|
+
pluckChanges,
|
|
15
18
|
} from '.'
|
|
16
19
|
|
|
17
20
|
export async function resetPostgresDatabase() {
|
|
@@ -128,7 +131,6 @@ async function createTableMigration(modelPath: string) {
|
|
|
128
131
|
await createPivotTableMigration(model, modelPath)
|
|
129
132
|
|
|
130
133
|
const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
|
|
131
|
-
const fields = model.attributes
|
|
132
134
|
const useTimestamps = model.traits?.useTimestamps ?? model.traits?.timestampable ?? true
|
|
133
135
|
const useSoftDeletes = model.traits?.useSoftDeletes ?? model.traits?.softDeletable ?? false
|
|
134
136
|
|
|
@@ -139,16 +141,17 @@ async function createTableMigration(modelPath: string) {
|
|
|
139
141
|
migrationContent += ` .createTable('${tableName}')\n`
|
|
140
142
|
migrationContent += ` .addColumn('id', 'serial', (col) => col.primaryKey())\n`
|
|
141
143
|
|
|
142
|
-
for (const [fieldName, options] of
|
|
144
|
+
for (const [fieldName, options] of arrangeColumns(model.attributes)) {
|
|
143
145
|
const fieldOptions = options as Attribute
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
+
const fieldNameFormatted = snakeCase(fieldName)
|
|
147
|
+
const columnType = mapFieldTypeToColumnType(fieldOptions.validations?.rule)
|
|
148
|
+
migrationContent += ` .addColumn('${fieldNameFormatted}', '${columnType}'`
|
|
146
149
|
|
|
147
150
|
// Check if there are configurations that require the lambda function
|
|
148
|
-
if (fieldOptions.unique || fieldOptions.
|
|
151
|
+
if (fieldOptions.unique || fieldOptions.validations?.rule?.required) {
|
|
149
152
|
migrationContent += `, col => col`
|
|
150
153
|
if (fieldOptions.unique) migrationContent += `.unique()`
|
|
151
|
-
if (fieldOptions.
|
|
154
|
+
if (fieldOptions.validations?.rule?.required) migrationContent += `.notNull()`
|
|
152
155
|
migrationContent += ``
|
|
153
156
|
}
|
|
154
157
|
|
|
@@ -227,11 +230,13 @@ export async function createAlterTableMigration(modelPath: string) {
|
|
|
227
230
|
// For simplicity, this is not implemented here
|
|
228
231
|
const lastMigrationFields = await getLastMigrationFields(modelName)
|
|
229
232
|
const lastFields = lastMigrationFields ?? {}
|
|
230
|
-
const currentFields = model.attributes
|
|
233
|
+
const currentFields = model.attributes as Attributes
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
+
const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
|
|
236
|
+
|
|
237
|
+
const fieldsToAdd = changes?.added || []
|
|
238
|
+
|
|
239
|
+
const fieldsToRemove = changes?.removed || []
|
|
235
240
|
|
|
236
241
|
let migrationContent = `import type { Database } from '@stacksjs/database'\n`
|
|
237
242
|
migrationContent += `import { sql } from '@stacksjs/database'\n\n`
|
|
@@ -241,7 +246,7 @@ export async function createAlterTableMigration(modelPath: string) {
|
|
|
241
246
|
// Add new fields
|
|
242
247
|
for (const fieldName of fieldsToAdd) {
|
|
243
248
|
const options = currentFields[fieldName] as Attribute
|
|
244
|
-
const columnType = mapFieldTypeToColumnType(options.
|
|
249
|
+
const columnType = mapFieldTypeToColumnType(options.validations?.rule)
|
|
245
250
|
migrationContent += ` .addColumn('${fieldName}', '${columnType}')\n`
|
|
246
251
|
}
|
|
247
252
|
|