@stacksjs/database 0.61.24 → 0.63.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() {
@@ -115,15 +120,17 @@ export async function generateSqliteMigration(modelPath: string) {
115
120
  else await createTableMigration(modelPath)
116
121
  }
117
122
 
118
- async function createTableMigration(modelPath: string): Promise<void> {
123
+ async function createTableMigration(modelPath: string) {
119
124
  log.debug('createTableMigration modelPath:', modelPath)
120
125
 
121
126
  const model = (await import(modelPath)).default as Model
122
127
  const tableName = await getTableName(model, modelPath)
123
128
 
129
+ const twoFactorEnabled = model.traits?.useAuth?.useTwoFactor
130
+
124
131
  await createPivotTableMigration(model, modelPath)
125
132
  const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
126
- const fields = model.attributes
133
+
127
134
  const useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? true
128
135
  const useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? false
129
136
 
@@ -134,26 +141,31 @@ async function createTableMigration(modelPath: string): Promise<void> {
134
141
  migrationContent += ` .createTable('${tableName}')\n`
135
142
  migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())\n`
136
143
 
137
- for (const [fieldName, options] of Object.entries(fields)) {
144
+ for (const [fieldName, options] of arrangeColumns(model.attributes)) {
138
145
  const fieldOptions = options as Attribute
139
- const columnType = mapFieldTypeToColumnType(fieldOptions.validator?.rule)
140
- migrationContent += ` .addColumn('${fieldName}', '${columnType}'`
146
+ const fieldNameFormatted = snakeCase(fieldName)
147
+ const columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule)
148
+ migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`
141
149
 
142
150
  // Check if there are configurations that require the lambda function
143
- if (fieldOptions.unique || fieldOptions.validator?.rule?.required) {
151
+ if (fieldOptions.unique || fieldOptions?.required) {
144
152
  migrationContent += `, col => col`
145
153
  if (fieldOptions.unique) migrationContent += `.unique()`
146
- if (fieldOptions.validator?.rule?.required) migrationContent += `.notNull()`
154
+ if (fieldOptions?.required) migrationContent += `.notNull()`
147
155
  migrationContent += ``
148
156
  }
149
157
 
150
158
  migrationContent += `)\n`
151
159
  }
152
160
 
161
+ if (false !== twoFactorEnabled && twoFactorEnabled) {
162
+ migrationContent += ` .addColumn('two_factor_secret', 'varchar(255)')\n`
163
+ }
164
+
153
165
  if (otherModelRelations?.length) {
154
166
  for (const modelRelation of otherModelRelations) {
155
167
  migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
156
- col.references('${modelRelation.relationTable}.id').onDelete('cascade').notNull()
168
+ col.references('${modelRelation.relationTable}.id').onDelete('cascade')
157
169
  ) \n`
158
170
  }
159
171
  }
@@ -174,7 +186,6 @@ async function createTableMigration(modelPath: string): Promise<void> {
174
186
  const migrationFileName = `${timestamp}-create-${tableName}-table.ts`
175
187
  const migrationFilePath = path.userMigrationsPath(migrationFileName)
176
188
 
177
- // Assuming fs.writeFileSync is available or use an equivalent method
178
189
  Bun.write(migrationFilePath, migrationContent)
179
190
 
180
191
  log.success(`Created migration: ${italic(migrationFileName)}`)
@@ -204,7 +215,6 @@ async function createPivotTableMigration(model: Model, modelPath: string) {
204
215
  const migrationFileName = `${timestamp}-create-${pivotTable.table}-table.ts`
205
216
  const migrationFilePath = path.userMigrationsPath(migrationFileName)
206
217
 
207
- // Assuming fs.writeFileSync is available or use an equivalent method
208
218
  Bun.write(migrationFilePath, migrationContent)
209
219
 
210
220
  log.success(`Created pivot migration: ${migrationFileName}`)
@@ -217,6 +227,7 @@ export async function createAlterTableMigration(modelPath: string) {
217
227
  const model = (await import(modelPath)).default as Model
218
228
  const modelName = getModelName(model, modelPath)
219
229
  const tableName = await getTableName(model, modelPath)
230
+ let hasChanged = false
220
231
 
221
232
  // Assuming you have a function to get the fields from the last migration
222
233
  // For simplicity, this is not implemented here
@@ -225,33 +236,95 @@ export async function createAlterTableMigration(modelPath: string) {
225
236
  const currentFields = model.attributes as Attributes
226
237
 
227
238
  // Determine fields to add and remove
228
- const fieldsToAdd = Object.keys(currentFields)
229
- const fieldsToRemove = Object.keys(lastFields)
239
+
240
+ const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
241
+
242
+ const fieldsToAdd = changes?.added || []
243
+
244
+ const fieldsToRemove = changes?.removed || []
230
245
 
231
246
  let migrationContent = `import type { Database } from '@stacksjs/database'\n`
232
247
  migrationContent += `import { sql } from '@stacksjs/database'\n\n`
233
248
  migrationContent += `export async function up(db: Database<any>) {\n`
234
- migrationContent += ` await db.schema.alterTable('${tableName}')\n`
249
+
250
+ if (fieldsToAdd.length || fieldsToRemove.length) {
251
+ hasChanged = true
252
+ migrationContent += ` await db.schema.alterTable('${tableName}')\n`
253
+ }
254
+
255
+ const fieldValidations = findDifferingKeys(lastFields, currentFields)
256
+
257
+ for (const fieldValidation of fieldValidations) {
258
+ hasChanged = true
259
+ const fieldNameFormatted = snakeCase(fieldValidation.key)
260
+ migrationContent += `await sql\`
261
+ ALTER TABLE ${tableName}
262
+ MODIFY COLUMN ${fieldNameFormatted} VARCHAR(${fieldValidation.max})
263
+ \`.execute(db)\n\n`
264
+ }
235
265
 
236
266
  // Add new fields
237
267
  for (const fieldName of fieldsToAdd) {
238
268
  const options = currentFields[fieldName] as Attribute
239
- const columnType = mapFieldTypeToColumnType(options.validator?.rule)
240
- migrationContent += ` .addColumn('${fieldName}', '${columnType}')\n`
269
+ const columnType = mapFieldTypeToColumnType(options.validation?.rule)
270
+ const formattedFieldName = snakeCase(fieldName)
271
+
272
+ migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`
273
+
274
+ // Check if there are configurations that require the lambda function
275
+ if (options.unique || options?.required) {
276
+ migrationContent += `, col => col`
277
+ if (options.unique) migrationContent += `.unique()`
278
+ if (options?.required) migrationContent += `.notNull()`
279
+ migrationContent += ``
280
+ }
281
+
282
+ migrationContent += `)\n\n`
241
283
  }
242
284
 
243
285
  // Remove fields that no longer exist
244
286
  for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
245
287
 
246
- migrationContent += ` .execute();\n`
288
+ if (fieldsToAdd.length || fieldsToRemove.length) migrationContent += ` .execute();\n`
289
+
290
+ const lastFieldOrder = Object.values(lastFields).map((attr) => attr.order)
291
+ const currentFieldOrder = Object.values(currentFields).map((attr) => attr.order)
292
+
293
+ if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
294
+ hasChanged = true
295
+ migrationContent += reArrangeColumns(model.attributes, tableName)
296
+ }
297
+
247
298
  migrationContent += `}\n`
248
299
 
249
300
  const timestamp = new Date().getTime().toString()
250
301
  const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
251
302
  const migrationFilePath = path.userMigrationsPath(migrationFileName)
252
303
 
253
- // Assuming fs.writeFileSync is available or use an equivalent method
254
- Bun.write(migrationFilePath, migrationContent)
304
+ if (hasChanged) {
305
+ Bun.write(migrationFilePath, migrationContent)
255
306
 
256
- log.success(`Created migration: ${italic(migrationFileName)}`)
307
+ log.success(`Created migration: ${italic(migrationFileName)}`)
308
+ }
309
+ }
310
+
311
+ function reArrangeColumns(attributes: Attributes | undefined, tableName: string): string {
312
+ const fields = arrangeColumns(attributes)
313
+ let migrationContent = ''
314
+
315
+ let previousField = ''
316
+ for (const [fieldName, options] of fields) {
317
+ const fieldNameFormatted = snakeCase(fieldName)
318
+
319
+ if (previousField) {
320
+ migrationContent += `await sql\`
321
+ ALTER TABLE ${tableName}
322
+ MODIFY COLUMN ${fieldNameFormatted} VARCHAR(255) NOT NULL AFTER ${snakeCase(previousField)};
323
+ \`.execute(db)\n\n`
324
+ }
325
+
326
+ previousField = fieldNameFormatted
327
+ }
328
+
329
+ return migrationContent
257
330
  }
package/src/migrations.ts CHANGED
@@ -88,6 +88,7 @@ export async function generateMigrations() {
88
88
 
89
89
  for (const file of modelFiles) {
90
90
  log.debug('Generating migration for:', file)
91
+
91
92
  await generateMigration(file)
92
93
  }
93
94
 
@@ -99,11 +100,11 @@ export async function generateMigrations() {
99
100
  }
100
101
 
101
102
  export async function generateMigration(modelPath: string) {
102
- if (driver === 'sqlite') generateSqliteMigration(modelPath)
103
+ if (driver === 'sqlite') await generateSqliteMigration(modelPath)
103
104
 
104
- if (driver === 'mysql') generateMysqlMigration(modelPath)
105
+ if (driver === 'mysql') await generateMysqlMigration(modelPath)
105
106
 
106
- if (driver === 'postgres') generatePostgresMigration(modelPath)
107
+ if (driver === 'postgres') await generatePostgresMigration(modelPath)
107
108
  }
108
109
 
109
110
  export async function getExecutedMigrations() {
package/src/seeder.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { italic, log } from '@stacksjs/cli'
2
2
  import { db } from '@stacksjs/database'
3
3
  import { modelTableName } from '@stacksjs/orm'
4
+ import { getModelName, getTableName } from '@stacksjs/orm'
4
5
  import { path } from '@stacksjs/path'
6
+ import { makeHash } from '@stacksjs/security'
5
7
  import { fs, glob } from '@stacksjs/storage'
6
- import { snakeCase } from '@stacksjs/strings'
8
+ import { plural, singular, snakeCase } from '@stacksjs/strings'
7
9
  import type { Model, RelationConfig } from '@stacksjs/types'
8
10
  import { isString } from '@stacksjs/validation'
11
+
9
12
  import { generateMigrations, resetDatabase, runDatabaseMigration } from './migrations'
10
13
 
11
14
  async function seedModel(name: string, model?: Model) {
@@ -25,21 +28,27 @@ async function seedModel(name: string, model?: Model) {
25
28
  const records = []
26
29
  const otherRelations = await fetchOtherModelRelations(model)
27
30
 
28
- log.debug(otherRelations)
29
-
30
31
  for (let i = 0; i < seedCount; i++) {
31
32
  const record: any = {}
32
33
 
33
34
  for (const fieldName in model.attributes) {
35
+ const formattedFieldName = snakeCase(fieldName)
34
36
  const field = model.attributes[fieldName]
37
+
35
38
  // Use the factory function if available, otherwise leave the field undefined
36
- record[fieldName] = field?.factory ? field.factory() : undefined
39
+ if (formattedFieldName === 'password')
40
+ record[formattedFieldName] = field?.factory ? await makeHash('Test@123', { algorithm: 'bcrypt' }) : undefined
41
+ else record[formattedFieldName] = field?.factory ? field.factory() : undefined
37
42
  }
38
43
 
39
44
  if (otherRelations?.length) {
40
45
  for (let j = 0; j < otherRelations.length; j++) {
41
46
  const relationElement = otherRelations[j] as RelationConfig
42
47
 
48
+ if (relationElement.relationship === 'belongsToMany') {
49
+ await seedPivotRelation(relationElement)
50
+ }
51
+
43
52
  record[relationElement?.foreignKey] = await seedModelRelation(relationElement?.relationModel as string)
44
53
  }
45
54
  }
@@ -51,6 +60,51 @@ async function seedModel(name: string, model?: Model) {
51
60
  await db.insertInto(tableName).values(records).execute()
52
61
  }
53
62
 
63
+ async function seedPivotRelation(relation: RelationConfig): Promise<any> {
64
+ const record: any = {}
65
+ const record2: any = {}
66
+ const pivotRecord: any = {}
67
+
68
+ const modelInstance = (await import(path.userModelsPath(relation?.model))).default
69
+ const relationModelInstance = (await import(path.userModelsPath(relation?.relationModel))).default
70
+
71
+ if (!relationModelInstance) return 1
72
+
73
+ const relationModelTable = relationModelInstance.table
74
+ const relationTable = relation.table
75
+ const pivotTable = relation.pivotTable
76
+ const modelKey = `${singular(relationTable)}_id`
77
+ const foreignKey = relation.foreignKey
78
+
79
+ for (const fieldName in relationModelInstance.attributes) {
80
+ const formattedFieldName = snakeCase(fieldName)
81
+ const field = relationModelInstance.attributes[fieldName]
82
+ // Use the factory function if available, otherwise leave the field undefined
83
+ if (formattedFieldName === 'password')
84
+ record[formattedFieldName] = field?.factory ? await makeHash('Test@123', { algorithm: 'bcrypt' }) : undefined
85
+ else record[formattedFieldName] = field?.factory ? field.factory() : undefined
86
+ }
87
+
88
+ for (const fieldName in modelInstance.attributes) {
89
+ const formattedFieldName = snakeCase(fieldName)
90
+ const field = modelInstance.attributes[fieldName]
91
+ // Use the factory function if available, otherwise leave the field undefined
92
+ if (formattedFieldName === 'password')
93
+ record2[formattedFieldName] = field?.factory ? await makeHash('Test@123', { algorithm: 'bcrypt' }) : undefined
94
+ else record2[formattedFieldName] = field?.factory ? field.factory() : undefined
95
+ }
96
+
97
+ const data = await db.insertInto(relationModelTable).values(record).executeTakeFirstOrThrow()
98
+ const data2 = await db.insertInto(relationTable).values(record2).executeTakeFirstOrThrow()
99
+ const relationData = data.insertId || 1
100
+ const modelData = data2.insertId || 1
101
+
102
+ pivotRecord[foreignKey] = relationData
103
+ pivotRecord[modelKey] = modelData
104
+
105
+ if (pivotTable) await db.insertInto(pivotTable).values(pivotRecord).executeTakeFirstOrThrow()
106
+ }
107
+
54
108
  async function seedModelRelation(modelName: string): Promise<BigInt | number> {
55
109
  const modelInstance = (await import(path.userModelsPath(modelName))).default
56
110
 
@@ -60,9 +114,13 @@ async function seedModelRelation(modelName: string): Promise<BigInt | number> {
60
114
  const table = modelInstance.table
61
115
 
62
116
  for (const fieldName in modelInstance.attributes) {
117
+ const formattedFieldName = snakeCase(fieldName)
63
118
  const field = modelInstance.attributes[fieldName]
64
119
  // Use the factory function if available, otherwise leave the field undefined
65
- record[fieldName] = field?.factory ? field.factory() : undefined
120
+
121
+ if (formattedFieldName === 'password')
122
+ record[formattedFieldName] = field?.factory ? await makeHash(field.factory(), { algorithm: 'bcrypt' }) : undefined
123
+ else record[formattedFieldName] = field?.factory ? field.factory() : undefined
66
124
  }
67
125
 
68
126
  const data = await db.insertInto(table).values(record).executeTakeFirstOrThrow()
@@ -70,10 +128,13 @@ async function seedModelRelation(modelName: string): Promise<BigInt | number> {
70
128
  return data.insertId || 1
71
129
  }
72
130
 
73
- export async function getRelations(model: Model): Promise<RelationConfig[]> {
131
+ export async function getRelations(model: Model, modelPath: string): Promise<RelationConfig[]> {
74
132
  const relationsArray = ['hasOne', 'hasMany', 'belongsToMany', 'hasOneThrough']
75
133
  const relationships = []
76
134
 
135
+ const modelName = getModelName(model, modelPath)
136
+ const tableName = getTableName(model, modelPath)
137
+
77
138
  for (const relation of relationsArray) {
78
139
  if (hasRelations(model, relation)) {
79
140
  for (const relationInstance of model[relation]) {
@@ -85,19 +146,19 @@ export async function getRelations(model: Model): Promise<RelationConfig[]> {
85
146
 
86
147
  const modelRelationPath = path.userModelsPath(`${relationModel}.ts`)
87
148
  const modelRelation = (await import(modelRelationPath)).default
88
- const formattedModelName = model.name?.toLowerCase()
149
+ const formattedModelName = modelName?.toLowerCase()
89
150
 
90
151
  relationships.push({
91
152
  relationship: relation,
92
153
  model: relationModel,
93
154
  table: modelRelation.table,
94
- relationModel: model.name,
95
- relationTable: model.table,
155
+ relationModel: modelName,
156
+ relationTable: tableName,
96
157
  foreignKey: relationInstance.foreignKey || `${formattedModelName}_id`,
97
- relationName: relationInstance.relationName || '',
98
- throughModel: relationInstance.through || '',
99
- throughForeignKey: relationInstance.throughForeignKey || '',
100
- pivotTable: relationInstance?.pivotTable || `${formattedModelName}_${modelRelation.table}`,
158
+ relationName: relationInstance.relationName,
159
+ throughModel: relationInstance.through,
160
+ throughForeignKey: relationInstance.throughForeignKey,
161
+ pivotTable: relationInstance?.pivotTable || getPivotTableName(formattedModelName || '', modelRelation.table),
101
162
  })
102
163
  }
103
164
  }
@@ -106,6 +167,22 @@ export async function getRelations(model: Model): Promise<RelationConfig[]> {
106
167
  return relationships
107
168
  }
108
169
 
170
+ function getPivotTableName(formattedModelName: string, modelRelationTable: string): string {
171
+ // Create an array of the model names
172
+ const models = [formattedModelName, modelRelationTable]
173
+
174
+ // Sort the array alphabetically
175
+ models.sort()
176
+
177
+ models[0] = singular(models[0] || '')
178
+ models[1] = plural(models[1] || '')
179
+
180
+ // Join the sorted array with an underscore
181
+ const pivotTableName = models.join('_')
182
+
183
+ return pivotTableName
184
+ }
185
+
109
186
  export async function fetchOtherModelRelations(model: Model): Promise<RelationConfig[]> {
110
187
  const modelFiles = glob.sync(path.userModelsPath('*.ts'))
111
188
  const modelRelations = []
@@ -116,7 +193,7 @@ export async function fetchOtherModelRelations(model: Model): Promise<RelationCo
116
193
 
117
194
  if (model.name === modelFile.default.name) continue
118
195
 
119
- const relations = await getRelations(modelFile.default)
196
+ const relations = await getRelations(modelFile.default, modelFileElement)
120
197
 
121
198
  if (!relations.length) continue
122
199
 
@@ -134,21 +211,21 @@ function hasRelations(obj: any, key: string): boolean {
134
211
 
135
212
  export async function seed() {
136
213
  // TODO: need to check other databases too
137
- const dbPath = path.userDatabasePath('stacks.sqlite')
214
+ // const dbPath = path.userDatabasePath('stacks.sqlite')
138
215
 
139
- if (!fs.existsSync(dbPath)) {
140
- log.warn('No database found, configuring it...')
141
- // first, ensure the database is reset
142
- await resetDatabase()
216
+ // if (!fs.existsSync(dbPath)) {
217
+ // log.warn('No database found, configuring it...')
218
+ // // first, ensure the database is reset
219
+ // await resetDatabase()
143
220
 
144
- // then, generate the migrations
145
- await generateMigrations()
221
+ // // then, generate the migrations
222
+ // await generateMigrations()
146
223
 
147
- // finally, migrate the database
148
- await runDatabaseMigration()
149
- } else {
150
- log.debug('Database configured...')
151
- }
224
+ // // finally, migrate the database
225
+ // await runDatabaseMigration()
226
+ // } else {
227
+ // log.debug('Database configured...')
228
+ // }
152
229
 
153
230
  // if a custom seeder exists, use it instead
154
231
  const customSeederPath = path.userDatabasePath('seeder.ts')
package/src/utils.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { database } from '@stacksjs/config'
2
2
  import { log } from '@stacksjs/logging'
3
3
  import type { Database } from '@stacksjs/orm'
4
+ import { plural, snakeCase } from '@stacksjs/strings'
5
+ import type { Model } from '@stacksjs/types'
4
6
  import { Kysely, MysqlDialect, PostgresDialect, sql } from 'kysely'
5
7
  import { BunWorkerDialect } from 'kysely-bun-worker'
6
8
  import { createPool } from 'mysql2'