@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.
@@ -1,284 +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 { ok } from '@stacksjs/error-handling'
5
- import { fetchOtherModelRelations, getPivotTables, getTableName } from '@stacksjs/orm'
6
- import { path } from '@stacksjs/path'
7
- import { fs, globSync } from '@stacksjs/storage'
8
- import { snakeCase } from '@stacksjs/strings'
9
- import {
10
- arrangeColumns,
11
- checkPivotMigration,
12
- getLastMigrationFields,
13
- hasTableBeenMigrated,
14
- mapFieldTypeToColumnType,
15
- pluckChanges,
16
- } from '.'
17
-
18
- export async function resetPostgresDatabase() {
19
- const tables = await fetchPostgresTables()
20
-
21
- for (const table of tables) await db.schema.dropTable(table).ifExists().execute()
22
-
23
- await db.schema.dropTable('migrations').ifExists().execute()
24
- await db.schema.dropTable('migration_locks').ifExists().execute()
25
-
26
- const files = await fs.readdir(path.userMigrationsPath())
27
- const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
28
-
29
- const userModelFiles = globSync([path.userModelsPath('*.ts')], { absolute: true })
30
-
31
- for (const userModel of userModelFiles) {
32
- const userModelPath = (await import(userModel)).default
33
-
34
- const pivotTables = await getPivotTables(userModelPath, userModelPath)
35
-
36
- for (const pivotTable of pivotTables) await db.schema.dropTable(pivotTable.table).ifExists().execute()
37
- }
38
-
39
- if (modelFiles.length) {
40
- for (const modelFile of modelFiles) {
41
- if (modelFile.endsWith('.ts')) {
42
- const modelPath = path.frameworkPath(`database/models/${modelFile}`)
43
-
44
- if (fs.existsSync(modelPath))
45
- await Bun.$`rm ${modelPath}`
46
- }
47
- }
48
- }
49
-
50
- if (files.length) {
51
- for (const file of files) {
52
- if (file.endsWith('.ts')) {
53
- const migrationPath = path.userMigrationsPath(`${file}`)
54
-
55
- if (fs.existsSync(migrationPath))
56
- await Bun.$`rm ${migrationPath}`
57
- }
58
- }
59
- }
60
-
61
- return ok('All tables dropped successfully!')
62
- }
63
-
64
- export async function generatePostgresMigration(modelPath: string): Promise<void> {
65
- // check if any files are in the database folder
66
- const files = await fs.readdir(path.userMigrationsPath())
67
-
68
- if (files.length === 0) {
69
- log.debug('No migrations found in the database folder, deleting all framework/database/*.json files...')
70
-
71
- // delete the *.ts files in the database/models folder
72
- const modelFiles = await fs.readdir(path.frameworkPath('database/models'))
73
-
74
- if (modelFiles.length) {
75
- log.debug('No existing model files in framework path...')
76
-
77
- for (const file of modelFiles) {
78
- if (file.endsWith('.ts'))
79
- await fs.unlink(path.frameworkPath(`database/models/${file}`))
80
- }
81
- }
82
- }
83
-
84
- const model = (await import(modelPath)).default as Model
85
- const fileName = path.basename(modelPath)
86
- const tableName = getTableName(model, modelPath)
87
-
88
- const fieldsString = JSON.stringify(model.attributes, null, 2) // Pretty print the JSON
89
- const copiedModelPath = path.frameworkPath(`database/models/${fileName}`)
90
-
91
- let haveFieldsChanged = false
92
-
93
- // if the file exists, we need to check if the fields have changed
94
- if (fs.existsSync(copiedModelPath)) {
95
- log.info(`Fields have already been generated for ${tableName}`)
96
-
97
- const previousFields = await getLastMigrationFields(fileName)
98
- const previousFieldsString = JSON.stringify(previousFields, null, 2) // Convert to string for comparison
99
-
100
- if (previousFieldsString === fieldsString) {
101
- log.debug(`Fields have not changed for ${tableName}`)
102
- return
103
- }
104
-
105
- haveFieldsChanged = true
106
- log.debug(`Fields have changed for ${tableName}`)
107
- }
108
- else {
109
- log.debug(`Fields have not been generated for ${tableName}`)
110
- }
111
-
112
- // store the fields of the model to a file
113
- await Bun.$`cp ${modelPath} ${copiedModelPath}`
114
-
115
- // if the fields have changed, we need to create a new update migration
116
- // if the fields have not changed, we need to migrate the table
117
-
118
- // we need to check if this tableName has already been migrated
119
- const hasBeenMigrated = await hasTableBeenMigrated(tableName)
120
-
121
- log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`)
122
-
123
- if (haveFieldsChanged)
124
- await createAlterTableMigration(modelPath)
125
- else await createTableMigration(modelPath)
126
- }
127
-
128
- async function createTableMigration(modelPath: string) {
129
- log.debug('createTableMigration modelPath:', modelPath)
130
-
131
- const model = (await import(modelPath)).default as Model
132
- const tableName = getTableName(model, modelPath)
133
-
134
- await createPivotTableMigration(model, modelPath)
135
-
136
- const otherModelRelations = await fetchOtherModelRelations(model, modelPath)
137
- const useTimestamps = model.traits?.useTimestamps ?? model.traits?.timestampable ?? true
138
- const useSoftDeletes = model.traits?.useSoftDeletes ?? model.traits?.softDeletable ?? false
139
-
140
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
141
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
142
- migrationContent += `export async function up(db: Database<any>) {\n`
143
- migrationContent += ` await db.schema\n`
144
- migrationContent += ` .createTable('${tableName}')\n`
145
- migrationContent += ` .addColumn('id', 'serial', (col) => col.primaryKey())\n`
146
-
147
- for (const [fieldName, options] of arrangeColumns(model.attributes)) {
148
- const fieldOptions = options as Attribute
149
- const fieldNameFormatted = snakeCase(fieldName)
150
- const columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule)
151
- migrationContent += ` .addColumn('${fieldNameFormatted}', '${columnType}'`
152
-
153
- // Check if there are configurations that require the lambda function
154
- if (fieldOptions.unique || fieldOptions.validation?.rule?.required) {
155
- migrationContent += `, col => col`
156
- if (fieldOptions.unique)
157
- migrationContent += `.unique()`
158
- if (fieldOptions.validation?.rule?.required)
159
- migrationContent += `.notNull()`
160
- migrationContent += ``
161
- }
162
-
163
- migrationContent += `)\n`
164
- }
165
-
166
- if (otherModelRelations?.length) {
167
- for (const modelRelation of otherModelRelations) {
168
- migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
169
- col.references('${modelRelation.relationTable}.id').onDelete('cascade').notNull()
170
- ) \n`
171
- }
172
- }
173
-
174
- // Append created_at and updated_at columns if useTimestamps is true
175
- if (useTimestamps) {
176
- migrationContent += ` .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql.raw('CURRENT_TIMESTAMP')))\n`
177
- migrationContent += ` .addColumn('updated_at', 'timestamp')\n`
178
- }
179
-
180
- // Append deleted_at column if useSoftDeletes is true
181
- if (useSoftDeletes)
182
- migrationContent += ` .addColumn('deleted_at', 'timestamp')\n`
183
-
184
- migrationContent += ` .execute()\n`
185
- migrationContent += `}\n`
186
-
187
- const timestamp = new Date().getTime().toString()
188
- const migrationFileName = `${timestamp}-create-${tableName}-table.ts`
189
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
190
-
191
- // Assuming fs.writeFileSync is available or use an equivalent method
192
- Bun.write(migrationFilePath, migrationContent)
193
-
194
- log.success(`Created migration: ${italic(migrationFileName)}`)
195
- }
196
-
197
- async function createPivotTableMigration(model: Model, modelPath: string) {
198
- const pivotTables = await getPivotTables(model, modelPath)
199
-
200
- if (!pivotTables.length)
201
- return
202
- for (const pivotTable of pivotTables) {
203
- const hasBeenMigrated = await checkPivotMigration(pivotTable.table)
204
-
205
- if (hasBeenMigrated)
206
- return
207
-
208
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
209
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
210
- migrationContent += `export async function up(db: Database<any>) {\n`
211
- migrationContent += ` await db.schema\n`
212
- migrationContent += ` .createTable('${pivotTable.table}')\n`
213
- migrationContent += ` .addColumn('id', 'serial', (col) => col.primaryKey())\n`
214
- migrationContent += ` .addColumn('user_id', 'integer')\n`
215
- migrationContent += ` .addColumn('subscriber_id', 'integer')\n`
216
- migrationContent += ` .execute()\n`
217
- migrationContent += ` }\n`
218
-
219
- const timestamp = new Date().getTime().toString()
220
- const migrationFileName = `${timestamp}-create-${pivotTable.table}-table.ts`
221
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
222
-
223
- // Assuming fs.writeFileSync is available or use an equivalent method
224
- Bun.write(migrationFilePath, migrationContent)
225
-
226
- log.success(`Created migration: ${italic(migrationFileName)}`)
227
- }
228
- }
229
-
230
- async function createAlterTableMigration(modelPath: string) {
231
- const model = (await import(modelPath)).default as Model
232
- const modelName = path.basename(modelPath)
233
- const tableName = getTableName(model, modelPath)
234
-
235
- // Assuming you have a function to get the fields from the last migration
236
- // For simplicity, this is not implemented here
237
- const lastMigrationFields = await getLastMigrationFields(modelName)
238
- const lastFields = lastMigrationFields ?? {}
239
- const currentFields = model.attributes as Attributes
240
- const changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields))
241
- const fieldsToAdd = changes?.added || []
242
- const fieldsToRemove = changes?.removed || []
243
-
244
- let migrationContent = `import type { Database } from '@stacksjs/database'\n`
245
- migrationContent += `import { sql } from '@stacksjs/database'\n\n`
246
- migrationContent += `export async function up(db: Database<any>) {\n`
247
- migrationContent += ` await db.schema.alterTable('${tableName}')\n`
248
-
249
- // Add new fields
250
- for (const fieldName of fieldsToAdd) {
251
- const options = currentFields[fieldName] as Attribute
252
- const columnType = mapFieldTypeToColumnType(options.validation?.rule)
253
- migrationContent += ` .addColumn('${fieldName}', '${columnType}')\n`
254
- }
255
-
256
- // Remove fields that no longer exist
257
- for (const fieldName of fieldsToRemove) migrationContent += ` .dropColumn('${fieldName}')\n`
258
-
259
- migrationContent += ` .execute();\n`
260
- migrationContent += `}\n`
261
-
262
- const timestamp = new Date().getTime().toString()
263
- const migrationFileName = `${timestamp}-update-${tableName}-table.ts`
264
- const migrationFilePath = path.userMigrationsPath(migrationFileName)
265
-
266
- // Assuming fs.writeFileSync is available or use an equivalent method
267
- Bun.write(migrationFilePath, migrationContent)
268
-
269
- log.success(`Created migration: ${italic(migrationFileName)}`)
270
- }
271
-
272
- export async function fetchPostgresTables(): Promise<string[]> {
273
- const modelFiles = globSync([path.userModelsPath('*.ts')])
274
- const tables: string[] = []
275
-
276
- for (const modelPath of modelFiles) {
277
- const model = (await import(modelPath)).default
278
- const tableName = getTableName(model, modelPath)
279
-
280
- tables.push(tableName)
281
- }
282
-
283
- return tables
284
- }