@stacksjs/database 0.74.24 → 0.74.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/query-logger.js +2 -2
- package/dist/query-parser.d.ts +4 -0
- package/dist/query-parser.js +1 -1
- package/dist/sql-helpers.js +1 -1
- package/dist/utils.js +1 -1
- package/package.json +17 -17
package/dist/query-logger.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{parseQuery}from"./query-parser";import{db}from"./utils";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}
|
|
2
|
-
`).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}
|
|
1
|
+
import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{AsyncLocalStorage}from"node:async_hooks";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{normalizeQuery,parseQuery}from"./query-parser";import{db}from"./utils";const QUERY_TRACKER_KEY=Symbol.for("stacks.database.queryTracker");let configuredQueryTracker=()=>{};export function setQueryTracker(fn){configuredQueryTracker=fn}function trackQuery(query,durationMs,connection){const shared=globalThis[QUERY_TRACKER_KEY];(typeof shared==="function"?shared:configuredQueryTracker)(query,durationMs,connection)}const queryLogContext=new AsyncLocalStorage;export async function logQuery(event){if(queryLogContext.getStore())return;try{const{query,durationMs,error,bindings}=extractQueryInfo(event);if(query)try{trackQuery(query,durationMs,config.database?.default||"unknown")}catch{}if(!config.database?.queryLogging?.enabled)return;const excludedQueries=config.database?.queryLogging?.excludedQueries;if(!query.trim()||isExcludedQuery(query,Array.isArray(excludedQueries)?excludedQueries:[]))return;const status=determineQueryStatus(durationMs,error),logRecord=await createQueryLogRecord(query,durationMs,status,error,bindings);if(config.database?.queryLogging?.analysis?.enabled&&(status==="slow"||config.database.queryLogging.analysis.analyzeAll))await enhanceWithQueryAnalysis(logRecord);await queryLogContext.run(!0,()=>storeQueryLog(logRecord));if(status!=="completed")log[status==="failed"?"error":"warn"](`Query ${status}:`,{query:logRecord.query,duration:logRecord.duration,connection:logRecord.connection,...error&&{error}})}catch(err){log.error("Failed to log query:",err)}}export function isExcludedQuery(query,excludedQueries){const normalizedQuery=query.toLowerCase();return excludedQueries.some((pattern)=>{const normalizedPattern=pattern.trim().toLowerCase();return normalizedPattern.length>0&&normalizedQuery.includes(normalizedPattern)})}function queryText(sql){if(typeof sql==="string")return usableQueryText(sql);if(sql&&typeof sql==="object"&&typeof sql.then==="function")return"";if(sql&&typeof sql.toString==="function")return usableQueryText(String(sql));return""}function usableQueryText(text){return text.startsWith("[object ")?"":text}function extractQueryInfo(event){const query=queryText(event.query?.sql),durationMs=event.queryDurationMillis||0,error=event.error;let bindings;if(event.query?.parameters)try{bindings=JSON.stringify(event.query.parameters)}catch{bindings="[]"}return{query,durationMs,error,bindings}}function determineQueryStatus(durationMs,error){const slowThreshold=config.database?.queryLogging?.slowThreshold||100;if(error)return"failed";if(durationMs>slowThreshold)return"slow";return"completed"}async function createQueryLogRecord(query,durationMs,status,error,bindings){const connection=config.database.default||"unknown",normalizedQuery=normalizeQuery(query)||query,{trace,caller}=extractTraceInfo();return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:error?String(error):void 0,executed_at:sqlDateTime(),bindings,trace,...caller,memory_usage:memoryUsage().heapUsed/1024/1024}}const SECRET_PATTERNS=[/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bAKIA[0-9A-Z]{16}/g,/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g];function sanitizeStackTrace(stack){let out=stack;for(const pattern of SECRET_PATTERNS)out=out.replace(pattern,"<redacted>");return out}let lastTraceInfo;function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"";if(lastTraceInfo?.trace===stack)return lastTraceInfo;const callerLine=stack.split(`
|
|
2
|
+
`).slice(1).find((line)=>!line.includes("query-logger.ts"));let caller={};if(callerLine){const methodMatch=callerLine.match(/at (.+?) \(/),fileMatch=callerLine.match(/\((.+?):(\d+):(\d+)\)/);if(methodMatch&&methodMatch[1]){const methodParts=methodMatch[1].split(".");caller={model:methodParts.length>1?methodParts[0]:void 0,method:methodParts.length>1?methodParts[1]:methodParts[0]}}if(fileMatch&&fileMatch[1]&&fileMatch[2])caller={...caller,file:fileMatch[1],line:Number.parseInt(fileMatch[2],10)}}const result={trace:sanitizeStackTrace(stack),caller};if(result.trace===stack&&stack.length<=8192)lastTraceInfo=result;return result}catch{return{trace:"",caller:{}}}}async function enhanceWithQueryAnalysis(logRecord){try{const{tables,type}=parseQuery(logRecord.query);logRecord.affected_tables=JSON.stringify(tables||[]);if(type==="SELECT"&&config.database?.queryLogging?.analysis?.explainPlan){const explainResult=await getExplainPlan(logRecord.query);if(explainResult){logRecord.explain_plan=explainResult.plan;logRecord.indexes_used=JSON.stringify(explainResult.indexesUsed||[]);logRecord.missing_indexes=JSON.stringify(explainResult.missingIndexes||[]);if(config.database?.queryLogging?.analysis?.suggestions)logRecord.optimization_suggestions=JSON.stringify(generateOptimizationSuggestions(explainResult,logRecord))}}const tags=[type];if(tables&&tables.length>0)tags.push(...tables.map((table)=>`table:${table}`));logRecord.tags=JSON.stringify(tags)}catch(error){log.debug("Error during query analysis:",error)}}const EXPLAIN_DIALECTS=new Set(["sqlite","mysql","postgres"]);async function getExplainPlan(query){try{const rawDriver=(await import("@stacksjs/config")).config?.database?.default,driver=typeof rawDriver==="string"?rawDriver:"sqlite";if(!EXPLAIN_DIALECTS.has(driver))return null;let sqlText;if(driver==="mysql")sqlText=`EXPLAIN FORMAT=JSON ${query}`;else if(driver==="postgres")sqlText=`EXPLAIN (FORMAT JSON) ${query}`;else sqlText=`EXPLAIN QUERY PLAN ${query}`;const result=await db.unsafe?.(sqlText);if(!result)return null;const raw=result,rows=Array.isArray(raw)?raw:raw?.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLog(logRecord){try{await db.insertInto("query_logs").values(logRecord).execute()}catch(error){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}}
|
package/dist/query-parser.d.ts
CHANGED
|
@@ -2,3 +2,7 @@
|
|
|
2
2
|
* Parse and normalize a SQL query
|
|
3
3
|
*/
|
|
4
4
|
export declare function parseQuery(sql: string): { normalized: string, type: string, tables: string[] };
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a SQL query by replacing literal values with placeholders
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeQuery(sql: string): string;
|
package/dist/query-parser.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export function parseQuery(sql){const result={normalized:"",type:"OTHER",tables:[]};if(!sql)return result;try{const upperQuery=sql.trim().toUpperCase();if(upperQuery.startsWith("SELECT"))result.type="SELECT";else if(upperQuery.startsWith("INSERT"))result.type="INSERT";else if(upperQuery.startsWith("UPDATE"))result.type="UPDATE";else if(upperQuery.startsWith("DELETE"))result.type="DELETE";result.tables=extractTables(sql,result.type);result.normalized=normalizeQuery(sql);return result}catch{return{normalized:sql,type:"OTHER",tables:[]}}}function buildIdentifierRegex(keyword,flags="i"){return new RegExp(`${keyword}\\s+(?:"([^"]+)"|\`([^\`]+)\`|\\[([^\\]]+)\\]|([\\w.]+))`,flags)}function pickMatchedIdentifier(match){if(!match)return null;const ident=match[1]??match[2]??match[3]??match[4];if(!ident)return null;return ident.split(".").pop()??null}function extractTables(sql,type){const tables=[];try{switch(type){case"SELECT":{const fromMatch=sql.match(buildIdentifierRegex("from")),fromTable=pickMatchedIdentifier(fromMatch);if(fromTable)tables.push(fromTable);const joinRegex=buildIdentifierRegex("join","gi");for(const m of sql.matchAll(joinRegex)){const joinTable=pickMatchedIdentifier(m);if(joinTable)tables.push(joinTable)}break}case"INSERT":{const intoMatch=sql.match(buildIdentifierRegex("into")),intoTable=pickMatchedIdentifier(intoMatch);if(intoTable)tables.push(intoTable);break}case"UPDATE":{const updateMatch=sql.match(buildIdentifierRegex("update")),updateTable=pickMatchedIdentifier(updateMatch);if(updateTable)tables.push(updateTable);break}case"DELETE":{const deleteFromMatch=sql.match(buildIdentifierRegex("from")),deleteTable=pickMatchedIdentifier(deleteFromMatch);if(deleteTable)tables.push(deleteTable);break}}}catch{}return[...new Set(tables)].filter(Boolean)}function normalizeQuery(sql){try{let normalizedSql=sql;normalizedSql=normalizedSql.replace(/
|
|
1
|
+
export function parseQuery(sql){const result={normalized:"",type:"OTHER",tables:[]};if(!sql)return result;try{const upperQuery=sql.trim().toUpperCase();if(upperQuery.startsWith("SELECT"))result.type="SELECT";else if(upperQuery.startsWith("INSERT"))result.type="INSERT";else if(upperQuery.startsWith("UPDATE"))result.type="UPDATE";else if(upperQuery.startsWith("DELETE"))result.type="DELETE";result.tables=extractTables(sql,result.type);result.normalized=normalizeQuery(sql);return result}catch{return{normalized:sql,type:"OTHER",tables:[]}}}function buildIdentifierRegex(keyword,flags="i"){return new RegExp(`${keyword}\\s+(?:"([^"]+)"|\`([^\`]+)\`|\\[([^\\]]+)\\]|([\\w.]+))`,flags)}function pickMatchedIdentifier(match){if(!match)return null;const ident=match[1]??match[2]??match[3]??match[4];if(!ident)return null;return ident.split(".").pop()??null}function extractTables(sql,type){const tables=[];try{switch(type){case"SELECT":{const fromMatch=sql.match(buildIdentifierRegex("from")),fromTable=pickMatchedIdentifier(fromMatch);if(fromTable)tables.push(fromTable);const joinRegex=buildIdentifierRegex("join","gi");for(const m of sql.matchAll(joinRegex)){const joinTable=pickMatchedIdentifier(m);if(joinTable)tables.push(joinTable)}break}case"INSERT":{const intoMatch=sql.match(buildIdentifierRegex("into")),intoTable=pickMatchedIdentifier(intoMatch);if(intoTable)tables.push(intoTable);break}case"UPDATE":{const updateMatch=sql.match(buildIdentifierRegex("update")),updateTable=pickMatchedIdentifier(updateMatch);if(updateTable)tables.push(updateTable);break}case"DELETE":{const deleteFromMatch=sql.match(buildIdentifierRegex("from")),deleteTable=pickMatchedIdentifier(deleteFromMatch);if(deleteTable)tables.push(deleteTable);break}}}catch{}return[...new Set(tables)].filter(Boolean)}export function normalizeQuery(sql){try{let normalizedSql=sql;normalizedSql=normalizedSql.replace(/'[^']*(?:''[^']*)*'/g,"?");normalizedSql=normalizedSql.replace(/"[^"]*(?:""[^"]*)*"/g,"?");normalizedSql=normalizedSql.replace(/\b(?:\d+|true|false|null)\b/gi,"?");if(/[^\S ]| {2}/.test(normalizedSql))normalizedSql=normalizedSql.replace(/\s+/g," ");normalizedSql=normalizedSql.trim();return normalizedSql}catch{return sql}}
|
package/dist/sql-helpers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dialectCapabilities}from"./dialect";export function sqlDateTime(value
|
|
1
|
+
import{dialectCapabilities}from"./dialect";let lastSqlTimestampMs,lastSqlTimestamp="";export function sqlDateTime(value){if(value!==void 0)return value.toISOString().slice(0,-1);const now=Date.now();if(now!==lastSqlTimestampMs){lastSqlTimestamp=new Date(now).toISOString().slice(0,-1);lastSqlTimestampMs=now}return lastSqlTimestamp}export function sqlDateTimeLiteral(value){return`'${sqlDateTime(value)}'`}export function parseSqlDateTime(value){if(value===null||value===void 0)return null;if(value instanceof Date)return Number.isNaN(value.getTime())?null:value;if(typeof value==="number")return Number.isNaN(value)?null:new Date(value);if(typeof value!=="string")return null;const trimmed=value.trim();if(!trimmed)return null;let normalized=trimmed.replace(" ","T");if(!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized))normalized+="Z";const parsed=new Date(normalized);return Number.isNaN(parsed.getTime())?null:parsed}export function sqlHelpers(driver){const caps=dialectCapabilities(driver),isPostgres=caps.wire==="postgres",isMysql=caps.wire==="mysql",isSqlite=caps.wire==="sqlite";return{driver,isPostgres,isMysql,isSqlite,now:isPostgres||isMysql?"NOW()":"datetime('now')",utcNow:isPostgres?"(now() AT TIME ZONE 'utc')":isMysql?"(UTC_TIMESTAMP)":"CURRENT_TIMESTAMP",boolTrue:isPostgres?"true":"1",boolFalse:isPostgres?"false":"0",autoIncrement:isPostgres?"SERIAL":"INTEGER",bigInteger:isSqlite?"INTEGER":"BIGINT",primaryKey:!caps.supportsAutoIncrement?"PRIMARY KEY":isPostgres?"PRIMARY KEY":isMysql?"PRIMARY KEY AUTO_INCREMENT":"PRIMARY KEY AUTOINCREMENT",pkColumn:!caps.supportsAutoIncrement?"id BIGINT NOT NULL PRIMARY KEY":isPostgres?"id SERIAL PRIMARY KEY":isMysql?"id INTEGER PRIMARY KEY AUTO_INCREMENT":"id INTEGER PRIMARY KEY AUTOINCREMENT",bigPkColumn:!caps.supportsAutoIncrement?"id BIGINT NOT NULL PRIMARY KEY":isPostgres?"id BIGSERIAL PRIMARY KEY":isMysql?"id BIGINT PRIMARY KEY AUTO_INCREMENT":"id INTEGER PRIMARY KEY AUTOINCREMENT",datetime:isMysql?"DATETIME":"TIMESTAMP",nullableTimestamp:isMysql?"DATETIME NULL":"TIMESTAMP",param(index){return isPostgres?`$${index}`:"?"},params(...values){if(isPostgres)return{sql:values.map((_,i)=>`$${i+1}`).join(", "),values};return{sql:values.map(()=>"?").join(", "),values}}}}
|
package/dist/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database?.connections)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}function forwardDatabaseQuery(event){import("./query-logger").then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function serializeSqliteTransaction(run){if(sqliteTxOwner.getStore())return run();const result=sqliteTxTail.then(()=>sqliteTxOwner.run(!0,run));sqliteTxTail=result.then(()=>{return},()=>{return});return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map;export function resetDatabaseConnection(){resetQueryBuilderConnection();_dbInstance=null;_replicaInstances=new Map}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=`${resolved.host}:${resolved.port??""}`,cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const replicas=getReplicas(),policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export function asRows(rows){return rows}export function asRow(row){return row}export const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();const instance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb():getDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
|
|
1
|
+
import{AsyncLocalStorage}from"node:async_hooks";import{createQueryBuilder,registerPersistentQueryHooks,resetConnection as resetQueryBuilderConnection,setConfig}from"@stacksjs/query-builder";import{SQL}from"bun";import{env as envVars}from"@stacksjs/env";import{getConnectionDefaults}from"./defaults";import{isMysqlWire,isVitessSharded,toQueryBuilderDialect}from"./dialect";import{relativeMigrationDirectory,resolveMigrationDirectory,snapshotDirForQueryBuilder}from"./migration-path";import{contextInTransaction,markContextWrote,resolveReplicaConnection,selectReplica,shouldRouteToReplica,withTransactionContext}from"./replicas";import{aggregateFunctions}from"./types";const sqliteDefaults=getConnectionDefaults("sqlite",envVars),mysqlDefaults=getConnectionDefaults("mysql",envVars),postgresDefaults=getConnectionDefaults("postgres",envVars);let appEnv=envVars.APP_ENV||"local",dbDriver=envVars.DB_CONNECTION||"sqlite",dbConfig={connections:{sqlite:{database:sqliteDefaults.database,prefix:""},mysql:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},singlestore:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:mysqlDefaults.port,prefix:""},vitess:{name:mysqlDefaults.database,host:mysqlDefaults.host,username:mysqlDefaults.username,password:mysqlDefaults.password,port:15306,prefix:"",sharded:isVitessSharded()},postgres:{name:postgresDefaults.database,host:postgresDefaults.host,username:postgresDefaults.username,password:postgresDefaults.password,port:postgresDefaults.port,prefix:""}}},dbConfigLockTail=Promise.resolve();const DB_CONFIG_LOCK_MAX_HOLD_MS=60000;export function acquireDbConfigLock(){let release=()=>{};const held=new Promise((resolve)=>{release=resolve}),acquired=dbConfigLockTail.then(()=>{const watchdog=setTimeout(()=>{console.warn(`[database] the db config lock was held for over ${DB_CONFIG_LOCK_MAX_HOLD_MS/1000}s. Releasing it so the queue can proceed - a test file most likely failed before its afterAll ran.`);release()},DB_CONFIG_LOCK_MAX_HOLD_MS);watchdog.unref?.();held.then(()=>clearTimeout(watchdog));return release});dbConfigLockTail=dbConfigLockTail.then(()=>held).catch(()=>{});return acquired}export function initializeDbConfig(config){if(config?.app?.env)appEnv=config.app.env;if(config?.database?.default)dbDriver=config.database.default;if(config?.database?.connections)dbConfig=config.database;updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function getDriver(){return dbDriver}function getDatabaseConfig(){return dbConfig}function getDialect(){return toQueryBuilderDialect(getDriver())}function getDbConfig(){const driver=getDriver(),database=getDatabaseConfig(),env=getEnv();if(driver==="sqlite"){const defaultName=env!=="testing"?"database/stacks.sqlite":"database/stacks_testing.sqlite";return{database:database.connections?.sqlite?.database??defaultName}}if(driver==="mysql")return{database:database.connections?.mysql?.name||"stacks",host:database.connections?.mysql?.host??"127.0.0.1",username:database.connections?.mysql?.username??"root",password:database.connections?.mysql?.password??"",port:database.connections?.mysql?.port??3306};if(driver==="singlestore")return{database:database.connections?.singlestore?.name||"stacks",host:database.connections?.singlestore?.host??"127.0.0.1",username:database.connections?.singlestore?.username??"root",password:database.connections?.singlestore?.password??"",port:database.connections?.singlestore?.port??3306};if(driver==="vitess")return{database:database.connections?.vitess?.name||"stacks",host:database.connections?.vitess?.host??"127.0.0.1",username:database.connections?.vitess?.username??"root",password:database.connections?.vitess?.password??"",port:database.connections?.vitess?.port??15306};if(driver==="postgres"){const dbName=database.connections?.postgres?.name??"stacks";return{database:env==="testing"?`${dbName}_testing`:dbName,host:database.connections?.postgres?.host??"127.0.0.1",username:database.connections?.postgres?.username??"",password:database.connections?.postgres?.password??"",port:database.connections?.postgres?.port??5432}}return{database:":memory:"}}export const QB_SNAPSHOT_DIR="storage/framework/database";export function qbSnapshotDir(){return snapshotDirForQueryBuilder()}export const RAW_QUERY_SOFT_DELETE_CONFIG={enabled:!1,column:"deleted_at",defaultFilter:!0};export function createDatabaseQueryHooks(dispatch){function forward(event){try{Promise.resolve(dispatch(event)).catch(()=>{})}catch{}}return{onQueryEnd:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs}),onQueryError:(event)=>forward({query:{sql:event.sql,parameters:event.params},queryDurationMillis:event.durationMs,error:event.error})}}let queryLoggerModule;function forwardDatabaseQuery(event){queryLoggerModule??=import("./query-logger").catch((error)=>{queryLoggerModule=void 0;throw error});queryLoggerModule.then(({logQuery})=>logQuery(event)).catch(()=>{})}registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}function getReplicas(){const driver=getDriver();if(driver==="sqlite")return[];return getDatabaseConfig().connections?.[driver]?.replicas??[]}function getReadPolicy(){return getDatabaseConfig().reads??{}}function toBunPoolOptions(pool){if(!pool)return{};const options={};if(pool.max!==void 0)options.max=pool.max;if(pool.idleTimeoutMs!==void 0)options.idleTimeout=Math.round(pool.idleTimeoutMs/1000);if(pool.acquireTimeoutMs!==void 0)options.connectionTimeout=Math.round(pool.acquireTimeoutMs/1000);if(pool.maxLifetimeMs!==void 0)options.maxLifetime=Math.round(pool.maxLifetimeMs/1000);return options}function updateQueryBuilderConfig(){const dialect=getDialect(),dbConfigForQb=getDbConfig(),pool=getPoolConfig();setConfig({dialect,vitess:{sharded:isVitessSharded(dbConfig.connections.vitess?.sharded)},database:pool?{...dbConfigForQb,pool}:dbConfigForQb,verbose:getEnv()!=="production",snapshotDir:qbSnapshotDir(),migrationDir:relativeMigrationDirectory(resolveMigrationDirectory(toQueryBuilderDialect(dialect),{snapshotDir:qbSnapshotDir()})),timestamps:{createdAt:"created_at",updatedAt:"updated_at",defaultOrderColumn:"created_at"},softDeletes:RAW_QUERY_SOFT_DELETE_CONFIG})}updateQueryBuilderConfig();let _dbInstance=null,_configInitPromise=null;function ensureConfigLoaded(){if(!_configInitPromise)_configInitPromise=(async()=>{try{const{config,overridesReady}=await import("@stacksjs/config");await overridesReady;if(config){initializeDbConfig(config);_dbInstance=null}}catch{}})();return _configInitPromise}export async function ensureDatabaseConfigLoaded(){await ensureConfigLoaded()}export{applySqlitePragmas,SQLITE_BOOTSTRAP_PRAGMAS}from"@stacksjs/query-builder";const sqliteTxOwner=new AsyncLocalStorage;let sqliteTxTail=Promise.resolve();function serializeSqliteTransaction(run){if(sqliteTxOwner.getStore())return run();const result=sqliteTxTail.then(()=>sqliteTxOwner.run(!0,run));sqliteTxTail=result.then(()=>{return},()=>{return});return result}function applySqliteTransactionSerialization(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>serializeSqliteTransaction(()=>original(...args))}function applyTransactionRoutingContext(instance){const original=instance.transaction.bind(instance);instance.transaction=(...args)=>withTransactionContext(()=>original(...args))}function getDb(){if(!_dbInstance){updateQueryBuilderConfig();_dbInstance=createQueryBuilder();if(getDialect()==="sqlite")applySqliteTransactionSerialization(_dbInstance);applyTransactionRoutingContext(_dbInstance)}return _dbInstance}let _replicaInstances=new Map;export function resetDatabaseConnection(){resetQueryBuilderConnection();_dbInstance=null;_replicaInstances=new Map}function getReplicaDb(replica){const primary=getDbConfig(),resolved=resolveReplicaConnection(replica,primary),key=`${resolved.host}:${resolved.port??""}`,cached=_replicaInstances.get(key);if(cached)return cached;const scheme=isMysqlWire(getDriver())?"mysql":"postgres",auth=resolved.username?`${encodeURIComponent(resolved.username)}:${encodeURIComponent(resolved.password??"")}@`:"",url=`${scheme}://${auth}${resolved.host}:${resolved.port}/${resolved.database}`,sql=new SQL({url,...toBunPoolOptions(getPoolConfig())}),instance=createQueryBuilder({sql});_replicaInstances.set(key,instance);return instance}function getReadDb(){const replicas=getReplicas(),policy=getReadPolicy();if(!shouldRouteToReplica({policy,replicas}))return getDb();const replica=selectReplica(replicas,policy.strategy);return replica?getReplicaDb(replica):getDb()}function getExplicitReadDb(){const replicas=getReplicas();if(!replicas.length||contextInTransaction())return getDb();const replica=selectReplica(replicas,getReadPolicy().strategy);return replica?getReplicaDb(replica):getDb()}const WRITE_ENTRY_POINTS=new Set(["insertInto","updateTable","deleteFrom","create","createMany","insertOrIgnore","insertGetId","updateOrInsert","upsert"]),READ_ENTRY_POINTS=new Set(["selectFrom","selectFromSub","select"]);ensureConfigLoaded();export function asRows(rows){return rows}export function asRow(row){return row}export const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;if(typeof prop==="string"&&WRITE_ENTRY_POINTS.has(prop))markContextWrote();const instance=typeof prop==="string"&&READ_ENTRY_POINTS.has(prop)?getReadDb():getDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}}),readDb=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;const instance=getExplicitReadDb(),value=instance[prop];if(typeof value==="function")return value.bind(instance);return value}});export{setConfig};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/database",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.26",
|
|
6
6
|
"description": "The Stacks database integration.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -60,26 +60,26 @@
|
|
|
60
60
|
"prepublishOnly": "bun run build"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@stacksjs/config": "0.74.
|
|
64
|
-
"@stacksjs/env": "0.74.
|
|
65
|
-
"@stacksjs/error-handling": "0.74.
|
|
66
|
-
"@stacksjs/faker": "^0.74.
|
|
67
|
-
"@stacksjs/features": "0.74.
|
|
68
|
-
"@stacksjs/logging": "0.74.
|
|
69
|
-
"@stacksjs/model-meta": "0.74.
|
|
70
|
-
"@stacksjs/path": "0.74.
|
|
71
|
-
"@stacksjs/query-builder": "^0.74.
|
|
72
|
-
"@stacksjs/security": "0.74.
|
|
73
|
-
"@stacksjs/storage": "0.74.
|
|
74
|
-
"@stacksjs/strings": "0.74.
|
|
63
|
+
"@stacksjs/config": "0.74.26",
|
|
64
|
+
"@stacksjs/env": "0.74.26",
|
|
65
|
+
"@stacksjs/error-handling": "0.74.26",
|
|
66
|
+
"@stacksjs/faker": "^0.74.26",
|
|
67
|
+
"@stacksjs/features": "0.74.26",
|
|
68
|
+
"@stacksjs/logging": "0.74.26",
|
|
69
|
+
"@stacksjs/model-meta": "0.74.26",
|
|
70
|
+
"@stacksjs/path": "0.74.26",
|
|
71
|
+
"@stacksjs/query-builder": "^0.74.26",
|
|
72
|
+
"@stacksjs/security": "0.74.26",
|
|
73
|
+
"@stacksjs/storage": "0.74.26",
|
|
74
|
+
"@stacksjs/strings": "0.74.26",
|
|
75
75
|
"@stacksjs/ts-validation": "^0.5.6",
|
|
76
|
-
"bun-query-builder": "^0.2.
|
|
76
|
+
"bun-query-builder": "^0.2.68",
|
|
77
77
|
"dynamodb-tooling": "^0.3.2"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@stacksjs/cli": "0.74.
|
|
81
|
-
"@stacksjs/router": "0.74.
|
|
82
|
-
"@stacksjs/utils": "0.74.
|
|
80
|
+
"@stacksjs/cli": "0.74.26",
|
|
81
|
+
"@stacksjs/router": "0.74.26",
|
|
82
|
+
"@stacksjs/utils": "0.74.26",
|
|
83
83
|
"better-dx": "^0.2.24"
|
|
84
84
|
}
|
|
85
85
|
}
|