@stacksjs/database 0.75.467 → 0.75.473
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/trait-tables.js +8 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/package.json +16 -16
package/dist/trait-tables.js
CHANGED
|
@@ -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:"chat_messages",column:"attachments",definition:"TEXT"},{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 (
|
|
63
|
+
)`}export function traitTableColumnGuarantees(sql){return[{table:"chat_messages",column:"attachments",definition:"TEXT"},{table:"chat_messages",column:"edited_at",definition:sql.nullableTimestamp},{table:"chat_messages",column:"deleted_at",definition:sql.nullableTimestamp},{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,
|
|
@@ -79,5 +79,12 @@ import{log}from"@stacksjs/logging";import{db}from"./utils";import{resolvedDataba
|
|
|
79
79
|
client_key VARCHAR(100) NOT NULL,
|
|
80
80
|
created_at ${sql.datetime} NOT NULL,
|
|
81
81
|
read_at ${sql.nullableTimestamp},
|
|
82
|
+
-- When the sender last rewrote it, so a reader can be told the words
|
|
83
|
+
-- changed after they were delivered rather than silently shown new ones.
|
|
84
|
+
edited_at ${sql.nullableTimestamp},
|
|
85
|
+
-- An unsent message keeps its row: the conversation's order, its read
|
|
86
|
+
-- state and the retry key all hang off that id. The words go (see
|
|
87
|
+
-- orm/src/messaging.ts unsend), the row stays.
|
|
88
|
+
deleted_at ${sql.nullableTimestamp},
|
|
82
89
|
UNIQUE (conversation_id, sender_id, client_key)
|
|
83
90
|
)`]}export function traitTablePrerequisiteSql(corpus,sql){const statements=sqlStatementsOf(corpus).map((statement)=>statement.replace(/'(?:''|[^'])*'/g,"''")),create=new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${SQL_IDENTIFIER}\\s*\\(`,"i"),references=(statement)=>[...statement.matchAll(new RegExp(`\\bREFERENCES\\s+${SQL_IDENTIFIER}\\s*\\(`,"gi"))].map((match)=>match[1].toLowerCase()),declared=new Set(statements.map((statement)=>create.exec(statement)?.[1]?.toLowerCase()).filter(Boolean)),native=new Map([...messagingTableSql(sql),commentablesTableSql(sql),taggablesTableSql(sql),categorizablesTableSql(sql),commentableUpvotesTableSql(sql),taggableModelsTableSql(sql),categorizableModelsTableSql(sql)].map((statement)=>[create.exec(statement)[1].toLowerCase(),statement])),plan=[],visited=new Set;function visit(table){if(declared.has(table)||visited.has(table)||!native.has(table))return;visited.add(table);const statement=native.get(table);for(const parent of references(statement))visit(parent);plan.push(statement)}for(const statement of statements)for(const table of references(statement))visit(table);return plan}async function reconcileEmptySqlitePivots(sql){if(!sql.isSqlite)return;for(const[table,column]of[["taggable_models","taggable_id"],["categorizable_models","categorizable_id"]]){if((await db.unsafe(`PRAGMA table_info(${table})`).execute()).some((existing)=>existing.name===column))continue;if((await db.unsafe(`SELECT 1 FROM ${table} LIMIT 1`).execute()).length>0)throw Error(`Cannot infer ${table}.${column} for existing rows. Restore the owner IDs from an authoritative backup or repair the pivot explicitly, then rerun migrate.`);try{await db.unsafe(`ALTER TABLE ${table} ADD COLUMN ${column} ${sql.bigInteger} NOT NULL`).execute()}catch(error){if(!isDuplicateColumnError(error))throw error}}}export async function migrateTraitTables(options={}){const dbDriver=resolvedDatabaseDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{for(const statement of messagingTableSql(sql))await db.unsafe(statement).execute();if(options.verbose)log.info("Creating commentables table...");await db.unsafe(commentablesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggables table...");await db.unsafe(taggablesTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizables table...");await db.unsafe(categorizablesTableSql(sql)).execute();if(options.verbose)log.info("Creating commentable_upvotes table...");await db.unsafe(commentableUpvotesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggable_models pivot...");await db.unsafe(taggableModelsTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizable_models pivot...");await db.unsafe(categorizableModelsTableSql(sql)).execute();try{for(const{table,foreignKey}of await likeableTargets()){if(options.verbose)log.info(`Creating ${table} table...`);await db.unsafe(likesTableSql(sql,table,foreignKey)).execute()}}catch(error){log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error?error.message:String(error)}`)}for(const{table,column,definition}of traitTableColumnGuarantees(sql))try{await db.unsafe(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).execute();if(options.verbose)log.info(`Added missing ${table}.${column}`)}catch(error){if(!isDuplicateColumnError(error))throw error}await reconcileEmptySqlitePivots(sql);for(const statement of traitTableIndexSql())try{await db.unsafe(indexSqlForDialect(statement,dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.success("Polymorphic trait tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create polymorphic trait tables: ${message}`);return{success:!1,error:message}}}
|
package/dist/types.d.ts
CHANGED
|
@@ -78,8 +78,8 @@ export declare interface ColumnRef {
|
|
|
78
78
|
* Fluent aggregate-function builder accessible via
|
|
79
79
|
* `eb.fn.count(...)`, `eb.fn.sum(...)`, etc. The chained `.as(name)`
|
|
80
80
|
* names the resulting column in the projection; `.filterWhere(...)`
|
|
81
|
-
* scopes the aggregate to a sub-population
|
|
82
|
-
* status = 'success'
|
|
81
|
+
* scopes the aggregate to a sub-population, rendered as
|
|
82
|
+
* `COUNT(CASE WHEN status = 'success' THEN id END)`.
|
|
83
83
|
*
|
|
84
84
|
* The `sql` text is part of the contract: bun-query-builder's select
|
|
85
85
|
* pipeline renders fragments from their `.sql` property, so every
|
package/dist/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const SAFE_ALIAS=/^[A-Z_][A-Z0-9_]*$/i,SAFE_COLUMN=/^[A-Z_][A-Z0-9_]*(\.[A-Z_][A-Z0-9_]*)?$/i;function assertSqlTextIdentifier(value,kind){if(!(kind==="column"?SAFE_COLUMN:SAFE_ALIAS).test(value))throw TypeError(`[database] refusing to interpolate unsafe ${kind} ${JSON.stringify(value)} into SQL text - expected a plain identifier (letters, digits, underscores)`)}function createSqlFragment(text,parameters){return{sql:text,parameters,as(alias){assertSqlTextIdentifier(alias,"alias");return createSqlFragment(`${text} AS ${alias}`,parameters)},toString(){return text}}}export function sql(strings,...values){const sqlParts=[],parameters=[];for(let i=0;i<strings.length;i++){sqlParts.push(strings[i]);if(i<values.length)if(values[i]&&typeof values[i]==="object"&&"raw"in values[i])sqlParts.push(values[i].raw);else{sqlParts.push("?");parameters.push(values[i])}}return createSqlFragment(sqlParts.join(""),parameters)}sql.raw=function raw(value){return{raw:value}};sql.ref=function ref(column){return{raw:column}};sql.literal=function literal(value){return{raw:inlineSqlLiteral(value)}};const SAFE_FILTER_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like","not like","ilike","not ilike","is","is not"]);function inlineSqlLiteral(value){if(value===null||value===void 0)return"NULL";if(typeof value==="number"){if(!Number.isFinite(value))throw TypeError(`[database] refusing to inline non-finite number into SQL: ${value}`);return String(value)}if(typeof value==="boolean")return value?"1":"0";if(typeof value==="bigint")return value.toString();const text=String(value);if(text.includes("\\"))throw TypeError("[database] refusing to inline a string containing a backslash into SQL: MySQL reads it as an escape. Bind it instead: db.unsafe(sql, params) with sqlHelpers().param(n)");return`'${text.replace(/'/g,"''")}'`}function createAggregateExpression(
|
|
1
|
+
const SAFE_ALIAS=/^[A-Z_][A-Z0-9_]*$/i,SAFE_COLUMN=/^[A-Z_][A-Z0-9_]*(\.[A-Z_][A-Z0-9_]*)?$/i;function assertSqlTextIdentifier(value,kind){if(!(kind==="column"?SAFE_COLUMN:SAFE_ALIAS).test(value))throw TypeError(`[database] refusing to interpolate unsafe ${kind} ${JSON.stringify(value)} into SQL text - expected a plain identifier (letters, digits, underscores)`)}function createSqlFragment(text,parameters){return{sql:text,parameters,as(alias){assertSqlTextIdentifier(alias,"alias");return createSqlFragment(`${text} AS ${alias}`,parameters)},toString(){return text}}}export function sql(strings,...values){const sqlParts=[],parameters=[];for(let i=0;i<strings.length;i++){sqlParts.push(strings[i]);if(i<values.length)if(values[i]&&typeof values[i]==="object"&&"raw"in values[i])sqlParts.push(values[i].raw);else{sqlParts.push("?");parameters.push(values[i])}}return createSqlFragment(sqlParts.join(""),parameters)}sql.raw=function raw(value){return{raw:value}};sql.ref=function ref(column){return{raw:column}};sql.literal=function literal(value){return{raw:inlineSqlLiteral(value)}};const SAFE_FILTER_OPERATORS=new Set(["=","!=","<>","<","<=",">",">=","like","not like","ilike","not ilike","is","is not"]);function inlineSqlLiteral(value){if(value===null||value===void 0)return"NULL";if(typeof value==="number"){if(!Number.isFinite(value))throw TypeError(`[database] refusing to inline non-finite number into SQL: ${value}`);return String(value)}if(typeof value==="boolean")return value?"1":"0";if(typeof value==="bigint")return value.toString();const text=String(value);if(text.includes("\\"))throw TypeError("[database] refusing to inline a string containing a backslash into SQL: MySQL reads it as an escape. Bind it instead: db.unsafe(sql, params) with sqlHelpers().param(n)");return`'${text.replace(/'/g,"''")}'`}function createAggregateExpression(name,column,conditions,alias){const argument=conditions.length===0?column??"*":`CASE WHEN ${conditions.join(" AND ")} THEN ${column??1} END`;return{sql:`${name}(${argument})${alias===void 0?"":` AS ${alias}`}`,as(next){assertSqlTextIdentifier(next,"alias");return createAggregateExpression(name,column,conditions,next)},filterWhere(filterColumn,op,value){assertSqlTextIdentifier(filterColumn,"column");if(!SAFE_FILTER_OPERATORS.has(op.toLowerCase()))throw TypeError(`[database] refusing unsafe aggregate filter operator ${JSON.stringify(op)} - allowed: ${[...SAFE_FILTER_OPERATORS].join(", ")}`);return createAggregateExpression(name,column,[...conditions,`${filterColumn} ${op} ${inlineSqlLiteral(value)}`],alias)}}}function aggregate(name,column){if(column!==void 0)assertSqlTextIdentifier(column,"column");return createAggregateExpression(name,column,[])}export const aggregateFunctions={countAll:()=>aggregate("COUNT"),count:(column)=>aggregate("COUNT",column),sum:(column)=>aggregate("SUM",column),avg:(column)=>aggregate("AVG",column),min:(column)=>aggregate("MIN",column),max:(column)=>aggregate("MAX",column)};
|
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.
|
|
5
|
+
"version": "0.75.473",
|
|
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.
|
|
70
|
-
"@stacksjs/env": "0.75.
|
|
71
|
-
"@stacksjs/error-handling": "0.75.
|
|
72
|
-
"@stacksjs/faker": "0.75.
|
|
73
|
-
"@stacksjs/features": "0.75.
|
|
74
|
-
"@stacksjs/logging": "0.75.
|
|
75
|
-
"@stacksjs/model-meta": "0.75.
|
|
76
|
-
"@stacksjs/path": "0.75.
|
|
77
|
-
"@stacksjs/query-builder": "0.75.
|
|
78
|
-
"@stacksjs/security": "0.75.
|
|
79
|
-
"@stacksjs/storage": "0.75.
|
|
80
|
-
"@stacksjs/strings": "0.75.
|
|
69
|
+
"@stacksjs/config": "0.75.473",
|
|
70
|
+
"@stacksjs/env": "0.75.473",
|
|
71
|
+
"@stacksjs/error-handling": "0.75.473",
|
|
72
|
+
"@stacksjs/faker": "0.75.473",
|
|
73
|
+
"@stacksjs/features": "0.75.473",
|
|
74
|
+
"@stacksjs/logging": "0.75.473",
|
|
75
|
+
"@stacksjs/model-meta": "0.75.473",
|
|
76
|
+
"@stacksjs/path": "0.75.473",
|
|
77
|
+
"@stacksjs/query-builder": "0.75.473",
|
|
78
|
+
"@stacksjs/security": "0.75.473",
|
|
79
|
+
"@stacksjs/storage": "0.75.473",
|
|
80
|
+
"@stacksjs/strings": "0.75.473",
|
|
81
81
|
"@stacksjs/ts-validation": "^0.5.7",
|
|
82
82
|
"bun-query-builder": "^0.3.38",
|
|
83
83
|
"dynamodb-tooling": "^0.3.6"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@stacksjs/cli": "0.75.
|
|
87
|
-
"@stacksjs/router": "0.75.
|
|
88
|
-
"@stacksjs/utils": "0.75.
|
|
86
|
+
"@stacksjs/cli": "0.75.473",
|
|
87
|
+
"@stacksjs/router": "0.75.473",
|
|
88
|
+
"@stacksjs/utils": "0.75.473",
|
|
89
89
|
"better-dx": "^0.2.24"
|
|
90
90
|
}
|
|
91
91
|
}
|