@stacksjs/database 0.74.26 → 0.74.27

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,2 +1,2 @@
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)}}
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,QUERY_LOG_BATCH_SIZE=100,QUERY_LOG_BATCH_DELAY_MS=5,pendingQueryLogs=[];let queryLogFlushScheduled=!1,queryLogFlushInFlight=!1;function enqueueQueryLog(record){const settled=new Promise((resolve)=>{pendingQueryLogs.push({record,resolve})});if(pendingQueryLogs.length>=QUERY_LOG_BATCH_SIZE)flushQueuedQueryLogs();else if(!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}return settled}async function flushQueuedQueryLogs(){if(queryLogFlushInFlight)return;queryLogFlushInFlight=!0;try{while(pendingQueryLogs.length>0){const batch=pendingQueryLogs.splice(0,QUERY_LOG_BATCH_SIZE),recordsByShape=Map.groupBy(batch.map((item)=>item.record),(record)=>Object.keys(record).join("\x00"));for(const records of recordsByShape.values())if(!await queryLogContext.run(!0,()=>storeQueryLogs(records,records.length===1)))for(const record of records)await queryLogContext.run(!0,()=>storeQueryLogs([record]));for(const item of batch)item.resolve()}}finally{queryLogFlushInFlight=!1;if(pendingQueryLogs.length>0&&!queryLogFlushScheduled){queryLogFlushScheduled=!0;setTimeout(()=>{queryLogFlushScheduled=!1;flushQueuedQueryLogs()},QUERY_LOG_BATCH_DELAY_MS)}}}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 enqueueQueryLog(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,traceInfo=status==="completed"&&!config.database?.queryLogging?.captureAllTraces?void 0:extractTraceInfo();return{query,normalized_query:normalizedQuery,duration:durationMs,connection,status,error:error?String(error):void 0,executed_at:sqlDateTime(),bindings,trace:traceInfo?.trace,...traceInfo?.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 storeQueryLogs(logRecords,reportFailure=!0){try{const values=logRecords;if(values.length===1)await db.insertInto("query_logs").values(values).execute();else await db.transaction(async(rawTrx)=>{await rawTrx.insertInto("query_logs").values(values).execute()});return!0}catch(error){if(reportFailure){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)}return!1}}
package/dist/utils.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare function initializeDbConfig(config: DbConfigSource | null | undef
13
13
  */
14
14
  export declare function qbSnapshotDir(): string;
15
15
  export declare function createDatabaseQueryHooks(dispatch: (event: DatabaseQueryLogEvent) => void | Promise<void>): QueryHooks;
16
+ /** Establish read-routing state only when this connection can use a replica. */
17
+ export declare function withDatabaseRoutingContext<T>(fn: () => T): T;
16
18
  export declare function ensureDatabaseConfigLoaded(): Promise<void>;
17
19
  /**
18
20
  * Discard every cached database client after the underlying query-builder
@@ -141,6 +143,7 @@ export declare interface DbConfigSource {
141
143
  default?: string
142
144
  connections?: Partial<DbConfig['connections']>
143
145
  reads?: DbConfig['reads']
146
+ queryLogging?: { enabled?: boolean }
144
147
  }
145
148
  }
146
149
  export declare interface DatabaseQueryLogEvent {
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})}}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};
1
+ import{AsyncLocalStorage}from"node:async_hooks";import{config as queryBuilderConfig,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,withRoutingContext,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",queryLoggingEnabled=envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv),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;queryLoggingEnabled=config?.database?.queryLogging?.enabled??envVars.DB_QUERY_LOGGING_ENABLED??!isProductionEnvironment(appEnv);syncDatabaseQueryHooks();updateQueryBuilderConfig();_dbInstance=null;_replicaInstances=new Map}function getEnv(){return appEnv}function isProductionEnvironment(value){return value==="production"||value==="prod"}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(()=>{})}let unregisterDatabaseQueryHooks;function syncDatabaseQueryHooks(){const shouldInstall=!isProductionEnvironment(appEnv)||queryLoggingEnabled;if(shouldInstall&&!unregisterDatabaseQueryHooks)unregisterDatabaseQueryHooks=registerPersistentQueryHooks(createDatabaseQueryHooks(forwardDatabaseQuery));else if(!shouldInstall&&unregisterDatabaseQueryHooks){unregisterDatabaseQueryHooks();unregisterDatabaseQueryHooks=void 0}}syncDatabaseQueryHooks();function getPoolConfig(){const driver=getDriver();if(driver==="sqlite")return;return getDatabaseConfig().connections?.[driver]?.pool}const EMPTY_REPLICAS=[];function getReplicas(){const driver=getDriver();if(driver==="sqlite")return EMPTY_REPLICAS;return getDatabaseConfig().connections?.[driver]?.replicas??EMPTY_REPLICAS}function getReadPolicy(){return getDatabaseConfig().reads??{}}export function withDatabaseRoutingContext(fn){return getReplicas().length===0?fn():withRoutingContext(fn)}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}const SIMPLE_SQLITE_TABLE=/^[A-Z_][A-Z0-9_]*$/i,SIMPLE_SQLITE_COLUMN=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_SELECTION=/^[A-Z_][A-Z0-9_]*(?:\.[A-Z_][A-Z0-9_]*)?(?:\s+AS\s+[A-Z_][A-Z0-9_]*)?$/i,SIMPLE_SQLITE_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like"]);function hasActiveQueryBuilderHooks(){return Boolean(queryBuilderConfig.hooks&&Object.values(queryBuilderConfig.hooks).some((value)=>value!==void 0))}function createDeferredSqliteSelect(instance,table){let columns,predicateColumn,predicateOperator,predicateValue,additionalPredicates,rowLimit,materialized;const materialize=()=>{if(materialized)return materialized;let builder=instance.selectFrom(table);if(columns)builder=builder.select.call(builder,columns);if(predicateColumn!==void 0){const apply=builder.where;builder=apply.call(builder,predicateColumn,predicateOperator,predicateValue);if(additionalPredicates)for(const predicate of additionalPredicates)builder=apply.call(builder,predicate.column,predicate.operator,predicate.value)}if(rowLimit!==void 0)builder=builder.limit.call(builder,rowLimit);materialized=builder;return builder};let proxy;proxy=new Proxy({select(value){const selected=Array.isArray(value)?value:[value];if(selected.length===0||!selected.every((column)=>typeof column==="string"&&(column==="*"||SIMPLE_SQLITE_SELECTION.test(column)))){const builder=materialize();return builder.select.call(builder,value)}columns=selected;return proxy},where(column,operator,value){if(typeof column!=="string"||!SIMPLE_SQLITE_COLUMN.test(column)||typeof operator!=="string"||!SIMPLE_SQLITE_OPERATORS.has(operator.toLowerCase())){const builder=materialize();return builder.where.call(builder,column,operator,value)}if(predicateColumn===void 0){predicateColumn=column;predicateOperator=operator;predicateValue=value}else(additionalPredicates??=[]).push({column,operator,value});return proxy},limit(value){if(typeof value!=="number"||!Number.isFinite(value)||value<0||!Number.isInteger(value)){const builder=materialize();return builder.limit.call(builder,value)}rowLimit=value;return proxy},execute(){let query=`SELECT ${columns?.join(", ")??"*"} FROM ${table}`;const params=[];if(predicateColumn!==void 0){query+=` WHERE ${predicateColumn} ${predicateOperator} ?`;params.push(predicateValue);if(additionalPredicates)query+=` AND ${additionalPredicates.map((predicate)=>{params.push(predicate.value);return`${predicate.column} ${predicate.operator} ?`}).join(" AND ")}`}if(rowLimit!==void 0)query+=` LIMIT ${rowLimit}`;return instance.unsafe(query,params).execute()}},{get(target,property){const value=target[property];if(value!==void 0)return value;const builder=materialize(),fallback=builder[property];return typeof fallback==="function"?fallback.bind(builder):fallback}});return proxy}function selectFromDatabase(table){const dialect=getDialect(),instance=dialect==="sqlite"?getDb():getReadDb();if(dialect==="sqlite"&&!queryBuilderConfig.softDeletes?.enabled&&!hasActiveQueryBuilderHooks()&&SIMPLE_SQLITE_TABLE.test(table))return createDeferredSqliteSelect(instance,table);return instance.selectFrom(table)}export const db=new Proxy({},{get(_target,prop){if(prop==="fn")return aggregateFunctions;if(prop==="read")return readDb;if(prop==="selectFrom")return selectFromDatabase;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.26",
5
+ "version": "0.74.27",
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.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",
63
+ "@stacksjs/config": "0.74.27",
64
+ "@stacksjs/env": "0.74.27",
65
+ "@stacksjs/error-handling": "0.74.27",
66
+ "@stacksjs/faker": "^0.74.27",
67
+ "@stacksjs/features": "0.74.27",
68
+ "@stacksjs/logging": "0.74.27",
69
+ "@stacksjs/model-meta": "0.74.27",
70
+ "@stacksjs/path": "0.74.27",
71
+ "@stacksjs/query-builder": "^0.74.27",
72
+ "@stacksjs/security": "0.74.27",
73
+ "@stacksjs/storage": "0.74.27",
74
+ "@stacksjs/strings": "0.74.27",
75
75
  "@stacksjs/ts-validation": "^0.5.6",
76
76
  "bun-query-builder": "^0.2.68",
77
77
  "dynamodb-tooling": "^0.3.2"
78
78
  },
79
79
  "devDependencies": {
80
- "@stacksjs/cli": "0.74.26",
81
- "@stacksjs/router": "0.74.26",
82
- "@stacksjs/utils": "0.74.26",
80
+ "@stacksjs/cli": "0.74.27",
81
+ "@stacksjs/router": "0.74.27",
82
+ "@stacksjs/utils": "0.74.27",
83
83
  "better-dx": "^0.2.24"
84
84
  }
85
85
  }