@stacksjs/database 0.61.23 → 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.
@@ -1,17 +1,22 @@
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 { getTableName, getModelName } from '@stacksjs/orm'
4
+ import { getModelName, getTableName } from '@stacksjs/orm'
5
5
  import { path } from '@stacksjs/path'
6
6
  import { fs, glob } from '@stacksjs/storage'
7
+ import { snakeCase } from '@stacksjs/strings'
7
8
  import type { Attribute, Attributes, Model } from '@stacksjs/types'
8
9
  import {
10
+ arrangeColumns,
9
11
  checkPivotMigration,
10
12
  fetchOtherModelRelations,
13
+ findDifferingKeys,
11
14
  getLastMigrationFields,
12
15
  getPivotTables,
13
16
  hasTableBeenMigrated,
17
+ isArrayEqual,
14
18
  mapFieldTypeToColumnType,
19
+ pluckChanges,
15
20
  } from '.'
16
21
 
17
22
  export async function resetSqliteDatabase() {
@@ -123,7 +128,6 @@ async function createTableMigration(modelPath: string): Promise<void> {
123
128
 
124
129
  await createPivotTableMigration(model, modelPath)
125
130
  const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
126
- const fields = model.attributes
127
131
  const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
128
132
  const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
129
133
 
@@ -134,16 +138,16 @@ async function createTableMigration(modelPath: string): Promise<void> {
134
138
  migrationContent += ` .createTable('${tableName}')\n`
135
139
  migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
136
140
 
137
- for (const [fieldName, options] of Object.entries(fields)) {
141
+ for (const [fieldName, options] of arrangeColumns(model.attributes)) {
138
142
  const fieldOptions = options as Attribute
139
- const columnType = mapFieldTypeToColumnType(fieldOptions.validator?.rule)
143
+ const columnType = mapFieldTypeToColumnType(fieldOptions.validations?.rule)
140
144
  migrationContent += ` .addColumn('${fieldName}', '${columnType}'`
141
145
 
142
146
  // Check if there are configurations that require the lambda function
143
- if (fieldOptions.unique || fieldOptions.validator?.rule?.required) {
147
+ if (fieldOptions.unique || fieldOptions.validations?.rule?.required) {
144
148
  migrationContent += `, col => col`
145
149
  if (fieldOptions.unique) migrationContent += `.unique()`
146
- if (fieldOptions.validator?.rule?.required) migrationContent += `.notNull()`
150
+ if (fieldOptions.validations?.rule?.required) migrationContent += `.notNull()`
147
151
  migrationContent += ``
148
152
  }
149
153
 
@@ -217,41 +221,103 @@ export async function createAlterTableMigration(modelPath: string) {
217
221
  const model = (await import(modelPath)).default as Model
218
222
  const modelName = getModelName(model, modelPath)
219
223
  const tableName = await getTableName(model, modelPath)
220
-
224
+ let hasChanged = false
221
225
  // Assuming you have a function to get the fields from the last migration
222
226
  // For simplicity, this is not implemented here
223
227
  const lastMigrationFields = await getLastMigrationFields(modelName)
224
228
  const lastFields = lastMigrationFields ?? {}
225
229
  const currentFields = model.attributes as Attributes
226
230
 
227
- // Determine fields to add and remove
228
- const fieldsToAdd = Object.keys(currentFields)
229
- const fieldsToRemove = Object.keys(lastFields)
231
+ const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
232
+
233
+ const fieldsToAdd = changes?.added || []
234
+
235
+ const fieldsToRemove = changes?.removed || []
230
236
 
231
237
  let migrationContent = `import type { Database } from '@stacksjs/database'\n`
232
238
  migrationContent += `import { sql } from '@stacksjs/database'\n\n`
233
239
  migrationContent += `export async function up(db: Database<any>) {\n`
234
240
  migrationContent += ` await db.schema.alterTable('${tableName}')\n`
235
241
 
242
+ if (fieldsToAdd.length || fieldsToRemove.length) {
243
+ hasChanged = true
244
+ migrationContent += ` await db.schema.alterTable('${tableName}')\n`
245
+ }
246
+
247
+ const fieldValidations = findDifferingKeys(lastFields, currentFields)
248
+
249
+ for (const fieldValidation of fieldValidations) {
250
+ hasChanged = true
251
+ const fieldNameFormatted = snakeCase(fieldValidation.key)
252
+ migrationContent += `await sql\`
253
+ ALTER TABLE ${tableName}
254
+ MODIFY COLUMN ${fieldNameFormatted} VARCHAR(${fieldValidation.max})
255
+ \`.execute(db)\n\n`
256
+ }
257
+
236
258
  // Add new fields
237
259
  for (const fieldName of fieldsToAdd) {
238
260
  const options = currentFields[fieldName] as Attribute
239
- const columnType = mapFieldTypeToColumnType(options.validator?.rule)
240
- migrationContent += ` .addColumn('${fieldName}', '${columnType}')\n`
241
- }
261
+ const columnType = mapFieldTypeToColumnType(options.validation?.rule)
262
+ const formattedFieldName = snakeCase(fieldName)
242
263
 
264
+ migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`
265
+
266
+ // Check if there are configurations that require the lambda function
267
+ if (options.unique || options?.required) {
268
+ migrationContent += `, col => col`
269
+ if (options.unique) migrationContent += `.unique()`
270
+ if (options?.required) migrationContent += `.notNull()`
271
+ migrationContent += ``
272
+ }
273
+
274
+ migrationContent += `)\n\n`
275
+ }
243
276
  // Remove fields that no longer exist
244
277
  for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
245
278
 
279
+ if (fieldsToAdd.length || fieldsToRemove.length) migrationContent += ` .execute();\n`
280
+
281
+ const lastFieldOrder = Object.values(lastFields).map((attr) => attr.order)
282
+ const currentFieldOrder = Object.values(currentFields).map((attr) => attr.order)
283
+
284
+ if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
285
+ hasChanged = true
286
+ migrationContent += reArrangeColumns(model.attributes, tableName)
287
+ }
288
+
246
289
  migrationContent += ` .execute();\n`
290
+
247
291
  migrationContent += `}\n`
248
292
 
249
293
  const timestamp = new Date().getTime().toString()
250
294
  const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
251
295
  const migrationFilePath = path.userMigrationsPath(migrationFileName)
252
296
 
253
- // Assuming fs.writeFileSync is available or use an equivalent method
254
- Bun.write(migrationFilePath, migrationContent)
297
+ if (hasChanged) {
298
+ Bun.write(migrationFilePath, migrationContent)
255
299
 
256
- log.success(`Created migration: ${italic(migrationFileName)}`)
300
+ log.success(`Created migration: ${italic(migrationFileName)}`)
301
+ }
302
+
303
+ function reArrangeColumns(attributes: Attributes | undefined, tableName: string): string {
304
+ const fields = arrangeColumns(attributes)
305
+ let migrationContent = ''
306
+
307
+ let previousField = ''
308
+ for (const [fieldName, options] of fields) {
309
+ const fieldNameFormatted = snakeCase(fieldName)
310
+
311
+ if (previousField) {
312
+ migrationContent += `await sql\`
313
+ ALTER TABLE ${tableName}
314
+ MODIFY COLUMN ${fieldNameFormatted} VARCHAR(255) NOT NULL AFTER ${snakeCase(previousField)};
315
+ \`.execute(db)\n\n`
316
+ }
317
+
318
+ previousField = fieldNameFormatted
319
+ }
320
+
321
+ return migrationContent
322
+ }
257
323
  }
package/src/seeder.ts CHANGED
@@ -3,7 +3,7 @@ import { db } from '@stacksjs/database'
3
3
  import { modelTableName } from '@stacksjs/orm'
4
4
  import { path } from '@stacksjs/path'
5
5
  import { fs, glob } from '@stacksjs/storage'
6
- import { snakeCase } from '@stacksjs/strings'
6
+ import { singular, snakeCase } from '@stacksjs/strings'
7
7
  import type { Model, RelationConfig } from '@stacksjs/types'
8
8
  import { isString } from '@stacksjs/validation'
9
9
  import { generateMigrations, resetDatabase, runDatabaseMigration } from './migrations'
@@ -31,9 +31,11 @@ async function seedModel(name: string, model?: Model) {
31
31
  const record: any = {}
32
32
 
33
33
  for (const fieldName in model.attributes) {
34
+ const formattedFieldName = snakeCase(fieldName)
34
35
  const field = model.attributes[fieldName]
36
+
35
37
  // Use the factory function if available, otherwise leave the field undefined
36
- record[fieldName] = field?.factory ? field.factory() : undefined
38
+ record[formattedFieldName] = field?.factory ? field.factory() : undefined
37
39
  }
38
40
 
39
41
  if (otherRelations?.length) {
@@ -60,9 +62,10 @@ async function seedModelRelation(modelName: string): Promise<BigInt | number> {
60
62
  const table = modelInstance.table
61
63
 
62
64
  for (const fieldName in modelInstance.attributes) {
65
+ const formattedFieldName = snakeCase(fieldName)
63
66
  const field = modelInstance.attributes[fieldName]
64
67
  // Use the factory function if available, otherwise leave the field undefined
65
- record[fieldName] = field?.factory ? field.factory() : undefined
68
+ record[formattedFieldName] = field?.factory ? field.factory() : undefined
66
69
  }
67
70
 
68
71
  const data = await db.insertInto(table).values(record).executeTakeFirstOrThrow()
@@ -97,7 +100,7 @@ export async function getRelations(model: Model): Promise<RelationConfig[]> {
97
100
  relationName: relationInstance.relationName || '',
98
101
  throughModel: relationInstance.through || '',
99
102
  throughForeignKey: relationInstance.throughForeignKey || '',
100
- pivotTable: relationInstance?.pivotTable || `${formattedModelName}_${modelRelation.table}`,
103
+ pivotTable: relationInstance?.pivotTable || getPivotTableName(formattedModelName || '', modelRelation.table),
101
104
  })
102
105
  }
103
106
  }
@@ -106,6 +109,21 @@ export async function getRelations(model: Model): Promise<RelationConfig[]> {
106
109
  return relationships
107
110
  }
108
111
 
112
+ function getPivotTableName(formattedModelName: string, modelRelationTable: string): string {
113
+ // Create an array of the model names
114
+ const models = [formattedModelName, modelRelationTable]
115
+
116
+ // Sort the array alphabetically
117
+ models.sort()
118
+
119
+ models[0] = singular(models[0] || '')
120
+
121
+ // Join the sorted array with an underscore
122
+ const pivotTableName = models.join('_')
123
+
124
+ return pivotTableName
125
+ }
126
+
109
127
  export async function fetchOtherModelRelations(model: Model): Promise<RelationConfig[]> {
110
128
  const modelFiles = glob.sync(path.userModelsPath('*.ts'))
111
129
  const modelRelations = []