@stacksjs/database 0.70.258 → 0.70.260

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.
Files changed (50) hide show
  1. package/dist/auth-tables.js +18 -137
  2. package/dist/column.js +1 -26
  3. package/dist/custom/audits.js +20 -54
  4. package/dist/custom/errors.js +16 -46
  5. package/dist/custom/index.js +1 -3
  6. package/dist/custom/jobs.js +13 -137
  7. package/dist/database.js +1 -181
  8. package/dist/datetime-columns.js +2 -79
  9. package/dist/ddl-constraints.js +7 -111
  10. package/dist/defaults.js +1 -48
  11. package/dist/dialect.js +1 -79
  12. package/dist/driver-config.js +1 -172
  13. package/dist/drivers/defaults/index.js +1 -1
  14. package/dist/drivers/defaults/traits.js +1 -29
  15. package/dist/drivers/dynamodb.js +1 -607
  16. package/dist/drivers/helpers.js +1 -206
  17. package/dist/drivers/index.js +1 -9
  18. package/dist/drivers/mysql.js +58 -299
  19. package/dist/drivers/postgres.js +78 -368
  20. package/dist/drivers/sqlite.js +61 -379
  21. package/dist/ensure-database.js +1 -145
  22. package/dist/fk-audit.js +3 -187
  23. package/dist/index.js +1 -64
  24. package/dist/managed-columns.js +1 -59
  25. package/dist/migration-dialect.js +4 -107
  26. package/dist/migration-ledger.js +1 -382
  27. package/dist/migration-lock.js +1 -143
  28. package/dist/migrations.js +15 -1118
  29. package/dist/model-sources.js +1 -76
  30. package/dist/notification-tables.js +4 -49
  31. package/dist/query-logger.js +2 -241
  32. package/dist/query-parser.js +1 -93
  33. package/dist/rbac-tables.js +6 -61
  34. package/dist/relation-columns.js +1 -66
  35. package/dist/replicas.js +1 -74
  36. package/dist/safe-migrations.js +2 -52
  37. package/dist/schema.js +1 -10
  38. package/dist/seeder.js +1 -457
  39. package/dist/sql-helpers.js +1 -50
  40. package/dist/table.js +1 -26
  41. package/dist/tools/setup.js +1 -6
  42. package/dist/trait-tables.js +8 -153
  43. package/dist/transaction-context.js +1 -62
  44. package/dist/types.js +1 -98
  45. package/dist/unique-audit.js +3 -155
  46. package/dist/utils.js +1 -285
  47. package/dist/uuid-columns.js +1 -68
  48. package/dist/validators.js +1 -122
  49. package/dist/vschema.js +2 -121
  50. package/package.json +20 -13
@@ -1,397 +1,79 @@
1
- import { log } from "@stacksjs/logging";
2
- function italic(str) {
3
- return `\x1B[3m${str}\x1B[23m`;
4
- }
5
- import { app } from "@stacksjs/config";
6
- import { db, SQLITE_BOOTSTRAP_PRAGMAS } from "../utils";
7
- import { ok } from "@stacksjs/error-handling";
8
- import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
9
- import { path } from "@stacksjs/path";
10
- import { fs, globSync } from "@stacksjs/storage";
11
- import { snakeCase } from "@stacksjs/strings";
12
- import {
13
- arrangeColumns,
14
- checkPivotMigration,
15
- deleteFrameworkModels,
16
- deleteMigrationFiles,
17
- fetchTables,
18
- findDifferingKeys,
19
- getLastMigrationFields,
20
- getLikeableForeignKey,
21
- getUpvoteTableName,
22
- isArrayEqual,
23
- mapFieldTypeToColumnType,
24
- pluckChanges
25
- } from "./helpers";
26
- import { dropCommonTables } from "./defaults/traits";
27
- export async function resetSqliteDatabase() {
28
- await deleteFrameworkModels();
29
- await deleteMigrationFiles();
30
- await dropSqliteTables();
31
- return ok("All tables dropped successfully!");
32
- }
33
- export async function configureSqlitePragmas() {
34
- try {
35
- for (const pragma of SQLITE_BOOTSTRAP_PRAGMAS)
36
- await db.unsafe(pragma).execute();
37
- } catch (err) {
38
- log.debug(`[sqlite] Failed to apply pragmas: ${err.message}`);
39
- }
40
- }
41
- export async function dropSqliteTables() {
42
- const userModelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 }), tables = await fetchTables(), safeName = /^[a-z_][\w]*$/i;
43
- await db.unsafe("PRAGMA foreign_keys = OFF").execute();
44
- try {
45
- for (const table of tables) {
46
- if (!safeName.test(table))
47
- throw Error(`[sqlite] Refusing to drop table with unsafe name: ${table}`);
48
- await db.unsafe(`DROP TABLE IF EXISTS "${table}"`).execute();
49
- }
50
- await db.unsafe('DROP TABLE IF EXISTS "migrations"').execute();
51
- await dropCommonTables();
52
- for (const userModel of userModelFiles) {
53
- const userModelPath = (await import(userModel)).default, pivotTables = await getPivotTables(userModelPath, userModel);
54
- for (const pivotTable of pivotTables) {
55
- if (!safeName.test(pivotTable.table))
56
- throw Error(`[sqlite] Refusing to drop pivot table with unsafe name: ${pivotTable.table}`);
57
- await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}"`).execute();
58
- }
59
- }
60
- } finally {
61
- await db.unsafe("PRAGMA foreign_keys = ON").execute();
62
- }
63
- }
64
- export function fetchSqliteFile() {
65
- if (app.env === "testing")
66
- return fetchTestSqliteFile();
67
- return path.userDatabasePath("stacks.sqlite");
68
- }
69
- export function fetchTestSqliteFile() {
70
- return path.userDatabasePath("stacks_testing.sqlite");
71
- }
72
- export async function generateSqliteMigration(modelPath) {
73
- const model = (await import(modelPath)).default, fileName = path.basename(modelPath), tableName = await getTableName(model, modelPath), fieldsString = JSON.stringify(model.attributes, null, 2), copiedModelPath = path.frameworkPath(`cache/models/${fileName}`);
74
- let haveFieldsChanged = !1;
75
- if (fs.existsSync(copiedModelPath)) {
76
- log.debug(`Fields have already been generated for ${tableName}`);
77
- const previousFields = await getLastMigrationFields(fileName);
78
- if (JSON.stringify(previousFields, null, 2) === fieldsString) {
79
- log.debug(`Fields have not changed for ${tableName}`);
80
- return;
81
- }
82
- haveFieldsChanged = !0;
83
- log.debug(`Fields have changed for ${tableName}`);
84
- } else
85
- log.debug(`Fields have not been generated for ${tableName}`);
86
- await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`;
87
- const hasBeenMigrated = !1;
88
- log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);
89
- if (haveFieldsChanged)
90
- await createAlterTableMigration(modelPath);
91
- else
92
- await createTableMigration(modelPath);
93
- }
94
- export async function copyModelFiles(modelPath) {
95
- const model = (await import(modelPath)).default, fileName = path.basename(modelPath), tableName = await getTableName(model, modelPath), fieldsString = JSON.stringify(model.attributes, null, 2), copiedModelPath = path.frameworkPath(`cache/models/${fileName}`);
96
- if (fs.existsSync(copiedModelPath)) {
97
- log.debug(`Fields have already been generated for ${tableName}`);
98
- const previousFields = await getLastMigrationFields(fileName);
99
- if (JSON.stringify(previousFields, null, 2) === fieldsString) {
100
- log.debug(`Fields have not changed for ${tableName}`);
101
- return;
102
- }
103
- }
104
- await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`;
105
- }
106
- async function createTableMigration(modelPath) {
107
- log.debug("createTableMigration modelPath:", modelPath);
108
- const model = (await import(modelPath)).default, tableName = getTableName(model, modelPath), modelName = getModelName(model, modelPath), twoFactorEnabled = model.traits?.useAuth && typeof model.traits.useAuth !== "boolean" ? model.traits.useAuth.useTwoFactor : !1;
109
- await createPivotTableMigration(model, modelPath);
110
- const otherModelRelations = await fetchOtherModelRelations(modelName), useTimestamps = model?.traits?.useTimestamps ?? model?.traits?.timestampable ?? !0, useSocials = model?.traits?.useSocials && Array.isArray(model.traits.useSocials) && model.traits.useSocials.length > 0, useLikeable = Array.isArray(model?.traits?.likeable) ? model.traits.likeable.length > 0 : Boolean(model?.traits?.likeable), useSoftDeletes = model?.traits?.useSoftDeletes ?? model?.traits?.softDeletable ?? !1, usePasskey = (typeof model.traits?.useAuth === "object" && model.traits.useAuth.usePasskey) ?? !1, useBillable = model.traits?.billable || !1, useUuid = model.traits?.useUuid || !1;
111
- let migrationContent = `import type { Database } from '@stacksjs/database'
112
- `;
113
- migrationContent += `import { sql } from '@stacksjs/database'
1
+ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B[23m`}import{app}from"@stacksjs/config";import{db,SQLITE_BOOTSTRAP_PRAGMAS}from"../utils";import{ok}from"@stacksjs/error-handling";import{fetchOtherModelRelations,getModelName,getPivotTables,getTableName}from"@stacksjs/orm";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{snakeCase}from"@stacksjs/strings";import{arrangeColumns,checkPivotMigration,deleteFrameworkModels,deleteMigrationFiles,fetchTables,findDifferingKeys,getLastMigrationFields,getLikeableForeignKey,getUpvoteTableName,isArrayEqual,mapFieldTypeToColumnType,pluckChanges}from"./helpers";import{dropCommonTables}from"./defaults/traits";export async function resetSqliteDatabase(){await deleteFrameworkModels();await deleteMigrationFiles();await dropSqliteTables();return ok("All tables dropped successfully!")}export async function configureSqlitePragmas(){try{for(const pragma of SQLITE_BOOTSTRAP_PRAGMAS)await db.unsafe(pragma).execute()}catch(err){log.debug(`[sqlite] Failed to apply pragmas: ${err.message}`)}}export async function dropSqliteTables(){const userModelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),tables=await fetchTables(),safeName=/^[a-z_][\w]*$/i;await db.unsafe("PRAGMA foreign_keys = OFF").execute();try{for(const table of tables){if(!safeName.test(table))throw Error(`[sqlite] Refusing to drop table with unsafe name: ${table}`);await db.unsafe(`DROP TABLE IF EXISTS "${table}"`).execute()}await db.unsafe('DROP TABLE IF EXISTS "migrations"').execute();await dropCommonTables();for(const userModel of userModelFiles){const userModelPath=(await import(userModel)).default,pivotTables=await getPivotTables(userModelPath,userModel);for(const pivotTable of pivotTables){if(!safeName.test(pivotTable.table))throw Error(`[sqlite] Refusing to drop pivot table with unsafe name: ${pivotTable.table}`);await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}"`).execute()}}}finally{await db.unsafe("PRAGMA foreign_keys = ON").execute()}}export function fetchSqliteFile(){if(app.env==="testing")return fetchTestSqliteFile();return path.userDatabasePath("stacks.sqlite")}export function fetchTestSqliteFile(){return path.userDatabasePath("stacks_testing.sqlite")}export async function generateSqliteMigration(modelPath){const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=await getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);let haveFieldsChanged=!1;if(fs.existsSync(copiedModelPath)){log.debug(`Fields have already been generated for ${tableName}`);const previousFields=await getLastMigrationFields(fileName);if(JSON.stringify(previousFields,null,2)===fieldsString){log.debug(`Fields have not changed for ${tableName}`);return}haveFieldsChanged=!0;log.debug(`Fields have changed for ${tableName}`)}else log.debug(`Fields have not been generated for ${tableName}`);await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`;const hasBeenMigrated=!1;log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);if(haveFieldsChanged)await createAlterTableMigration(modelPath);else await createTableMigration(modelPath)}export async function copyModelFiles(modelPath){const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=await getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);if(fs.existsSync(copiedModelPath)){log.debug(`Fields have already been generated for ${tableName}`);const previousFields=await getLastMigrationFields(fileName);if(JSON.stringify(previousFields,null,2)===fieldsString){log.debug(`Fields have not changed for ${tableName}`);return}}await Bun.$`mkdir -p ${path.frameworkPath("cache/models")} && cp ${modelPath} ${copiedModelPath}`}async function createTableMigration(modelPath){log.debug("createTableMigration modelPath:",modelPath);const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),modelName=getModelName(model,modelPath),twoFactorEnabled=model.traits?.useAuth&&typeof model.traits.useAuth!=="boolean"?model.traits.useAuth.useTwoFactor:!1;await createPivotTableMigration(model,modelPath);const otherModelRelations=await fetchOtherModelRelations(modelName),useTimestamps=model?.traits?.useTimestamps??model?.traits?.timestampable??!0,useSocials=model?.traits?.useSocials&&Array.isArray(model.traits.useSocials)&&model.traits.useSocials.length>0,useLikeable=Array.isArray(model?.traits?.likeable)?model.traits.likeable.length>0:Boolean(model?.traits?.likeable),useSoftDeletes=model?.traits?.useSoftDeletes??model?.traits?.softDeletable??!1,usePasskey=(typeof model.traits?.useAuth==="object"&&model.traits.useAuth.usePasskey)??!1,useBillable=model.traits?.billable||!1,useUuid=model.traits?.useUuid||!1;let migrationContent=`import type { Database } from '@stacksjs/database'
2
+ `;migrationContent+=`import { sql } from '@stacksjs/database'
114
3
 
115
- `;
116
- migrationContent += `export async function up(db: Database<any>) {
117
- `;
118
- migrationContent += ` await (db as any).schema
119
- `;
120
- migrationContent += ` .createTable('${tableName}')
121
- `;
122
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
123
- `;
124
- if (useUuid)
125
- migrationContent += ` .addColumn('uuid', 'text')
126
- `;
127
- if (useSocials) {
128
- const socials = model.traits?.useSocials || [];
129
- if (socials.includes("google"))
130
- migrationContent += ` .addColumn('google_id', 'text')
131
- `;
132
- if (socials.includes("github"))
133
- migrationContent += ` .addColumn('github_id', 'text')
134
- `;
135
- if (socials.includes("apple"))
136
- migrationContent += ` .addColumn('apple_id', 'text')
137
- `;
138
- if (socials.includes("twitter"))
139
- migrationContent += ` .addColumn('twitter_id', 'text')
140
- `;
141
- if (socials.includes("facebook"))
142
- migrationContent += ` .addColumn('facebook_id', 'text')
143
- `;
144
- }
145
- for (const [fieldName, options] of arrangeColumns(model.attributes)) {
146
- const fieldOptions = options, fieldNameFormatted = snakeCase(fieldName), columnType = mapFieldTypeToColumnType(fieldOptions.validation?.rule, "sqlite");
147
- migrationContent += ` .addColumn('${fieldNameFormatted}', ${columnType}`;
148
- const isRequired = fieldOptions.validation?.rule.isRequired;
149
- if (isRequired || fieldOptions.unique || fieldOptions.default !== void 0) {
150
- migrationContent += ", col => col";
151
- if (isRequired)
152
- migrationContent += ".notNull()";
153
- if (fieldOptions.unique)
154
- migrationContent += ".unique()";
155
- if (fieldOptions.default !== void 0)
156
- if (typeof fieldOptions.default === "string")
157
- migrationContent += `.defaultTo('${fieldOptions.default}')`;
158
- else if (fieldOptions.default === null)
159
- migrationContent += ".defaultTo(null)";
160
- else
161
- migrationContent += `.defaultTo(${fieldOptions.default})`;
162
- migrationContent += "";
163
- }
164
- migrationContent += `)
165
- `;
166
- }
167
- if (twoFactorEnabled !== !1 && twoFactorEnabled)
168
- migrationContent += ` .addColumn('two_factor_secret', 'text')
169
- `;
170
- if (useBillable)
171
- migrationContent += ` .addColumn('stripe_id', 'text')
172
- `;
173
- if (useSoftDeletes)
174
- migrationContent += ` .addColumn('deleted_at', 'timestamp')
175
- `;
176
- if (usePasskey)
177
- migrationContent += ` .addColumn('public_passkey', 'text')
178
- `;
179
- if (otherModelRelations?.length)
180
- for (const modelRelation of otherModelRelations) {
181
- if (!modelRelation.foreignKey)
182
- continue;
183
- migrationContent += ` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
4
+ `;migrationContent+=`export async function up(db: Database<any>) {
5
+ `;migrationContent+=` await (db as any).schema
6
+ `;migrationContent+=` .createTable('${tableName}')
7
+ `;migrationContent+=` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
8
+ `;if(useUuid)migrationContent+=` .addColumn('uuid', 'text')
9
+ `;if(useSocials){const socials=model.traits?.useSocials||[];if(socials.includes("google"))migrationContent+=` .addColumn('google_id', 'text')
10
+ `;if(socials.includes("github"))migrationContent+=` .addColumn('github_id', 'text')
11
+ `;if(socials.includes("apple"))migrationContent+=` .addColumn('apple_id', 'text')
12
+ `;if(socials.includes("twitter"))migrationContent+=` .addColumn('twitter_id', 'text')
13
+ `;if(socials.includes("facebook"))migrationContent+=` .addColumn('facebook_id', 'text')
14
+ `}for(const[fieldName,options]of arrangeColumns(model.attributes)){const fieldOptions=options,fieldNameFormatted=snakeCase(fieldName),columnType=mapFieldTypeToColumnType(fieldOptions.validation?.rule,"sqlite");migrationContent+=` .addColumn('${fieldNameFormatted}', ${columnType}`;const isRequired=fieldOptions.validation?.rule.isRequired;if(isRequired||fieldOptions.unique||fieldOptions.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(fieldOptions.unique)migrationContent+=".unique()";if(fieldOptions.default!==void 0)if(typeof fieldOptions.default==="string")migrationContent+=`.defaultTo('${fieldOptions.default}')`;else if(fieldOptions.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${fieldOptions.default})`;migrationContent+=""}migrationContent+=`)
15
+ `}if(twoFactorEnabled!==!1&&twoFactorEnabled)migrationContent+=` .addColumn('two_factor_secret', 'text')
16
+ `;if(useBillable)migrationContent+=` .addColumn('stripe_id', 'text')
17
+ `;if(useSoftDeletes)migrationContent+=` .addColumn('deleted_at', 'timestamp')
18
+ `;if(usePasskey)migrationContent+=` .addColumn('public_passkey', 'text')
19
+ `;if(otherModelRelations?.length)for(const modelRelation of otherModelRelations){if(!modelRelation.foreignKey)continue;migrationContent+=` .addColumn('${modelRelation.foreignKey}', 'integer', (col) =>
184
20
  col.references('${modelRelation.relationTable}.id').onDelete('cascade')
185
21
  )
186
- `;
187
- }
188
- if (useTimestamps) {
189
- migrationContent += " .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";
190
- migrationContent += ` .addColumn('updated_at', 'timestamp')
191
- `;
192
- }
193
- migrationContent += ` .execute()
22
+ `}if(useTimestamps){migrationContent+=" .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";migrationContent+=` .addColumn('updated_at', 'timestamp')
23
+ `}migrationContent+=` .execute()
194
24
 
195
- `;
196
- if (otherModelRelations?.length)
197
- for (const modelRelation of otherModelRelations) {
198
- if (!modelRelation.foreignKey)
199
- continue;
200
- migrationContent += generateForeignKeyIndexSQL(tableName, modelRelation.foreignKey);
201
- }
202
- if (model.indexes?.length) {
203
- migrationContent += `
204
- `;
205
- for (const index of model.indexes)
206
- migrationContent += generateIndexCreationSQL(tableName, index);
207
- }
208
- migrationContent += generatePrimaryKeyIndexSQL(tableName);
209
- if (useLikeable) {
210
- const upvoteTable = getUpvoteTableName(model, tableName);
211
- if (upvoteTable) {
212
- const foreignKey = getLikeableForeignKey(model, tableName);
213
- migrationContent += `
25
+ `;if(otherModelRelations?.length)for(const modelRelation of otherModelRelations){if(!modelRelation.foreignKey)continue;migrationContent+=generateForeignKeyIndexSQL(tableName,modelRelation.foreignKey)}if(model.indexes?.length){migrationContent+=`
26
+ `;for(const index of model.indexes)migrationContent+=generateIndexCreationSQL(tableName,index)}migrationContent+=generatePrimaryKeyIndexSQL(tableName);if(useLikeable){const upvoteTable=getUpvoteTableName(model,tableName);if(upvoteTable){const foreignKey=getLikeableForeignKey(model,tableName);migrationContent+=`
214
27
  // Create upvote table
215
- `;
216
- migrationContent += ` await (db as any).schema
217
- `;
218
- migrationContent += ` .createTable('${upvoteTable}')
219
- `;
220
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
221
- `;
222
- migrationContent += ` .addColumn('${foreignKey}', 'integer', col => col.notNull())
223
- `;
224
- migrationContent += ` .addColumn('user_id', 'integer', col => col.notNull())
225
- `;
226
- migrationContent += " .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";
227
- migrationContent += ` .addColumn('updated_at', 'timestamp')
228
- `;
229
- migrationContent += ` .execute()
28
+ `;migrationContent+=` await (db as any).schema
29
+ `;migrationContent+=` .createTable('${upvoteTable}')
30
+ `;migrationContent+=` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
31
+ `;migrationContent+=` .addColumn('${foreignKey}', 'integer', col => col.notNull())
32
+ `;migrationContent+=` .addColumn('user_id', 'integer', col => col.notNull())
33
+ `;migrationContent+=" .addColumn('created_at', 'timestamp', col => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`))\n";migrationContent+=` .addColumn('updated_at', 'timestamp')
34
+ `;migrationContent+=` .execute()
230
35
 
231
- `;
232
- migrationContent += ` // Add indexes for upvote table
233
- `;
234
- migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_${foreignKey}_index').on('${upvoteTable}').column('${foreignKey}').execute()
235
- `;
236
- migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_user_${foreignKey}_unique').on('${upvoteTable}').columns(['user_id', '${foreignKey}']).unique().execute()
237
- `;
238
- migrationContent += ` await (db as any).schema.createIndex('${upvoteTable}_id_index').on('${upvoteTable}').column('id').execute()
239
- `;
240
- }
241
- }
242
- migrationContent += `}
243
- `;
244
- const migrationFileName = `${new Date().getTime().toString()}-create-${tableName}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
245
- await Bun.write(migrationFilePath, migrationContent);
246
- log.success(`Created migration: ${italic(migrationFileName)}`);
247
- }
248
- async function createPivotTableMigration(model, modelPath) {
249
- const pivotTables = await getPivotTables(model, modelPath);
250
- if (!pivotTables.length)
251
- return;
252
- for (const pivotTable of pivotTables) {
253
- if (await checkPivotMigration(pivotTable.table))
254
- return;
255
- let migrationContent = `import type { Database } from '@stacksjs/database'
256
- `;
257
- migrationContent += `import { sql } from '@stacksjs/database'
36
+ `;migrationContent+=` // Add indexes for upvote table
37
+ `;migrationContent+=` await (db as any).schema.createIndex('${upvoteTable}_${foreignKey}_index').on('${upvoteTable}').column('${foreignKey}').execute()
38
+ `;migrationContent+=` await (db as any).schema.createIndex('${upvoteTable}_user_${foreignKey}_unique').on('${upvoteTable}').columns(['user_id', '${foreignKey}']).unique().execute()
39
+ `;migrationContent+=` await (db as any).schema.createIndex('${upvoteTable}_id_index').on('${upvoteTable}').column('id').execute()
40
+ `}}migrationContent+=`}
41
+ `;const migrationFileName=`${new Date().getTime().toString()}-create-${tableName}-table.ts`,migrationFilePath=path.userMigrationsPath(migrationFileName);await Bun.write(migrationFilePath,migrationContent);log.success(`Created migration: ${italic(migrationFileName)}`)}async function createPivotTableMigration(model,modelPath){const pivotTables=await getPivotTables(model,modelPath);if(!pivotTables.length)return;for(const pivotTable of pivotTables){if(await checkPivotMigration(pivotTable.table))return;let migrationContent=`import type { Database } from '@stacksjs/database'
42
+ `;migrationContent+=`import { sql } from '@stacksjs/database'
258
43
 
259
- `;
260
- migrationContent += `export async function up(db: Database<any>) {
261
- `;
262
- migrationContent += ` await (db as any).schema
263
- `;
264
- migrationContent += ` .createTable('${pivotTable.table}')
265
- `;
266
- migrationContent += ` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
267
- `;
268
- migrationContent += ` .addColumn('${pivotTable.firstForeignKey}', 'integer')
269
- `;
270
- migrationContent += ` .addColumn('${pivotTable.secondForeignKey}', 'integer')
271
- `;
272
- migrationContent += ` .addColumn('created_at', 'timestamp', col => col.defaultTo(sql.raw('CURRENT_TIMESTAMP')))
273
- `;
274
- migrationContent += ` .execute()
275
- `;
276
- migrationContent += ` }
277
- `;
278
- const migrationFileName = `${new Date().getTime().toString()}-create-${pivotTable.table}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
279
- await Bun.write(migrationFilePath, migrationContent);
280
- log.success(`Created pivot migration: ${migrationFileName}`);
281
- }
282
- }
283
- async function createAlterTableMigration(modelPath) {
284
- const model = (await import(modelPath)).default, modelName = getModelName(model, modelPath), tableName = getTableName(model, modelPath);
285
- let hasChanged = !1;
286
- const oldModel = (await import(path.frameworkPath(`cache/models/${modelName}.ts`))).default, lastFields = await getLastMigrationFields(modelName) ?? {}, currentFields = model.attributes, changes = pluckChanges(Object.keys(lastFields), Object.keys(currentFields)), fieldsToAdd = changes?.added || [], fieldsToRemove = changes?.removed || [];
287
- let migrationContent = `import type { Database } from '@stacksjs/database'
288
- `;
289
- migrationContent += `import { sql } from '@stacksjs/database'
44
+ `;migrationContent+=`export async function up(db: Database<any>) {
45
+ `;migrationContent+=` await (db as any).schema
46
+ `;migrationContent+=` .createTable('${pivotTable.table}')
47
+ `;migrationContent+=` .addColumn('id', 'integer', col => col.primaryKey().autoIncrement())
48
+ `;migrationContent+=` .addColumn('${pivotTable.firstForeignKey}', 'integer')
49
+ `;migrationContent+=` .addColumn('${pivotTable.secondForeignKey}', 'integer')
50
+ `;migrationContent+=` .addColumn('created_at', 'timestamp', col => col.defaultTo(sql.raw('CURRENT_TIMESTAMP')))
51
+ `;migrationContent+=` .execute()
52
+ `;migrationContent+=` }
53
+ `;const migrationFileName=`${new Date().getTime().toString()}-create-${pivotTable.table}-table.ts`,migrationFilePath=path.userMigrationsPath(migrationFileName);await Bun.write(migrationFilePath,migrationContent);log.success(`Created pivot migration: ${migrationFileName}`)}}async function createAlterTableMigration(modelPath){const model=(await import(modelPath)).default,modelName=getModelName(model,modelPath),tableName=getTableName(model,modelPath);let hasChanged=!1;const oldModel=(await import(path.frameworkPath(`cache/models/${modelName}.ts`))).default,lastFields=await getLastMigrationFields(modelName)??{},currentFields=model.attributes,changes=pluckChanges(Object.keys(lastFields),Object.keys(currentFields)),fieldsToAdd=changes?.added||[],fieldsToRemove=changes?.removed||[];let migrationContent=`import type { Database } from '@stacksjs/database'
54
+ `;migrationContent+=`import { sql } from '@stacksjs/database'
290
55
 
291
- `;
292
- migrationContent += `export async function up(db: Database<any>) {
293
- `;
294
- if (fieldsToAdd.length || fieldsToRemove.length) {
295
- hasChanged = !0;
296
- migrationContent += ` await (db as any).schema.alterTable('${tableName}')
297
- `;
298
- }
299
- const fieldValidations = findDifferingKeys(lastFields, currentFields);
300
- for (const fieldValidation of fieldValidations) {
301
- hasChanged = !0;
302
- const fieldNameFormatted = snakeCase(fieldValidation.key);
303
- migrationContent += `await sql\`
56
+ `;migrationContent+=`export async function up(db: Database<any>) {
57
+ `;if(fieldsToAdd.length||fieldsToRemove.length){hasChanged=!0;migrationContent+=` await (db as any).schema.alterTable('${tableName}')
58
+ `}const fieldValidations=findDifferingKeys(lastFields,currentFields);for(const fieldValidation of fieldValidations){hasChanged=!0;const fieldNameFormatted=snakeCase(fieldValidation.key);migrationContent+=`await sql\`
304
59
  ALTER TABLE ${tableName}
305
60
  MODIFY COLUMN ${fieldNameFormatted} TEXT
306
61
  \`.execute(db)
307
62
 
308
- `;
309
- }
310
- for (const fieldName of fieldsToAdd) {
311
- const options = currentFields[fieldName], columnType = mapFieldTypeToColumnType(options.validation?.rule, "sqlite"), formattedFieldName = snakeCase(fieldName), isRequired = options.validation?.rule.isRequired;
312
- migrationContent += ` .addColumn('${formattedFieldName}', ${columnType}`;
313
- if (isRequired || options.unique || options.default !== void 0) {
314
- migrationContent += ", col => col";
315
- if (isRequired)
316
- migrationContent += ".notNull()";
317
- if (options.unique)
318
- migrationContent += ".unique()";
319
- if (options.default !== void 0)
320
- if (typeof options.default === "string")
321
- migrationContent += `.defaultTo('${options.default}')`;
322
- else if (options.default === null)
323
- migrationContent += ".defaultTo(null)";
324
- else
325
- migrationContent += `.defaultTo(${options.default})`;
326
- migrationContent += "";
327
- }
328
- migrationContent += `)
63
+ `}for(const fieldName of fieldsToAdd){const options=currentFields[fieldName],columnType=mapFieldTypeToColumnType(options.validation?.rule,"sqlite"),formattedFieldName=snakeCase(fieldName),isRequired=options.validation?.rule.isRequired;migrationContent+=` .addColumn('${formattedFieldName}', ${columnType}`;if(isRequired||options.unique||options.default!==void 0){migrationContent+=", col => col";if(isRequired)migrationContent+=".notNull()";if(options.unique)migrationContent+=".unique()";if(options.default!==void 0)if(typeof options.default==="string")migrationContent+=`.defaultTo('${options.default}')`;else if(options.default===null)migrationContent+=".defaultTo(null)";else migrationContent+=`.defaultTo(${options.default})`;migrationContent+=""}migrationContent+=`)
329
64
 
330
- `;
331
- }
332
- for (const fieldName of fieldsToRemove)
333
- migrationContent += ` .dropColumn('${fieldName}')
334
- `;
335
- if (fieldsToAdd.length || fieldsToRemove.length)
336
- migrationContent += ` .execute();
337
- `;
338
- const lastFieldOrder = Object.values(lastFields).map((attr) => attr.order), currentFieldOrder = Object.values(currentFields).map((attr) => attr.order);
339
- if (!isArrayEqual(lastFieldOrder, currentFieldOrder)) {
340
- hasChanged = !0;
341
- migrationContent += reArrangeColumns(model.attributes, tableName);
342
- }
343
- const oldIndexes = oldModel.indexes || [], newIndexes = model.indexes || [];
344
- for (const oldIndex of oldIndexes)
345
- if (!newIndexes.find((newIndex) => newIndex.name === oldIndex.name)) {
346
- hasChanged = !0;
347
- migrationContent += ` await (db as any).schema.dropIndex('${oldIndex.name}').execute()
348
- `;
349
- }
350
- for (const newIndex of newIndexes)
351
- if (!oldIndexes.find((oldIndex) => oldIndex.name === newIndex.name)) {
352
- hasChanged = !0;
353
- migrationContent += generateIndexCreationSQL(tableName, newIndex);
354
- }
355
- migrationContent += `}
356
- `;
357
- const migrationFileName = `${new Date().getTime().toString()}-update-${tableName}-table.ts`, migrationFilePath = path.userMigrationsPath(migrationFileName);
358
- if (hasChanged) {
359
- await Bun.write(migrationFilePath, migrationContent);
360
- log.success(`Created migration: ${italic(migrationFileName)}`);
361
- }
362
- }
363
- function reArrangeColumns(attributes, tableName) {
364
- const fields = arrangeColumns(attributes);
365
- let migrationContent = "", previousField = "";
366
- for (const [fieldName] of fields) {
367
- const fieldNameFormatted = snakeCase(fieldName);
368
- if (previousField)
369
- migrationContent += `await sql\`
65
+ `}for(const fieldName of fieldsToRemove)migrationContent+=` .dropColumn('${fieldName}')
66
+ `;if(fieldsToAdd.length||fieldsToRemove.length)migrationContent+=` .execute();
67
+ `;const lastFieldOrder=Object.values(lastFields).map((attr)=>attr.order),currentFieldOrder=Object.values(currentFields).map((attr)=>attr.order);if(!isArrayEqual(lastFieldOrder,currentFieldOrder)){hasChanged=!0;migrationContent+=reArrangeColumns(model.attributes,tableName)}const oldIndexes=oldModel.indexes||[],newIndexes=model.indexes||[];for(const oldIndex of oldIndexes)if(!newIndexes.find((newIndex)=>newIndex.name===oldIndex.name)){hasChanged=!0;migrationContent+=` await (db as any).schema.dropIndex('${oldIndex.name}').execute()
68
+ `}for(const newIndex of newIndexes)if(!oldIndexes.find((oldIndex)=>oldIndex.name===newIndex.name)){hasChanged=!0;migrationContent+=generateIndexCreationSQL(tableName,newIndex)}migrationContent+=`}
69
+ `;const migrationFileName=`${new Date().getTime().toString()}-update-${tableName}-table.ts`,migrationFilePath=path.userMigrationsPath(migrationFileName);if(hasChanged){await Bun.write(migrationFilePath,migrationContent);log.success(`Created migration: ${italic(migrationFileName)}`)}}function reArrangeColumns(attributes,tableName){const fields=arrangeColumns(attributes);let migrationContent="",previousField="";for(const[fieldName]of fields){const fieldNameFormatted=snakeCase(fieldName);if(previousField)migrationContent+=`await sql\`
370
70
  ALTER TABLE ${tableName}
371
71
  MODIFY COLUMN ${fieldNameFormatted} TEXT NOT NULL AFTER ${snakeCase(previousField)};
372
72
  \`.execute(db)
373
73
 
374
- `;
375
- previousField = fieldNameFormatted;
376
- }
377
- return migrationContent;
378
- }
379
- export function generateIndexCreationSQL(tableName, index) {
380
- if (index.unique || index.where) {
381
- const unique = index.unique ? "UNIQUE " : "", cols = index.columns.map((col) => snakeCase(col)).join(", "), whereClause = index.where ? ` WHERE ${index.where}` : "";
382
- return ` await db.unsafe(\`CREATE ${unique}INDEX IF NOT EXISTS "${index.name}" ON "${tableName}" (${cols})${whereClause}\`).execute()
383
- `;
384
- }
385
- const columnsStr = index.columns.map((col) => `\`${snakeCase(col)}\``).join(", ");
386
- return ` await (db as any).schema.createIndex('${index.name}').on('${tableName}').columns([${columnsStr}]).execute()
387
- `;
388
- }
389
- function generatePrimaryKeyIndexSQL(tableName) {
390
- return ` await (db as any).schema.createIndex('${tableName}_id_index').on('${tableName}').column('id').execute()
391
- `;
392
- }
393
- function generateForeignKeyIndexSQL(tableName, foreignKey) {
394
- return ` await (db as any).schema.createIndex('${tableName}_${foreignKey}_index').on('${tableName}').column(\`${foreignKey}\`).execute()
74
+ `;previousField=fieldNameFormatted}return migrationContent}export function generateIndexCreationSQL(tableName,index){if(index.unique||index.where){const unique=index.unique?"UNIQUE ":"",cols=index.columns.map((col)=>snakeCase(col)).join(", "),whereClause=index.where?` WHERE ${index.where}`:"";return` await db.unsafe(\`CREATE ${unique}INDEX IF NOT EXISTS "${index.name}" ON "${tableName}" (${cols})${whereClause}\`).execute()
75
+ `}const columnsStr=index.columns.map((col)=>`\`${snakeCase(col)}\``).join(", ");return` await (db as any).schema.createIndex('${index.name}').on('${tableName}').columns([${columnsStr}]).execute()
76
+ `}function generatePrimaryKeyIndexSQL(tableName){return` await (db as any).schema.createIndex('${tableName}_id_index').on('${tableName}').column('id').execute()
77
+ `}function generateForeignKeyIndexSQL(tableName,foreignKey){return` await (db as any).schema.createIndex('${tableName}_${foreignKey}_index').on('${tableName}').column(\`${foreignKey}\`).execute()
395
78
 
396
- `;
397
- }
79
+ `}
@@ -1,145 +1 @@
1
- import process from "node:process";
2
- import { SQL } from "bun";
3
- import { env as envVars } from "@stacksjs/env";
4
- import { DB_HOST_DEFAULT, DB_NAMES, DB_PORTS, DB_USERS, getConnectionDefaults } from "./defaults";
5
- const DEFAULT_TIMEOUT_MS = 1e4, UNSAFE_IDENTIFIER_CHARS = /["'`\\;\u0000\n\r]/, MAX_IDENTIFIER_LENGTH = 63;
6
- export function isValidDatabaseIdentifier(name) {
7
- if (!name || name.length > MAX_IDENTIFIER_LENGTH)
8
- return !1;
9
- return !UNSAFE_IDENTIFIER_CHARS.test(name);
10
- }
11
- export function quoteIdentifier(dialect, name) {
12
- return dialect === "mysql" ? `\`${name}\`` : `"${name}"`;
13
- }
14
- function stripWrappingQuotes(value) {
15
- return value.replace(/^['"]|['"]$/g, "");
16
- }
17
- export function classifyConnectionError(error) {
18
- const e = error, errno = e?.errno, code = typeof e?.code === "string" ? e.code : "", message = typeof e?.message === "string" ? e.message : String(error ?? ""), sqlState = typeof errno === "string" ? errno.toUpperCase() : "", mysqlErrno = typeof errno === "number" ? errno : Number.NaN;
19
- if (sqlState === "3D000")
20
- return "missing-database";
21
- if (sqlState === "28000" || sqlState === "28P01")
22
- return sqlState === "28P01" ? "auth-failed" : "missing-role";
23
- if (sqlState === "42501")
24
- return "permission-denied";
25
- if (mysqlErrno === 1049)
26
- return "missing-database";
27
- if (mysqlErrno === 1045)
28
- return "auth-failed";
29
- if (mysqlErrno === 1044)
30
- return "permission-denied";
31
- if (mysqlErrno === 2002 || mysqlErrno === 2003)
32
- return "server-unreachable";
33
- if (code.includes("CONNECTION_REFUSED") || code.includes("CONNECTION_CLOSED") || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EHOSTUNREACH")
34
- return "server-unreachable";
35
- if (code === "ETIMEDOUT" || code.includes("TIMEOUT"))
36
- return "timeout";
37
- if (/database .* does not exist|unknown database/i.test(message))
38
- return "missing-database";
39
- if (/role .* does not exist|user .* does not exist/i.test(message))
40
- return "missing-role";
41
- if (/password authentication failed|access denied for user/i.test(message))
42
- return "auth-failed";
43
- if (/permission denied|insufficient privilege/i.test(message))
44
- return "permission-denied";
45
- if (/econnrefused|connection refused|can'?t connect/i.test(message))
46
- return "server-unreachable";
47
- return "unknown";
48
- }
49
- export function resolveConnectionTarget(envProxy = envVars) {
50
- const driver = String(envProxy.DB_CONNECTION || "sqlite");
51
- if (driver !== "postgres" && driver !== "mysql" && driver !== "singlestore")
52
- return null;
53
- const dialect = driver === "postgres" ? "postgres" : "mysql", defaults = getConnectionDefaults(dialect, envProxy), database = stripWrappingQuotes(String(envProxy.DB_DATABASE || defaults.database || DB_NAMES.default)), host = String(envProxy.DB_HOST || defaults.host || DB_HOST_DEFAULT), port = Number(envProxy.DB_PORT || defaults.port || DB_PORTS[dialect]), username = String(envProxy.DB_USERNAME || defaults.username || DB_USERS[dialect]), password = String(envProxy.DB_PASSWORD ?? defaults.password ?? "");
54
- return {
55
- dialect,
56
- driver,
57
- database,
58
- host,
59
- port,
60
- username,
61
- password,
62
- maintenanceCandidates: dialect === "postgres" ? ["postgres", "template1"] : ["information_schema", "mysql"]
63
- };
64
- }
65
- export function buildConnectionUrl(target, database) {
66
- const scheme = target.dialect === "postgres" ? "postgres" : "mysql", auth = target.password ? `${encodeURIComponent(target.username)}:${encodeURIComponent(target.password)}` : encodeURIComponent(target.username), sslEnv = process.env.DB_SSL, ssl = sslEnv === "true" || sslEnv === "1" ? "?ssl=true" : "";
67
- return `${scheme}://${auth}@${target.host}:${target.port}/${encodeURIComponent(database)}${ssl}`;
68
- }
69
- function defaultConnect(url) {
70
- return new SQL(url);
71
- }
72
- async function withConnection(target, database, deps, work) {
73
- const connect = deps.connect ?? defaultConnect, timeoutMs = deps.timeoutMs ?? Number(process.env.DB_PREFLIGHT_TIMEOUT_MS || DEFAULT_TIMEOUT_MS), client = connect(buildConnectionUrl(target, database));
74
- let timer;
75
- try {
76
- const timeout = new Promise((_, reject) => {
77
- timer = setTimeout(() => {
78
- const e = Error(`Timed out after ${timeoutMs}ms connecting to ${target.dialect} at ${target.host}:${target.port}`);
79
- e.code = "ETIMEDOUT";
80
- reject(e);
81
- }, timeoutMs);
82
- });
83
- return await Promise.race([work(client), timeout]);
84
- } finally {
85
- if (timer)
86
- clearTimeout(timer);
87
- try {
88
- await client.close();
89
- } catch {}
90
- }
91
- }
92
- export async function probeTargetDatabase(target, deps = {}) {
93
- try {
94
- await withConnection(target, target.database, deps, async (client) => client.unsafe("select 1"));
95
- return { ok: !0 };
96
- } catch (error) {
97
- return { ok: !1, kind: classifyConnectionError(error), error };
98
- }
99
- }
100
- export async function createDatabase(target, deps = {}) {
101
- if (!isValidDatabaseIdentifier(target.database))
102
- return {
103
- created: !1,
104
- kind: "unknown",
105
- error: Error(`Refusing to create a database named ${JSON.stringify(target.database)}. Database names must be 1 to 63 characters and may not contain quotes, backslashes, semicolons, or newlines.`)
106
- };
107
- const identifier = quoteIdentifier(target.dialect, target.database), sql = target.dialect === "mysql" ? `CREATE DATABASE IF NOT EXISTS ${identifier}` : `CREATE DATABASE ${identifier}`;
108
- let lastError, lastKind = "unknown";
109
- for (const candidate of target.maintenanceCandidates)
110
- try {
111
- await withConnection(target, candidate, deps, async (client) => client.unsafe(sql));
112
- return { created: !0, via: candidate };
113
- } catch (error) {
114
- const kind = classifyConnectionError(error), message = error?.message ?? "", errno = error?.errno;
115
- if (errno === "42P04" || errno === 1007 || /already exists|database exists/i.test(String(message)))
116
- return { created: !1, via: candidate };
117
- lastError = error;
118
- lastKind = kind;
119
- if (kind === "permission-denied" || kind === "auth-failed" || kind === "missing-role")
120
- break;
121
- }
122
- return { created: !1, kind: lastKind, error: lastError };
123
- }
124
- export async function canCreateDatabases(target, deps = {}) {
125
- if (target.dialect !== "postgres")
126
- return null;
127
- for (const candidate of target.maintenanceCandidates)
128
- try {
129
- return await withConnection(target, candidate, deps, async (client) => {
130
- const rows = await client.unsafe("select rolcreatedb, rolsuper from pg_roles where rolname = current_user"), row = Array.isArray(rows) ? rows[0] : void 0;
131
- if (!row)
132
- return null;
133
- return Boolean(row.rolcreatedb || row.rolsuper);
134
- });
135
- } catch {}
136
- return null;
137
- }
138
- export function manualCreateHint(target) {
139
- if (target.dialect === "postgres")
140
- return `createdb -h ${target.host} -p ${target.port} -U ${target.username} ${target.database}`;
141
- return `mysql -h ${target.host} -P ${target.port} -u ${target.username} -e "CREATE DATABASE \\\`${target.database}\\\`"`;
142
- }
143
- export function describeTarget(target) {
144
- return `the ${target.driver} connection (${target.host}:${target.port}, user "${target.username}")`;
145
- }
1
+ import process from"node:process";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{DB_HOST_DEFAULT,DB_NAMES,DB_PORTS,DB_USERS,getConnectionDefaults}from"./defaults";const DEFAULT_TIMEOUT_MS=1e4,UNSAFE_IDENTIFIER_CHARS=/["'`\\;\u0000\n\r]/,MAX_IDENTIFIER_LENGTH=63;export function isValidDatabaseIdentifier(name){if(!name||name.length>MAX_IDENTIFIER_LENGTH)return!1;return!UNSAFE_IDENTIFIER_CHARS.test(name)}export function quoteIdentifier(dialect,name){return dialect==="mysql"?`\`${name}\``:`"${name}"`}function stripWrappingQuotes(value){return value.replace(/^['"]|['"]$/g,"")}export function classifyConnectionError(error){const e=error,errno=e?.errno,code=typeof e?.code==="string"?e.code:"",message=typeof e?.message==="string"?e.message:String(error??""),sqlState=typeof errno==="string"?errno.toUpperCase():"",mysqlErrno=typeof errno==="number"?errno:Number.NaN;if(sqlState==="3D000")return"missing-database";if(sqlState==="28000"||sqlState==="28P01")return sqlState==="28P01"?"auth-failed":"missing-role";if(sqlState==="42501")return"permission-denied";if(mysqlErrno===1049)return"missing-database";if(mysqlErrno===1045)return"auth-failed";if(mysqlErrno===1044)return"permission-denied";if(mysqlErrno===2002||mysqlErrno===2003)return"server-unreachable";if(code.includes("CONNECTION_REFUSED")||code.includes("CONNECTION_CLOSED")||code==="ECONNREFUSED"||code==="ENOTFOUND"||code==="EHOSTUNREACH")return"server-unreachable";if(code==="ETIMEDOUT"||code.includes("TIMEOUT"))return"timeout";if(/database .* does not exist|unknown database/i.test(message))return"missing-database";if(/role .* does not exist|user .* does not exist/i.test(message))return"missing-role";if(/password authentication failed|access denied for user/i.test(message))return"auth-failed";if(/permission denied|insufficient privilege/i.test(message))return"permission-denied";if(/econnrefused|connection refused|can'?t connect/i.test(message))return"server-unreachable";return"unknown"}export function resolveConnectionTarget(envProxy=envVars){const driver=String(envProxy.DB_CONNECTION||"sqlite");if(driver!=="postgres"&&driver!=="mysql"&&driver!=="singlestore")return null;const dialect=driver==="postgres"?"postgres":"mysql",defaults=getConnectionDefaults(dialect,envProxy),database=stripWrappingQuotes(String(envProxy.DB_DATABASE||defaults.database||DB_NAMES.default)),host=String(envProxy.DB_HOST||defaults.host||DB_HOST_DEFAULT),port=Number(envProxy.DB_PORT||defaults.port||DB_PORTS[dialect]),username=String(envProxy.DB_USERNAME||defaults.username||DB_USERS[dialect]),password=String(envProxy.DB_PASSWORD??defaults.password??"");return{dialect,driver,database,host,port,username,password,maintenanceCandidates:dialect==="postgres"?["postgres","template1"]:["information_schema","mysql"]}}export function buildConnectionUrl(target,database){const scheme=target.dialect==="postgres"?"postgres":"mysql",auth=target.password?`${encodeURIComponent(target.username)}:${encodeURIComponent(target.password)}`:encodeURIComponent(target.username),sslEnv=process.env.DB_SSL,ssl=sslEnv==="true"||sslEnv==="1"?"?ssl=true":"";return`${scheme}://${auth}@${target.host}:${target.port}/${encodeURIComponent(database)}${ssl}`}function defaultConnect(url){return new SQL(url)}async function withConnection(target,database,deps,work){const connect=deps.connect??defaultConnect,timeoutMs=deps.timeoutMs??Number(process.env.DB_PREFLIGHT_TIMEOUT_MS||DEFAULT_TIMEOUT_MS),client=connect(buildConnectionUrl(target,database));let timer;try{const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{const e=Error(`Timed out after ${timeoutMs}ms connecting to ${target.dialect} at ${target.host}:${target.port}`);e.code="ETIMEDOUT";reject(e)},timeoutMs)});return await Promise.race([work(client),timeout])}finally{if(timer)clearTimeout(timer);try{await client.close()}catch{}}}export async function probeTargetDatabase(target,deps={}){try{await withConnection(target,target.database,deps,async(client)=>client.unsafe("select 1"));return{ok:!0}}catch(error){return{ok:!1,kind:classifyConnectionError(error),error}}}export async function createDatabase(target,deps={}){if(!isValidDatabaseIdentifier(target.database))return{created:!1,kind:"unknown",error:Error(`Refusing to create a database named ${JSON.stringify(target.database)}. Database names must be 1 to 63 characters and may not contain quotes, backslashes, semicolons, or newlines.`)};const identifier=quoteIdentifier(target.dialect,target.database),sql=target.dialect==="mysql"?`CREATE DATABASE IF NOT EXISTS ${identifier}`:`CREATE DATABASE ${identifier}`;let lastError,lastKind="unknown";for(const candidate of target.maintenanceCandidates)try{await withConnection(target,candidate,deps,async(client)=>client.unsafe(sql));return{created:!0,via:candidate}}catch(error){const kind=classifyConnectionError(error),message=error?.message??"",errno=error?.errno;if(errno==="42P04"||errno===1007||/already exists|database exists/i.test(String(message)))return{created:!1,via:candidate};lastError=error;lastKind=kind;if(kind==="permission-denied"||kind==="auth-failed"||kind==="missing-role")break}return{created:!1,kind:lastKind,error:lastError}}export async function canCreateDatabases(target,deps={}){if(target.dialect!=="postgres")return null;for(const candidate of target.maintenanceCandidates)try{return await withConnection(target,candidate,deps,async(client)=>{const rows=await client.unsafe("select rolcreatedb, rolsuper from pg_roles where rolname = current_user"),row=Array.isArray(rows)?rows[0]:void 0;if(!row)return null;return Boolean(row.rolcreatedb||row.rolsuper)})}catch{}return null}export function manualCreateHint(target){if(target.dialect==="postgres")return`createdb -h ${target.host} -p ${target.port} -U ${target.username} ${target.database}`;return`mysql -h ${target.host} -P ${target.port} -u ${target.username} -e "CREATE DATABASE \\\`${target.database}\\\`"`}export function describeTarget(target){return`the ${target.driver} connection (${target.host}:${target.port}, user "${target.username}")`}