@stacksjs/database 0.72.57 → 0.72.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import process from"node:process";import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect}from"./dialect";function getDbDriver(){return envVars.DB_CONNECTION||"sqlite"}export function usersEmailVerifiedAtSql(sql){return`ALTER TABLE users ADD COLUMN email_verified_at ${sql.nullableTimestamp}`}export function usersPasswordChangedAtSql(sql){return`ALTER TABLE users ADD COLUMN password_changed_at ${sql.nullableTimestamp}`}export function usersTwoFactorColumnsSql(sql){return["ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(255)",`ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN NOT NULL DEFAULT ${sql.boolFalse}`,"ALTER TABLE users ADD COLUMN two_factor_last_used_step BIGINT"]}export function usersStripeIdSql(){return"ALTER TABLE users ADD COLUMN stripe_id VARCHAR(255)"}export function oauthAccessTokenDeviceColumnsSql(){return["ALTER TABLE oauth_access_tokens ADD COLUMN user_agent VARCHAR(255)","ALTER TABLE oauth_access_tokens ADD COLUMN ip_address VARCHAR(45)"]}export function passwordResetsExpiresAtSql(){return["ALTER TABLE password_resets ADD COLUMN expires_at TIMESTAMP"]}export async function ensureUsersAuthColumns(sql,options={}){const alters=[usersEmailVerifiedAtSql(sql),usersPasswordChangedAtSql(sql),...usersTwoFactorColumnsSql(sql),usersStripeIdSql()];for(const alterSql of alters)try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied or users missing): ${alterSql}`)}try{await db.unsafe(indexSqlForDialect("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_stripe_id ON users(stripe_id)",getDbDriver())).execute()}catch{if(options.verbose)log.debug("[auth-tables] Skipped users.stripe_id unique index (already applied or users missing)")}}export async function migrateAuthTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver),{isPostgres,boolTrue,now,pkColumn,nullableTimestamp,datetime,utcNow}=sql;if(options.verbose)log.info(`Creating auth tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating oauth_clients table...");await db.unsafe(`
1
+ import{randomBytes}from"node:crypto";import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect}from"./dialect";function getDbDriver(){return envVars.DB_CONNECTION||"sqlite"}export function usersEmailVerifiedAtSql(sql){return`ALTER TABLE users ADD COLUMN email_verified_at ${sql.nullableTimestamp}`}export function usersPasswordChangedAtSql(sql){return`ALTER TABLE users ADD COLUMN password_changed_at ${sql.nullableTimestamp}`}export function usersTwoFactorColumnsSql(sql){return["ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(255)",`ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN NOT NULL DEFAULT ${sql.boolFalse}`,"ALTER TABLE users ADD COLUMN two_factor_last_used_step BIGINT"]}export function usersStripeIdSql(){return"ALTER TABLE users ADD COLUMN stripe_id VARCHAR(255)"}export function oauthAccessTokenDeviceColumnsSql(){return["ALTER TABLE oauth_access_tokens ADD COLUMN user_agent VARCHAR(255)","ALTER TABLE oauth_access_tokens ADD COLUMN ip_address VARCHAR(45)"]}export function passwordResetsExpiresAtSql(){return["ALTER TABLE password_resets ADD COLUMN expires_at TIMESTAMP"]}export async function ensureUsersAuthColumns(sql,options={}){const alters=[usersEmailVerifiedAtSql(sql),usersPasswordChangedAtSql(sql),...usersTwoFactorColumnsSql(sql),usersStripeIdSql()];for(const alterSql of alters)try{await db.unsafe(alterSql).execute()}catch{if(options.verbose)log.debug(`[auth-tables] Skipped (already applied or users missing): ${alterSql}`)}try{await db.unsafe(indexSqlForDialect("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_stripe_id ON users(stripe_id)",getDbDriver())).execute()}catch{if(options.verbose)log.debug("[auth-tables] Skipped users.stripe_id unique index (already applied or users missing)")}}export async function migrateAuthTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver),{isPostgres,boolTrue,now,pkColumn,nullableTimestamp,datetime,utcNow}=sql;if(options.verbose)log.info(`Creating auth tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating oauth_clients table...");await db.unsafe(`
2
2
  CREATE TABLE IF NOT EXISTS oauth_clients (
3
3
  ${pkColumn},
4
4
  name VARCHAR(255) NOT NULL,
@@ -1,4 +1,4 @@
1
- import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B[23m`}import{db}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,hasTableBeenMigrated,isArrayEqual,mapFieldTypeToColumnType,pluckChanges}from"./helpers";import{dropCommonTables}from"./defaults/traits";export async function resetMysqlDatabase(){await dropMysqlTables();await deleteFrameworkModels();await deleteMigrationFiles();return ok("All tables dropped successfully!")}export async function dropMysqlTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),tables=await fetchTables();for(const table of tables){if(!/^[a-z_][\w]*$/i.test(table))throw Error(`[mysql] Refusing to drop table with unsafe name: ${table}`);await db.unsafe(`DROP TABLE IF EXISTS \`${table}\``).execute()}await dropCommonTables();for(const userModel of modelFiles){const userModelPath=(await import(userModel)).default,pivotTables=await getPivotTables(userModelPath,userModel);for(const pivotTable of pivotTables){if(!/^[a-z_][\w]*$/i.test(pivotTable.table))throw Error(`[mysql] Refusing to drop pivot table with unsafe name: ${pivotTable.table}`);await db.unsafe(`DROP TABLE IF EXISTS \`${pivotTable.table}\``).execute()}}}export async function generateMysqlMigration(modelPath){const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);let haveFieldsChanged=!1;if(fs.existsSync(copiedModelPath)){log.info(`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=await hasTableBeenMigrated(tableName);log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);if(haveFieldsChanged)await createAlterTableMigration(modelPath);else await createTableMigration(modelPath)}async function createTableMigration(modelPath){log.debug("createTableMigration modelPath:",modelPath);const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),twoFactorEnabled=model.traits?.useAuth&&typeof model.traits.useAuth!=="boolean"?model.traits.useAuth.useTwoFactor:!1;await createPivotTableMigration(model,modelPath);const 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;if(useBillable&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));let migrationContent=`import type { Database } from '@stacksjs/database'
1
+ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B[23m`}import{db}from"../utils";import{ok}from"@stacksjs/error-handling";import{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,hasTableBeenMigrated,isArrayEqual,mapFieldTypeToColumnType,pluckChanges}from"./helpers";import{dropCommonTables}from"./defaults/traits";export async function resetMysqlDatabase(){await dropMysqlTables();await deleteFrameworkModels();await deleteMigrationFiles();return ok("All tables dropped successfully!")}export async function dropMysqlTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),tables=await fetchTables();for(const table of tables){if(!/^[a-z_][\w]*$/i.test(table))throw Error(`[mysql] Refusing to drop table with unsafe name: ${table}`);await db.unsafe(`DROP TABLE IF EXISTS \`${table}\``).execute()}await dropCommonTables();for(const userModel of modelFiles){const userModelPath=(await import(userModel)).default,pivotTables=await getPivotTables(userModelPath,userModel);for(const pivotTable of pivotTables){if(!/^[a-z_][\w]*$/i.test(pivotTable.table))throw Error(`[mysql] Refusing to drop pivot table with unsafe name: ${pivotTable.table}`);await db.unsafe(`DROP TABLE IF EXISTS \`${pivotTable.table}\``).execute()}}}export async function generateMysqlMigration(modelPath){const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);let haveFieldsChanged=!1;if(fs.existsSync(copiedModelPath)){log.info(`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=await hasTableBeenMigrated(tableName);log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);if(haveFieldsChanged)await createAlterTableMigration(modelPath);else await createTableMigration(modelPath)}async function createTableMigration(modelPath){log.debug("createTableMigration modelPath:",modelPath);const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),twoFactorEnabled=model.traits?.useAuth&&typeof model.traits.useAuth!=="boolean"?model.traits.useAuth.useTwoFactor:!1;await createPivotTableMigration(model,modelPath);const 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;if(useBillable&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));let migrationContent=`import type { Database } from '@stacksjs/database'
2
2
  `;migrationContent+=`import { sql } from '@stacksjs/database'
3
3
 
4
4
  `;migrationContent+=`export async function up(db: Database<any>) {
@@ -1,4 +1,4 @@
1
- import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B[23m`}import{db}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{plural,snakeCase}from"@stacksjs/strings";import{arrangeColumns,checkPivotMigration,deleteFrameworkModels,deleteMigrationFiles,findDifferingKeys,getLastMigrationFields,getLikeableForeignKey,getUpvoteTableName,hasTableBeenMigrated,isArrayEqual,mapFieldTypeToColumnType,pluckChanges}from"./helpers";import{dropCommonTables,dropMigrationTables}from"./defaults/traits";export async function dropPostgresTables(){const tables=await fetchPostgresTables(),userModelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0});await dropMigrationTables();for(const table of tables)await db.unsafe(`DROP TABLE IF EXISTS "${table}" CASCADE`).execute();await dropCommonTables();for(const userModel of userModelFiles){const userModelPath=(await import(userModel)).default,pivotTables=await getPivotTables(userModelPath,userModel);for(const pivotTable of pivotTables)await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}" CASCADE`).execute()}}export async function resetPostgresDatabase(){await dropPostgresTables();await deleteFrameworkModels();await deleteMigrationFiles();await db.unsafe('CREATE TABLE IF NOT EXISTS "migrations" (id SERIAL PRIMARY KEY)').execute();await db.unsafe('CREATE TABLE IF NOT EXISTS "migration_locks" (id SERIAL PRIMARY KEY)').execute();await db.unsafe('CREATE TABLE IF NOT EXISTS "activities" (id SERIAL PRIMARY KEY)').execute();return ok("All tables dropped successfully!")}export async function generatePostgresMigration(modelPath){if((await fs.promises.readdir(path.userMigrationsPath(""))).length===0){log.debug("No migrations found in the database folder, clearing the model snapshot cache...");const cacheDir=path.frameworkPath("cache/models");if(fs.existsSync(cacheDir)){const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const file of modelFiles)if(file.endsWith(".ts"))await fs.promises.unlink(path.frameworkPath(`cache/models/${file}`))}}}const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);let haveFieldsChanged=!1;if(fs.existsSync(copiedModelPath)){log.info(`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=await hasTableBeenMigrated(tableName);log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);if((model.traits?.billable||!1)&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));if(haveFieldsChanged)await createAlterTableMigration(modelPath);else await createTableMigration(modelPath)}async function createTableMigration(modelPath){log.debug("createTableMigration modelPath:",modelPath);const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),twoFactorEnabled=model.traits?.useAuth&&typeof model.traits.useAuth!=="boolean"?model.traits.useAuth.useTwoFactor:!1;await createPivotTableMigration(model,modelPath);const 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;if(useBillable&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));let migrationContent=`import type { Database } from '@stacksjs/database'
1
+ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B[23m`}import{db}from"../utils";import{ok}from"@stacksjs/error-handling";import{getModelName,getPivotTables,getTableName}from"@stacksjs/orm";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{plural,snakeCase}from"@stacksjs/strings";import{arrangeColumns,checkPivotMigration,deleteFrameworkModels,deleteMigrationFiles,findDifferingKeys,getLastMigrationFields,getLikeableForeignKey,getUpvoteTableName,hasTableBeenMigrated,isArrayEqual,mapFieldTypeToColumnType,pluckChanges}from"./helpers";import{dropCommonTables,dropMigrationTables}from"./defaults/traits";export async function dropPostgresTables(){const tables=await fetchPostgresTables(),userModelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0});await dropMigrationTables();for(const table of tables)await db.unsafe(`DROP TABLE IF EXISTS "${table}" CASCADE`).execute();await dropCommonTables();for(const userModel of userModelFiles){const userModelPath=(await import(userModel)).default,pivotTables=await getPivotTables(userModelPath,userModel);for(const pivotTable of pivotTables)await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}" CASCADE`).execute()}}export async function resetPostgresDatabase(){await dropPostgresTables();await deleteFrameworkModels();await deleteMigrationFiles();await db.unsafe('CREATE TABLE IF NOT EXISTS "migrations" (id SERIAL PRIMARY KEY)').execute();await db.unsafe('CREATE TABLE IF NOT EXISTS "migration_locks" (id SERIAL PRIMARY KEY)').execute();await db.unsafe('CREATE TABLE IF NOT EXISTS "activities" (id SERIAL PRIMARY KEY)').execute();return ok("All tables dropped successfully!")}export async function generatePostgresMigration(modelPath){if((await fs.promises.readdir(path.userMigrationsPath(""))).length===0){log.debug("No migrations found in the database folder, clearing the model snapshot cache...");const cacheDir=path.frameworkPath("cache/models");if(fs.existsSync(cacheDir)){const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const file of modelFiles)if(file.endsWith(".ts"))await fs.promises.unlink(path.frameworkPath(`cache/models/${file}`))}}}const model=(await import(modelPath)).default,fileName=path.basename(modelPath),tableName=getTableName(model,modelPath),fieldsString=JSON.stringify(model.attributes,null,2),copiedModelPath=path.frameworkPath(`cache/models/${fileName}`);let haveFieldsChanged=!1;if(fs.existsSync(copiedModelPath)){log.info(`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=await hasTableBeenMigrated(tableName);log.debug(`Has ${tableName} been migrated? ${hasBeenMigrated}`);if((model.traits?.billable||!1)&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));if(haveFieldsChanged)await createAlterTableMigration(modelPath);else await createTableMigration(modelPath)}async function createTableMigration(modelPath){log.debug("createTableMigration modelPath:",modelPath);const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),twoFactorEnabled=model.traits?.useAuth&&typeof model.traits.useAuth!=="boolean"?model.traits.useAuth.useTwoFactor:!1;await createPivotTableMigration(model,modelPath);const 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;if(useBillable&&tableName==="users")await createTableMigration(path.frameworkPath("defaults/app/Models/Subscription.ts"));let migrationContent=`import type { Database } from '@stacksjs/database'
2
2
  `;migrationContent+=`import { sql } from '@stacksjs/database'
3
3
 
4
4
  `;migrationContent+=`export async function up(db: Database<any>) {
@@ -1 +1 @@
1
- import{Buffer}from"node:buffer";import{createHash}from"node:crypto";import{closeSync,openSync,readFileSync,statSync,unlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql"&&dialect!=="vitess")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress - lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs>STALE_LOCK_MS)try{unlinkSync(path)}catch{}}catch{}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=firstColumnValue(row,column);return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=firstColumnValue(row,column);if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function firstColumnValue(row,column){if(!row||typeof row!=="object")return;const record=row;if(column in record)return record[column];const values=Object.values(record);return values.length===1?values[0]:void 0}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}
1
+ import{Buffer}from"node:buffer";import{createHash}from"node:crypto";import{closeSync,openSync,statSync,unlinkSync,writeFileSync}from"node:fs";import process from"node:process";import{userDatabasePath}from"@stacksjs/path";const DEFAULT_TIMEOUT_MS=30000,INITIAL_BACKOFF_MS=100,MAX_BACKOFF_MS=2000,STALE_LOCK_MS=60000,LOCK_NAME="stacks_migrations";export async function acquireMigrationLock(dialect,adminDb,opts={}){const timeoutMs=opts.timeoutMs??DEFAULT_TIMEOUT_MS;if(dialect!=="sqlite"&&dialect!=="postgres"&&dialect!=="mysql"&&dialect!=="vitess")throw Error(`[migration-lock] unknown dialect: ${String(dialect)}`);if(dialect==="sqlite")return acquireSqliteLock(opts.sqliteLockPath,timeoutMs);if(!adminDb)throw Error(`[migration-lock] ${dialect} requires a database connection to acquire the lock`);if(dialect==="postgres")return acquirePostgresLock(adminDb,timeoutMs);return acquireMySqlLock(adminDb,timeoutMs)}function lockKeysForPostgres(){const hash=createHash("sha256").update(LOCK_NAME).digest(),key1=hash.readInt32BE(0),key2=hash.readInt32BE(4);return{key1,key2}}async function acquirePostgresLock(adminDb,timeoutMs){const{key1,key2}=lockKeysForPostgres(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT pg_try_advisory_lock(${key1}, ${key2}) AS acquired`);if(extractFirstBool(result,"acquired"))return{release:async()=>{try{await adminDb.unsafe(`SELECT pg_advisory_unlock(${key1}, ${key2})`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire postgres advisory lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}async function acquireMySqlLock(adminDb,timeoutMs){const start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){const result=await adminDb.unsafe(`SELECT GET_LOCK('${LOCK_NAME}', 0) AS acquired`);if(extractFirstInt(result,"acquired")===1)return{release:async()=>{try{await adminDb.unsafe(`SELECT RELEASE_LOCK('${LOCK_NAME}')`)}catch{}}};if(Date.now()-start>=timeoutMs)throw Error("[migration-lock] another migration is in progress - could not acquire MySQL named lock within timeout");await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function defaultSqliteLockPath(){return userDatabasePath(".migration.lock")}async function acquireSqliteLock(lockPath,timeoutMs){const path=lockPath??defaultSqliteLockPath(),start=Date.now();let backoff=INITIAL_BACKOFF_MS;while(!0){if(tryCreateLockFile(path)){let released=!1;return{release:async()=>{if(released)return;released=!0;try{unlinkSync(path)}catch{}}}}reclaimIfStale(path);if(Date.now()-start>=timeoutMs)throw Error(`[migration-lock] another migration is in progress - lock file ${path} held within timeout`);await sleepWithJitter(backoff);backoff=Math.min(backoff*2,MAX_BACKOFF_MS)}}function tryCreateLockFile(path){try{const fd=openSync(path,"wx");try{const payload=JSON.stringify({pid:process.pid,startedAt:new Date().toISOString()});writeFileSync(fd,Buffer.from(payload,"utf8"))}finally{closeSync(fd)}return!0}catch(e){if(e.code==="EEXIST")return!1;throw e}}function reclaimIfStale(path){try{const st=statSync(path);if(Date.now()-st.mtimeMs>STALE_LOCK_MS)try{unlinkSync(path)}catch{}}catch{}}function sleepWithJitter(ms){const jittered=ms*(1+Math.random()*0.25);return new Promise((resolve)=>setTimeout(resolve,jittered))}function extractFirstBool(result,column){const row=pluckFirstRow(result);if(!row)return!1;const value=firstColumnValue(row,column);return value===!0||value===1||value==="1"||value==="t"}function extractFirstInt(result,column){const row=pluckFirstRow(result);if(!row)return null;const value=firstColumnValue(row,column);if(typeof value==="number")return value;if(typeof value==="string"&&/^-?\d+$/.test(value))return Number.parseInt(value,10);return null}function firstColumnValue(row,column){if(!row||typeof row!=="object")return;const record=row;if(column in record)return record[column];const values=Object.values(record);return values.length===1?values[0]:void 0}function pluckFirstRow(result){if(!result)return null;if(Array.isArray(result))return result[0];if(typeof result==="object"&&"rows"in result&&Array.isArray(result.rows))return result.rows[0];return null}
@@ -1 +1 @@
1
- import process from"node:process";import{log}from"@stacksjs/logging";import{path}from"@stacksjs/path";import{getModelName,getTableName}from"@stacksjs/orm";import{fs}from"@stacksjs/storage";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return process.env.DB_CONNECTION||"sqlite"}function uuidColumnType(sql){if(sql.isPostgres)return"UUID";if(sql.isMysql)return"VARCHAR(255)";return"TEXT"}export function uuidColumnSql(table,sql){return`ALTER TABLE ${table} ADD COLUMN uuid ${uuidColumnType(sql)}`}async function loadModelsFrom(dir){const out=[];if(!fs.existsSync(dir))return out;const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){out.push(...await loadModelsFrom(fullPath));continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith("_")||entry.name.startsWith("index"))continue;try{const imported=(await import(fullPath)).default;if(imported?.name||imported?.table)out.push({filePath:fullPath,model:imported})}catch{}}return out}export async function findUuidTables(){const dirs=[path.userModelsPath(),path.frameworkPath("defaults/app/Models")],tables=new Set;for(const dir of dirs)for(const{filePath,model}of await loadModelsFrom(dir)){if(!model.traits?.useUuid)continue;tables.add(getTableName(model,filePath))}return[...tables]}export async function ensureUuidColumns(sql,options={}){const tables=await findUuidTables();for(const table of tables)try{await db.unsafe(uuidColumnSql(table,sql)).execute();if(options.verbose)log.debug(`[uuid-columns] Added uuid column to ${table}`)}catch{if(options.verbose)log.debug(`[uuid-columns] Skipped (already applied or ${table} missing): uuid column`)}}export async function ensureUuidColumnsForCurrentDriver(options={}){await ensureUuidColumns(sqlHelpers(getDbDriver()),options)}
1
+ import process from"node:process";import{log}from"@stacksjs/logging";import{path}from"@stacksjs/path";import{getTableName}from"@stacksjs/orm";import{fs}from"@stacksjs/storage";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return process.env.DB_CONNECTION||"sqlite"}function uuidColumnType(sql){if(sql.isPostgres)return"UUID";if(sql.isMysql)return"VARCHAR(255)";return"TEXT"}export function uuidColumnSql(table,sql){return`ALTER TABLE ${table} ADD COLUMN uuid ${uuidColumnType(sql)}`}async function loadModelsFrom(dir){const out=[];if(!fs.existsSync(dir))return out;const entries=fs.readdirSync(dir,{withFileTypes:!0});for(const entry of entries){const fullPath=path.join(dir,entry.name);if(entry.isDirectory()){out.push(...await loadModelsFrom(fullPath));continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith("_")||entry.name.startsWith("index"))continue;try{const imported=(await import(fullPath)).default;if(imported?.name||imported?.table)out.push({filePath:fullPath,model:imported})}catch{}}return out}export async function findUuidTables(){const dirs=[path.userModelsPath(),path.frameworkPath("defaults/app/Models")],tables=new Set;for(const dir of dirs)for(const{filePath,model}of await loadModelsFrom(dir)){if(!model.traits?.useUuid)continue;tables.add(getTableName(model,filePath))}return[...tables]}export async function ensureUuidColumns(sql,options={}){const tables=await findUuidTables();for(const table of tables)try{await db.unsafe(uuidColumnSql(table,sql)).execute();if(options.verbose)log.debug(`[uuid-columns] Added uuid column to ${table}`)}catch{if(options.verbose)log.debug(`[uuid-columns] Skipped (already applied or ${table} missing): uuid column`)}}export async function ensureUuidColumnsForCurrentDriver(options={}){await ensureUuidColumns(sqlHelpers(getDbDriver()),options)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.57",
5
+ "version": "0.72.60",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -60,22 +60,22 @@
60
60
  "prepublishOnly": "bun run build"
61
61
  },
62
62
  "dependencies": {
63
- "@stacksjs/faker": "^0.72.57",
64
- "@stacksjs/query-builder": "^0.72.57",
65
- "@stacksjs/ts-validation": "^0.5.5",
63
+ "@stacksjs/faker": "^0.72.60",
64
+ "@stacksjs/query-builder": "^0.72.60",
65
+ "@stacksjs/ts-validation": "^0.5.6",
66
66
  "bun-query-builder": "^0.2.53",
67
67
  "dynamodb-tooling": "^0.3.2"
68
68
  },
69
69
  "devDependencies": {
70
- "@stacksjs/cli": "0.72.57",
71
- "@stacksjs/config": "0.72.57",
72
- "@stacksjs/logging": "0.72.57",
73
- "@stacksjs/router": "0.72.57",
70
+ "@stacksjs/cli": "0.72.60",
71
+ "@stacksjs/config": "0.72.60",
72
+ "@stacksjs/logging": "0.72.60",
73
+ "@stacksjs/router": "0.72.60",
74
74
  "better-dx": "^0.2.24",
75
- "@stacksjs/path": "0.72.57",
76
- "@stacksjs/query-builder": "0.72.57",
77
- "@stacksjs/storage": "0.72.57",
78
- "@stacksjs/strings": "0.72.57",
79
- "@stacksjs/utils": "0.72.57"
75
+ "@stacksjs/path": "0.72.60",
76
+ "@stacksjs/query-builder": "0.72.60",
77
+ "@stacksjs/storage": "0.72.60",
78
+ "@stacksjs/strings": "0.72.60",
79
+ "@stacksjs/utils": "0.72.60"
80
80
  }
81
81
  }