@stacksjs/database 0.75.255 → 0.75.256

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 +1 @@
1
- import{log}from"@stacksjs/logging";import{getTableName}from"@stacksjs/model-meta";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{plural,snakeCase}from"@stacksjs/strings";import{db}from"../utils";import{enumValidator,isBooleanValidator,isDatetimeValidator,isDateValidator,isFloatValidator,isNumberValidator,isStringValidator,isTimestampValidator,isUnixValidator}from"../validators";export async function deleteMigrationFiles(){const files=await fs.promises.readdir(path.userMigrationsPath());if(files.length){for(const file of files)if(file.endsWith(".ts")){const migrationPath=path.userMigrationsPath(`${file}`);if(fs.existsSync(migrationPath))await Bun.$`rm ${migrationPath}`}}}export async function deleteFrameworkModels(){const cacheDir=path.frameworkPath("cache/models");if(!fs.existsSync(cacheDir))return;const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const modelFile of modelFiles)if(modelFile.endsWith(".ts")){const modelPath=path.frameworkPath(`cache/models/${modelFile}`);if(fs.existsSync(modelPath))await Bun.$`rm ${modelPath}`}}}export async function getLastMigrationFields(modelName){const model=(await import(path.frameworkPath(`cache/models/${modelName}`))).default;let fields={};if(typeof model.attributes==="object")fields=model.attributes;else try{fields=JSON.parse(model.attributes||"{}")}catch{fields={}}return fields}export async function modelTableName(model){if(typeof model==="string")model=(await import(model)).default;return model.table??snakeCase(plural(model?.name||""))}export async function hasTableBeenMigrated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return(await getExecutedMigrations()).some((migration)=>migration.name.includes(tableName))}export async function hasMigrationBeenCreated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return globSync([path.userMigrationsPath("*.ts")],{absolute:!0}).some((path)=>path.includes(`create-${tableName}`))}export async function getExecutedMigrations(){try{return await db.selectFrom("migrations").select("name").execute()}catch(error){if(error?.message.includes("no such table: migrations")){console.warn("Migrations table does not exist, returning empty list.");return[]}return[]}}function findCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max");return maxLengthRule?.params?.length||maxLengthRule?.params?.max||255}return 255}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";return`'varchar(${findCharacterLength(validator)})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}export function compareRanges(range1,range2){return range1.min===range2.min&&range1.max===range2.max}export async function checkPivotMigration(dynamicPart){return(await fs.promises.readdir(path.userMigrationsPath())).some((migrationFile)=>{const escapedDynamicPart=dynamicPart.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(-${escapedDynamicPart}-)`).test(migrationFile)})}export function pluckChanges(array1,array2){const removed=array1.filter((item)=>!array2.includes(item)),added=array2.filter((item)=>!array1.includes(item));if(removed.length===0&&added.length===0)return null;return{added,removed}}export function arrangeColumns(attributes){if(!attributes)return[];const entries=Object.entries(attributes);entries.sort(([_keyA,valueA],[_keyB,valueB])=>{const orderA=valueA.order??Number.POSITIVE_INFINITY,orderB=valueB.order??Number.POSITIVE_INFINITY;return orderA-orderB});return entries}export function isArrayEqual(arr1,arr2){if(!arr1||!arr2)return!1;if(arr1.length!==arr2.length)return!1;for(let i=0;i<arr1.length;i++)if(arr1[i]!==arr2[i])return!1;return!0}export function findDifferingKeys(obj1,obj2){const differingKeys=[];for(const key in obj1){const before=obj1[key],after=obj2[key];if(before&&after){const lastCharacterLength=findCharacterLength(before.validation.rule),latestCharacterLength=findCharacterLength(after.validation.rule);if(lastCharacterLength!==void 0&&latestCharacterLength!==void 0){if(lastCharacterLength!==latestCharacterLength)differingKeys.push({key,max:latestCharacterLength,min:latestCharacterLength})}}}return differingKeys}export async function fetchTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),upvoteTable=getUpvoteTableName(model,tableName);if(upvoteTable)tables.push(upvoteTable);tables.push(tableName)}return tables}export function getUpvoteTableName(model,tableName){const defaultTable=`${tableName}_likes`,traits=model.traits;if(!traits?.likeable)return;return typeof traits.likeable==="object"?traits.likeable.table||defaultTable:defaultTable}export function getLikeableForeignKey(model,tableName){const likeable=model.traits?.likeable;if(likeable&&typeof likeable==="object"&&!Array.isArray(likeable)&&likeable.foreignKey)return likeable.foreignKey;return`${tableName.replace(/s$/,"")}_id`}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="postgres")return"'varchar(255)'";if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}export function mapFieldTypeToColumnType(validator,driver="mysql"){if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return driver==="postgres"?"'timestamp'":"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float4'";if(["array","object"].includes(validator.name))return driver==="sqlite"?"'text'":"'json'";if(driver==="sqlite")return"'text'";return driver==="mysql"?"'varchar(255)'":"'text'"}export function checkIsRequired(rule){return rule.includes(".required()")}
1
+ import{log}from"@stacksjs/logging";import{getTableName}from"@stacksjs/model-meta";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{plural,snakeCase}from"@stacksjs/strings";import{db}from"../utils";import{enumValidator,isBooleanValidator,isDatetimeValidator,isDateValidator,isFloatValidator,isNumberValidator,isStringValidator,isTimestampValidator,isUnixValidator}from"../validators";export async function deleteMigrationFiles(){const files=await fs.promises.readdir(path.userMigrationsPath());if(files.length){for(const file of files)if(file.endsWith(".ts")){const migrationPath=path.userMigrationsPath(`${file}`);if(fs.existsSync(migrationPath))await Bun.$`rm ${migrationPath}`}}}export async function deleteFrameworkModels(){const cacheDir=path.frameworkPath("cache/models");if(!fs.existsSync(cacheDir))return;const modelFiles=await fs.promises.readdir(cacheDir);if(modelFiles.length){for(const modelFile of modelFiles)if(modelFile.endsWith(".ts")){const modelPath=path.frameworkPath(`cache/models/${modelFile}`);if(fs.existsSync(modelPath))await Bun.$`rm ${modelPath}`}}}export async function getLastMigrationFields(modelName){const model=(await import(path.frameworkPath(`cache/models/${modelName}`))).default;let fields={};if(typeof model.attributes==="object")fields=model.attributes;else try{fields=JSON.parse(model.attributes||"{}")}catch{fields={}}return fields}export async function modelTableName(model){if(typeof model==="string")model=(await import(model)).default;return model.table??snakeCase(plural(model?.name||""))}export async function hasTableBeenMigrated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return(await getExecutedMigrations()).some((migration)=>migration.name.includes(tableName))}export async function hasMigrationBeenCreated(tableName){log.debug(`hasTableBeenMigrated for table: ${tableName}`);return globSync([path.userMigrationsPath("*.ts")],{absolute:!0}).some((path)=>path.includes(`create-${tableName}`))}export async function getExecutedMigrations(){try{return await db.selectFrom("migrations").select("name").execute()}catch(error){if(error?.message.includes("no such table: migrations")){console.warn("Migrations table does not exist, returning empty list.");return[]}return[]}}function findCharacterLength(validator){if("getRules"in validator){const maxLengthRule=validator.getRules().find((rule)=>rule.name==="max");return maxLengthRule?.params?.length||maxLengthRule?.params?.max||255}return 255}export function prepareTextColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";return`'varchar(${findCharacterLength(validator)})'`}export function prepareDateTimeColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'text'";const name=validator.name;if(name==="unix")return"'bigint'";return name||"date"}export function compareRanges(range1,range2){return range1.min===range2.min&&range1.max===range2.max}export async function checkPivotMigration(dynamicPart){return(await fs.promises.readdir(path.userMigrationsPath())).some((migrationFile)=>{const escapedDynamicPart=dynamicPart.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(-${escapedDynamicPart}-)`).test(migrationFile)})}export function pluckChanges(array1,array2){const removed=array1.filter((item)=>!array2.includes(item)),added=array2.filter((item)=>!array1.includes(item));if(removed.length===0&&added.length===0)return null;return{added,removed}}export function arrangeColumns(attributes){if(!attributes)return[];const entries=Object.entries(attributes);entries.sort(([_keyA,valueA],[_keyB,valueB])=>{const orderA=valueA.order??Number.POSITIVE_INFINITY,orderB=valueB.order??Number.POSITIVE_INFINITY;return orderA-orderB});return entries}export function isArrayEqual(arr1,arr2){if(!arr1||!arr2)return!1;if(arr1.length!==arr2.length)return!1;for(let i=0;i<arr1.length;i++)if(arr1[i]!==arr2[i])return!1;return!0}export function findDifferingKeys(obj1,obj2){const differingKeys=[];for(const key in obj1){const before=obj1[key],after=obj2[key];if(before&&after){const lastCharacterLength=findCharacterLength(before.validation.rule),latestCharacterLength=findCharacterLength(after.validation.rule);if(lastCharacterLength!==void 0&&latestCharacterLength!==void 0){if(lastCharacterLength!==latestCharacterLength)differingKeys.push({key,max:latestCharacterLength,min:latestCharacterLength})}}}return differingKeys}export async function fetchTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.defaultsPath("app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath),upvoteTable=getUpvoteTableName(model,tableName);if(upvoteTable)tables.push(upvoteTable);tables.push(tableName)}return tables}export function getUpvoteTableName(model,tableName){const defaultTable=`${tableName}_likes`,traits=model.traits;if(!traits?.likeable)return;return typeof traits.likeable==="object"?traits.likeable.table||defaultTable:defaultTable}export function getLikeableForeignKey(model,tableName){const likeable=model.traits?.likeable;if(likeable&&typeof likeable==="object"&&!Array.isArray(likeable)&&likeable.foreignKey)return likeable.foreignKey;return`${tableName.replace(/s$/,"")}_id`}export function prepareNumberColumnType(validator,driver="mysql"){if(driver==="sqlite")return"'integer'";if("getRules"in validator){const minRule=validator.getRules().find((rule)=>rule.name==="min"),maxRule=validator.getRules().find((rule)=>rule.name==="max"),min=minRule?.params?.min??-2147483648,max=maxRule?.params?.max??2147483647;return min>=-2147483648&&max<=2147483647?"'integer'":"'bigint'"}return"'integer'"}export function prepareEnumColumnType(validator,driver="mysql"){const allowedValues=validator.getAllowedValues();if(!allowedValues)throw Error("Enum rule found but no allowedValues defined");const enumStructure=allowedValues.map((value)=>`'${value}'`).join(", ");if(driver==="postgres")return"'varchar(255)'";if(driver==="sqlite")return"'text'";return`sql\`enum(${enumStructure})\``}export function mapFieldTypeToColumnType(validator,driver="mysql"){if(enumValidator(validator))return prepareEnumColumnType(validator,driver);if(isStringValidator(validator))return prepareTextColumnType(validator,driver);if(isNumberValidator(validator))return prepareNumberColumnType(validator,driver);if(isBooleanValidator(validator))return"'boolean'";if(isDateValidator(validator))return"'date'";if(isDatetimeValidator(validator))return driver==="postgres"?"'timestamp'":"'datetime'";if(isUnixValidator(validator))return"'bigint'";if(isTimestampValidator(validator))return"'timestamp'";if(isFloatValidator(validator))return"'float4'";if(["array","object"].includes(validator.name))return driver==="sqlite"?"'text'":"'json'";if(driver==="sqlite")return"'text'";return driver==="mysql"?"'varchar(255)'":"'text'"}export function checkIsRequired(rule){return rule.includes(".required()")}
@@ -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{getModelName,getPivotTables,getTableName}from"@stacksjs/model-meta";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/model-meta";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.defaultsPath("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{getModelName,getPivotTables,getTableName}from"@stacksjs/model-meta";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/model-meta";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.defaultsPath("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>) {
@@ -85,4 +85,4 @@ import{log}from"@stacksjs/logging";function italic(str){return`\x1B[3m${str}\x1B
85
85
  `}function generatePrimaryKeyIndexSQL(tableName){return` await db.schema.createIndex('${tableName}_id_index').on('${tableName}').column('id').execute()
86
86
  `}function generateForeignKeyIndexSQL(tableName,foreignKey){return` await db.schema.createIndex('${tableName}_${foreignKey}_index').on('${tableName}').column('${foreignKey}').execute()
87
87
 
88
- `}export async function fetchPostgresTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath);tables.push(tableName)}return tables}
88
+ `}export async function fetchPostgresTables(){const modelFiles=globSync([path.userModelsPath("*.ts"),path.defaultsPath("app/Models/**/*.ts")],{absolute:!0}),tables=[];for(const modelPath of modelFiles){const model=(await import(modelPath)).default,tableName=getTableName(model,modelPath);tables.push(tableName)}return tables}
@@ -1,4 +1,4 @@
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/model-meta";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'
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/model-meta";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.defaultsPath("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
2
  `;migrationContent+=`import { sql } from '@stacksjs/database'
3
3
 
4
4
  `;migrationContent+=`export async function up(db: Database<any>) {
package/dist/fk-audit.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync}from"node:fs";import{dirname}from"node:path";import{plural,snakeCase}from"@stacksjs/strings";import{path}from"@stacksjs/path";import{globSync}from"@stacksjs/storage";import{dialectCapabilities,isKnownDialect,resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";export function safeGlob(pattern){const metaIdx=pattern.search(/[*?[]/),root=metaIdx===-1?dirname(pattern):dirname(pattern.slice(0,metaIdx));if(!existsSync(root))return[];try{return globSync(pattern,{absolute:!0})}catch{return[]}}function modelTable(model){return model.table||plural(snakeCase(model.name||""))}function modelNameForColumn(column){return column.replace(/_id$/,"").split("_").map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("")}export function getDeclaredFKsFromModels(models){const meta=new Map(models.map((model)=>[String(model.name??""),{primaryKey:String(model.primaryKey??"id"),table:modelTable(model)}])),declared=[],seen=new Set,push=(fk)=>{const key=`${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();if(seen.has(key))return;seen.add(key);declared.push(fk)};for(const model of models){const fromTable=modelTable(model),modelName=String(model.name??""),attributes=model.attributes??{},belongsTo=model.belongsTo,relations=Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.keys(belongsTo).map((name)=>({model:name})):[],declaresBelongsTo=relations.length>0,columns=new Set(Object.keys(attributes).map((name)=>snakeCase(name))),polymorphic=(column)=>columns.has(`${column.slice(0,-3)}_type`);for(const[attributeName,attribute]of Object.entries(attributes)){const fromColumn=snakeCase(attributeName);if(!fromColumn.endsWith("_id")||attribute.foreignKey===!1||polymorphic(fromColumn))continue;if(attribute.foreignKey&&typeof attribute.foreignKey==="object"){push({fromTable,fromColumn,toTable:attribute.foreignKey.table,toColumn:attribute.foreignKey.column??"id",model:modelName});continue}const declaredTarget=relations.some((entry)=>{const relatedName=typeof entry==="string"?entry:String(entry.model??"");return(typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`)===fromColumn});if(declaresBelongsTo&&!declaredTarget)continue;const related=meta.get(modelNameForColumn(fromColumn));if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}for(const entry of relations){const relatedName=typeof entry==="string"?entry:String(entry.model??"");if(!relatedName)continue;const fromColumn=typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`;if(Object.keys(attributes).some((attribute)=>snakeCase(attribute)===fromColumn)||polymorphic(fromColumn))continue;const related=meta.get(relatedName);if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}}return declared}export async function getDeclaredFKs(){const userModelFiles=safeGlob(path.userModelsPath("*.ts")),modelFiles=userModelFiles.length>0?userModelFiles:safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts")),models=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(model&&typeof model==="object")models.push(model)}return getDeclaredFKsFromModels(models)}export async function getLiveFKs(){const{db}=await import("./utils"),dialect=currentDialect();if(dialect==="sqlite")return getSqliteLiveFKs(db);if(dialect==="mysql")return getMysqlLiveFKs(db);if(dialect==="postgres")return getPostgresLiveFKs(db);return[]}export async function getLiveTables(){const{db}=await import("./utils"),dialect=currentDialect(),query=dialect==="sqlite"?"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'":dialect==="mysql"?"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()":dialect==="postgres"?"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()":"";if(!query)return new Set;try{const rows=await db.unsafe(query);return new Set((Array.isArray(rows)?rows:[]).map((row)=>row.name).filter((name)=>Boolean(name)).map((name)=>name.toLowerCase()))}catch{return new Set}}export async function auditForeignKeys(){const declared=await getDeclaredFKs(),live=await getLiveFKs(),liveKeys=new Set(live.map((fk)=>`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)),liveTables=await getLiveTables();return{declared,live,...classifyDeclaredFKs(declared,liveKeys,liveTables)}}export function classifyDeclaredFKs(declared,liveKeys,liveTables){const canClassify=liveTables.size>0,missing=[],absentTable=[];for(const d of declared){const key=`${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;if(liveKeys.has(key))continue;if(canClassify&&!liveTables.has(d.fromTable.toLowerCase()))absentTable.push(d);else missing.push(d)}return{missing,absentTable}}export function fkKey(fk){return`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`}export async function findFkOrphans(dialect){if((dialect??currentDialect())!=="sqlite")return{supported:!1,total:0,orphans:[]};const{db}=await import("./utils"),rows=await db.unsafe("PRAGMA foreign_key_check").execute(),checkRows=Array.isArray(rows)?rows:[],fkListCache=new Map;async function fkListFor(table){if(fkListCache.has(table))return fkListCache.get(table);if(!/^[a-z_]\w*$/i.test(table)){fkListCache.set(table,[]);return[]}const list=await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(),arr=Array.isArray(list)?list:[];fkListCache.set(table,arr);return arr}const grouped=new Map;for(const raw of checkRows){const r=raw,table=String(r.table??""),parent=String(r.parent??"");if(!table||!parent)continue;const fkid=Number(r.fkid??0);let column="";const fkList=await fkListFor(table);for(const fk of fkList){const f=fk;if(Number(f.id)===fkid&&f.from){column=String(f.from);break}}const key=`${table}\x00${parent}\x00${fkid}`;let entry=grouped.get(key);if(!entry){entry={table,column,parent,count:0,sampleRowids:[]};grouped.set(key,entry)}entry.count++;if(entry.sampleRowids.length<5&&typeof r.rowid==="number")entry.sampleRowids.push(r.rowid)}const orphans=[...grouped.values()];return{supported:!0,total:orphans.reduce((sum,o)=>sum+o.count,0),orphans}}function currentDialect(){const driver=resolvedDatabaseDriver().toLowerCase();if(!isKnownDialect(driver)||!dialectCapabilities(driver).supportsForeignKeys)return"other";return toSqlIntrospectionDialect(driver)}async function getSqliteLiveFKs(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"),rows=Array.isArray(tables)?tables:[],fks=[];for(const row of rows){const fromTable=row.name;if(!fromTable)continue;if(!/^[a-z_][\w]*$/i.test(fromTable))continue;const fkRows=await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);for(const fk of Array.isArray(fkRows)?fkRows:[]){const r=fk;if((r.seq??0)!==0)continue;if(!r.from||!r.to||!r.table)continue;fks.push({fromTable,fromColumn:r.from,toTable:r.table,toColumn:r.to})}}return fks}async function getMysqlLiveFKs(db){const rows=await db.unsafe(`
1
+ import{existsSync}from"node:fs";import{dirname}from"node:path";import{plural,snakeCase}from"@stacksjs/strings";import{path}from"@stacksjs/path";import{globSync}from"@stacksjs/storage";import{dialectCapabilities,isKnownDialect,resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";export function safeGlob(pattern){const metaIdx=pattern.search(/[*?[]/),root=metaIdx===-1?dirname(pattern):dirname(pattern.slice(0,metaIdx));if(!existsSync(root))return[];try{return globSync(pattern,{absolute:!0})}catch{return[]}}function modelTable(model){return model.table||plural(snakeCase(model.name||""))}function modelNameForColumn(column){return column.replace(/_id$/,"").split("_").map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("")}export function getDeclaredFKsFromModels(models){const meta=new Map(models.map((model)=>[String(model.name??""),{primaryKey:String(model.primaryKey??"id"),table:modelTable(model)}])),declared=[],seen=new Set,push=(fk)=>{const key=`${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();if(seen.has(key))return;seen.add(key);declared.push(fk)};for(const model of models){const fromTable=modelTable(model),modelName=String(model.name??""),attributes=model.attributes??{},belongsTo=model.belongsTo,relations=Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.keys(belongsTo).map((name)=>({model:name})):[],declaresBelongsTo=relations.length>0,columns=new Set(Object.keys(attributes).map((name)=>snakeCase(name))),polymorphic=(column)=>columns.has(`${column.slice(0,-3)}_type`);for(const[attributeName,attribute]of Object.entries(attributes)){const fromColumn=snakeCase(attributeName);if(!fromColumn.endsWith("_id")||attribute.foreignKey===!1||polymorphic(fromColumn))continue;if(attribute.foreignKey&&typeof attribute.foreignKey==="object"){push({fromTable,fromColumn,toTable:attribute.foreignKey.table,toColumn:attribute.foreignKey.column??"id",model:modelName});continue}const declaredTarget=relations.some((entry)=>{const relatedName=typeof entry==="string"?entry:String(entry.model??"");return(typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`)===fromColumn});if(declaresBelongsTo&&!declaredTarget)continue;const related=meta.get(modelNameForColumn(fromColumn));if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}for(const entry of relations){const relatedName=typeof entry==="string"?entry:String(entry.model??"");if(!relatedName)continue;const fromColumn=typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`;if(Object.keys(attributes).some((attribute)=>snakeCase(attribute)===fromColumn)||polymorphic(fromColumn))continue;const related=meta.get(relatedName);if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}}return declared}export async function getDeclaredFKs(){const userModelFiles=safeGlob(path.userModelsPath("*.ts")),modelFiles=userModelFiles.length>0?userModelFiles:safeGlob(path.defaultsPath("app/Models/**/*.ts")),models=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(model&&typeof model==="object")models.push(model)}return getDeclaredFKsFromModels(models)}export async function getLiveFKs(){const{db}=await import("./utils"),dialect=currentDialect();if(dialect==="sqlite")return getSqliteLiveFKs(db);if(dialect==="mysql")return getMysqlLiveFKs(db);if(dialect==="postgres")return getPostgresLiveFKs(db);return[]}export async function getLiveTables(){const{db}=await import("./utils"),dialect=currentDialect(),query=dialect==="sqlite"?"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'":dialect==="mysql"?"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()":dialect==="postgres"?"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()":"";if(!query)return new Set;try{const rows=await db.unsafe(query);return new Set((Array.isArray(rows)?rows:[]).map((row)=>row.name).filter((name)=>Boolean(name)).map((name)=>name.toLowerCase()))}catch{return new Set}}export async function auditForeignKeys(){const declared=await getDeclaredFKs(),live=await getLiveFKs(),liveKeys=new Set(live.map((fk)=>`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)),liveTables=await getLiveTables();return{declared,live,...classifyDeclaredFKs(declared,liveKeys,liveTables)}}export function classifyDeclaredFKs(declared,liveKeys,liveTables){const canClassify=liveTables.size>0,missing=[],absentTable=[];for(const d of declared){const key=`${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;if(liveKeys.has(key))continue;if(canClassify&&!liveTables.has(d.fromTable.toLowerCase()))absentTable.push(d);else missing.push(d)}return{missing,absentTable}}export function fkKey(fk){return`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`}export async function findFkOrphans(dialect){if((dialect??currentDialect())!=="sqlite")return{supported:!1,total:0,orphans:[]};const{db}=await import("./utils"),rows=await db.unsafe("PRAGMA foreign_key_check").execute(),checkRows=Array.isArray(rows)?rows:[],fkListCache=new Map;async function fkListFor(table){if(fkListCache.has(table))return fkListCache.get(table);if(!/^[a-z_]\w*$/i.test(table)){fkListCache.set(table,[]);return[]}const list=await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(),arr=Array.isArray(list)?list:[];fkListCache.set(table,arr);return arr}const grouped=new Map;for(const raw of checkRows){const r=raw,table=String(r.table??""),parent=String(r.parent??"");if(!table||!parent)continue;const fkid=Number(r.fkid??0);let column="";const fkList=await fkListFor(table);for(const fk of fkList){const f=fk;if(Number(f.id)===fkid&&f.from){column=String(f.from);break}}const key=`${table}\x00${parent}\x00${fkid}`;let entry=grouped.get(key);if(!entry){entry={table,column,parent,count:0,sampleRowids:[]};grouped.set(key,entry)}entry.count++;if(entry.sampleRowids.length<5&&typeof r.rowid==="number")entry.sampleRowids.push(r.rowid)}const orphans=[...grouped.values()];return{supported:!0,total:orphans.reduce((sum,o)=>sum+o.count,0),orphans}}function currentDialect(){const driver=resolvedDatabaseDriver().toLowerCase();if(!isKnownDialect(driver)||!dialectCapabilities(driver).supportsForeignKeys)return"other";return toSqlIntrospectionDialect(driver)}async function getSqliteLiveFKs(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"),rows=Array.isArray(tables)?tables:[],fks=[];for(const row of rows){const fromTable=row.name;if(!fromTable)continue;if(!/^[a-z_][\w]*$/i.test(fromTable))continue;const fkRows=await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);for(const fk of Array.isArray(fkRows)?fkRows:[]){const r=fk;if((r.seq??0)!==0)continue;if(!r.from||!r.to||!r.table)continue;fks.push({fromTable,fromColumn:r.from,toTable:r.table,toColumn:r.to})}}return fks}async function getMysqlLiveFKs(db){const rows=await db.unsafe(`
2
2
  SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
3
3
  FROM information_schema.KEY_COLUMN_USAGE
4
4
  WHERE TABLE_SCHEMA = DATABASE()
@@ -60,7 +60,7 @@ import{log}from"@stacksjs/logging";import{db}from"./utils";import{resolvedDataba
60
60
  upvoteable_id INTEGER NOT NULL,
61
61
  upvoteable_type VARCHAR(255) NOT NULL,
62
62
  ${createdAt(sql)}
63
- )`}export function traitTableColumnGuarantees(sql){return[{table:"taggables",column:"taggable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`},{table:"categorizables",column:"categorizable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`},{table:"commentables",column:"author_name",definition:"VARCHAR(255)"},{table:"commentables",column:"author_email",definition:"VARCHAR(255)"}]}export function traitTableIndexSql(){return["CREATE INDEX IF NOT EXISTS chat_messages_thread_index ON chat_messages (conversation_id, id)","CREATE INDEX IF NOT EXISTS chat_conversations_first_index ON chat_conversations (scope, participant_type, first_id)","CREATE INDEX IF NOT EXISTS chat_conversations_second_index ON chat_conversations (scope, participant_type, second_id)","CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)","CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)","CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)","CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)","CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)","CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)","CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_owner_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_owner_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"]}export async function likeableTargets(){const{path}=await import("@stacksjs/path"),{globSync}=await import("@stacksjs/storage"),{getTableName}=await import("@stacksjs/model-meta"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export function messagingTableSql(sql){return[`CREATE TABLE IF NOT EXISTS chat_conversations (
63
+ )`}export function traitTableColumnGuarantees(sql){return[{table:"taggables",column:"taggable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`},{table:"categorizables",column:"categorizable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`},{table:"commentables",column:"author_name",definition:"VARCHAR(255)"},{table:"commentables",column:"author_email",definition:"VARCHAR(255)"}]}export function traitTableIndexSql(){return["CREATE INDEX IF NOT EXISTS chat_messages_thread_index ON chat_messages (conversation_id, id)","CREATE INDEX IF NOT EXISTS chat_conversations_first_index ON chat_conversations (scope, participant_type, first_id)","CREATE INDEX IF NOT EXISTS chat_conversations_second_index ON chat_conversations (scope, participant_type, second_id)","CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)","CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)","CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)","CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)","CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)","CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)","CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_owner_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_owner_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"]}export async function likeableTargets(){const{path}=await import("@stacksjs/path"),{globSync}=await import("@stacksjs/storage"),{getTableName}=await import("@stacksjs/model-meta"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.defaultsPath("app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export function messagingTableSql(sql){return[`CREATE TABLE IF NOT EXISTS chat_conversations (
64
64
  id VARCHAR(64) PRIMARY KEY,
65
65
  scope VARCHAR(255) NOT NULL,
66
66
  participant_type VARCHAR(255) NOT NULL,
@@ -1,4 +1,4 @@
1
- import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??currentDialect();if(d==="sqlite")return getSqliteLiveUniques(db);if(d==="mysql")return getMysqlLiveUniques(db);if(d==="postgres")return getPostgresLiveUniques(db);return[]}export async function auditUniqueIndexes(dialect){const d=dialect??currentDialect();if(d!=="sqlite"&&d!=="mysql"&&d!=="postgres")return{supported:!1,declared:[],live:[],missing:[],skippedTables:[]};const declared=await getDeclaredUniques(),live=await getLiveUniqueIndexes(d),liveTables=await getLiveTables(d),liveByTable=new Map;for(const idx of live){const t=idx.table.toLowerCase(),key=columnSetKey(idx.columns);if(!liveByTable.has(t))liveByTable.set(t,new Set);liveByTable.get(t).add(key)}const missing=[],skippedTables=new Set;for(const decl of declared){const t=decl.table.toLowerCase();if(!liveTables.has(t)){skippedTables.add(decl.table);continue}const key=columnSetKey(decl.columns);if(!(liveByTable.get(t)?.has(key)??!1))missing.push(decl)}return{supported:!0,declared,live,missing,skippedTables:[...skippedTables]}}async function getLiveTables(dialect){const{db}=await import("./utils");let rows=[];if(dialect==="sqlite"){const r=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name)}else if(dialect==="mysql"){const r=await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.TABLE_NAME)}else if(dialect==="postgres"){const r=await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.tablename)}return new Set(rows.filter((n)=>typeof n==="string"&&n.length>0).map((n)=>n.toLowerCase()))}function columnSetKey(columns){return[...columns].map((c)=>c.toLowerCase()).sort().join(",")}function currentDialect(){return toSqlIntrospectionDialect(resolvedDatabaseDriver().toLowerCase())}async function getSqliteLiveUniques(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(),rows=Array.isArray(tables)?tables:[],out=[];for(const row of rows){const table=row.name;if(!table)continue;if(!/^[a-z_]\w*$/i.test(table))continue;const indexRows=await db.unsafe(`PRAGMA index_list("${table}")`).execute();for(const idx of Array.isArray(indexRows)?indexRows:[]){const r=idx;if(Number(r.unique)!==1||!r.name)continue;if(!/^[a-z_]\w*$/i.test(r.name))continue;const colRows=await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(),columns=(Array.isArray(colRows)?colRows:[]).map((c)=>String(c.name??"")).filter((c)=>c.length>0);if(columns.length>0)out.push({table,name:r.name,columns})}}return out}async function getMysqlLiveUniques(db){const rows=await db.unsafe(`
1
+ import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{resolvedDatabaseDriver,toSqlIntrospectionDialect}from"./dialect";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.defaultsPath("app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??currentDialect();if(d==="sqlite")return getSqliteLiveUniques(db);if(d==="mysql")return getMysqlLiveUniques(db);if(d==="postgres")return getPostgresLiveUniques(db);return[]}export async function auditUniqueIndexes(dialect){const d=dialect??currentDialect();if(d!=="sqlite"&&d!=="mysql"&&d!=="postgres")return{supported:!1,declared:[],live:[],missing:[],skippedTables:[]};const declared=await getDeclaredUniques(),live=await getLiveUniqueIndexes(d),liveTables=await getLiveTables(d),liveByTable=new Map;for(const idx of live){const t=idx.table.toLowerCase(),key=columnSetKey(idx.columns);if(!liveByTable.has(t))liveByTable.set(t,new Set);liveByTable.get(t).add(key)}const missing=[],skippedTables=new Set;for(const decl of declared){const t=decl.table.toLowerCase();if(!liveTables.has(t)){skippedTables.add(decl.table);continue}const key=columnSetKey(decl.columns);if(!(liveByTable.get(t)?.has(key)??!1))missing.push(decl)}return{supported:!0,declared,live,missing,skippedTables:[...skippedTables]}}async function getLiveTables(dialect){const{db}=await import("./utils");let rows=[];if(dialect==="sqlite"){const r=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name)}else if(dialect==="mysql"){const r=await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.TABLE_NAME)}else if(dialect==="postgres"){const r=await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.tablename)}return new Set(rows.filter((n)=>typeof n==="string"&&n.length>0).map((n)=>n.toLowerCase()))}function columnSetKey(columns){return[...columns].map((c)=>c.toLowerCase()).sort().join(",")}function currentDialect(){return toSqlIntrospectionDialect(resolvedDatabaseDriver().toLowerCase())}async function getSqliteLiveUniques(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(),rows=Array.isArray(tables)?tables:[],out=[];for(const row of rows){const table=row.name;if(!table)continue;if(!/^[a-z_]\w*$/i.test(table))continue;const indexRows=await db.unsafe(`PRAGMA index_list("${table}")`).execute();for(const idx of Array.isArray(indexRows)?indexRows:[]){const r=idx;if(Number(r.unique)!==1||!r.name)continue;if(!/^[a-z_]\w*$/i.test(r.name))continue;const colRows=await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(),columns=(Array.isArray(colRows)?colRows:[]).map((c)=>String(c.name??"")).filter((c)=>c.length>0);if(columns.length>0)out.push({table,name:r.name,columns})}}return out}async function getMysqlLiveUniques(db){const rows=await db.unsafe(`
2
2
  SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
3
3
  FROM information_schema.STATISTICS
4
4
  WHERE TABLE_SCHEMA = DATABASE()
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.255",
5
+ "version": "0.75.256",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -66,26 +66,26 @@
66
66
  "prepublishOnly": "bun run build"
67
67
  },
68
68
  "dependencies": {
69
- "@stacksjs/config": "0.75.255",
70
- "@stacksjs/env": "0.75.255",
71
- "@stacksjs/error-handling": "0.75.255",
72
- "@stacksjs/faker": "0.75.255",
73
- "@stacksjs/features": "0.75.255",
74
- "@stacksjs/logging": "0.75.255",
75
- "@stacksjs/model-meta": "0.75.255",
76
- "@stacksjs/path": "0.75.255",
77
- "@stacksjs/query-builder": "0.75.255",
78
- "@stacksjs/security": "0.75.255",
79
- "@stacksjs/storage": "0.75.255",
80
- "@stacksjs/strings": "0.75.255",
69
+ "@stacksjs/config": "0.75.256",
70
+ "@stacksjs/env": "0.75.256",
71
+ "@stacksjs/error-handling": "0.75.256",
72
+ "@stacksjs/faker": "0.75.256",
73
+ "@stacksjs/features": "0.75.256",
74
+ "@stacksjs/logging": "0.75.256",
75
+ "@stacksjs/model-meta": "0.75.256",
76
+ "@stacksjs/path": "0.75.256",
77
+ "@stacksjs/query-builder": "0.75.256",
78
+ "@stacksjs/security": "0.75.256",
79
+ "@stacksjs/storage": "0.75.256",
80
+ "@stacksjs/strings": "0.75.256",
81
81
  "@stacksjs/ts-validation": "^0.5.6",
82
82
  "bun-query-builder": "^0.3.9",
83
83
  "dynamodb-tooling": "^0.3.6"
84
84
  },
85
85
  "devDependencies": {
86
- "@stacksjs/cli": "0.75.255",
87
- "@stacksjs/router": "0.75.255",
88
- "@stacksjs/utils": "0.75.255",
86
+ "@stacksjs/cli": "0.75.256",
87
+ "@stacksjs/router": "0.75.256",
88
+ "@stacksjs/utils": "0.75.256",
89
89
  "better-dx": "^0.2.24"
90
90
  }
91
91
  }