@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.
- package/dist/auth-tables.js +18 -137
- package/dist/column.js +1 -26
- package/dist/custom/audits.js +20 -54
- package/dist/custom/errors.js +16 -46
- package/dist/custom/index.js +1 -3
- package/dist/custom/jobs.js +13 -137
- package/dist/database.js +1 -181
- package/dist/datetime-columns.js +2 -79
- package/dist/ddl-constraints.js +7 -111
- package/dist/defaults.js +1 -48
- package/dist/dialect.js +1 -79
- package/dist/driver-config.js +1 -172
- package/dist/drivers/defaults/index.js +1 -1
- package/dist/drivers/defaults/traits.js +1 -29
- package/dist/drivers/dynamodb.js +1 -607
- package/dist/drivers/helpers.js +1 -206
- package/dist/drivers/index.js +1 -9
- package/dist/drivers/mysql.js +58 -299
- package/dist/drivers/postgres.js +78 -368
- package/dist/drivers/sqlite.js +61 -379
- package/dist/ensure-database.js +1 -145
- package/dist/fk-audit.js +3 -187
- package/dist/index.js +1 -64
- package/dist/managed-columns.js +1 -59
- package/dist/migration-dialect.js +4 -107
- package/dist/migration-ledger.js +1 -382
- package/dist/migration-lock.js +1 -143
- package/dist/migrations.js +15 -1118
- package/dist/model-sources.js +1 -76
- package/dist/notification-tables.js +4 -49
- package/dist/query-logger.js +2 -241
- package/dist/query-parser.js +1 -93
- package/dist/rbac-tables.js +6 -61
- package/dist/relation-columns.js +1 -66
- package/dist/replicas.js +1 -74
- package/dist/safe-migrations.js +2 -52
- package/dist/schema.js +1 -10
- package/dist/seeder.js +1 -457
- package/dist/sql-helpers.js +1 -50
- package/dist/table.js +1 -26
- package/dist/tools/setup.js +1 -6
- package/dist/trait-tables.js +8 -153
- package/dist/transaction-context.js +1 -62
- package/dist/types.js +1 -98
- package/dist/unique-audit.js +3 -155
- package/dist/utils.js +1 -285
- package/dist/uuid-columns.js +1 -68
- package/dist/validators.js +1 -122
- package/dist/vschema.js +2 -121
- package/package.json +20 -13
package/dist/drivers/sqlite.js
CHANGED
|
@@ -1,397 +1,79 @@
|
|
|
1
|
-
import { log } from
|
|
2
|
-
|
|
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
|
-
|
|
117
|
-
`;
|
|
118
|
-
|
|
119
|
-
`;
|
|
120
|
-
|
|
121
|
-
`;
|
|
122
|
-
|
|
123
|
-
`;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
`;
|
|
218
|
-
|
|
219
|
-
`;
|
|
220
|
-
|
|
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
|
-
|
|
233
|
-
`;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
261
|
-
`;
|
|
262
|
-
|
|
263
|
-
`;
|
|
264
|
-
|
|
265
|
-
`;
|
|
266
|
-
|
|
267
|
-
`;
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
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
|
+
`}
|
package/dist/ensure-database.js
CHANGED
|
@@ -1,145 +1 @@
|
|
|
1
|
-
import process from
|
|
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}")`}
|