@stacksjs/database 0.70.257 → 0.70.259

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.
Files changed (50) hide show
  1. package/dist/auth-tables.js +18 -137
  2. package/dist/column.js +1 -26
  3. package/dist/custom/audits.js +20 -54
  4. package/dist/custom/errors.js +16 -46
  5. package/dist/custom/index.js +1 -3
  6. package/dist/custom/jobs.js +13 -137
  7. package/dist/database.js +1 -181
  8. package/dist/datetime-columns.js +8 -85
  9. package/dist/ddl-constraints.js +7 -111
  10. package/dist/defaults.js +1 -48
  11. package/dist/dialect.js +1 -79
  12. package/dist/driver-config.js +1 -172
  13. package/dist/drivers/defaults/index.js +1 -1
  14. package/dist/drivers/defaults/traits.js +1 -29
  15. package/dist/drivers/dynamodb.js +1 -607
  16. package/dist/drivers/helpers.js +1 -206
  17. package/dist/drivers/index.js +1 -9
  18. package/dist/drivers/mysql.js +58 -299
  19. package/dist/drivers/postgres.js +78 -368
  20. package/dist/drivers/sqlite.js +61 -379
  21. package/dist/ensure-database.js +1 -145
  22. package/dist/fk-audit.js +3 -187
  23. package/dist/index.js +1 -64
  24. package/dist/managed-columns.js +1 -59
  25. package/dist/migration-dialect.js +4 -107
  26. package/dist/migration-ledger.js +1 -382
  27. package/dist/migration-lock.js +1 -143
  28. package/dist/migrations.js +15 -1118
  29. package/dist/model-sources.js +1 -76
  30. package/dist/notification-tables.js +4 -49
  31. package/dist/query-logger.js +2 -241
  32. package/dist/query-parser.js +1 -93
  33. package/dist/rbac-tables.js +6 -61
  34. package/dist/relation-columns.js +1 -66
  35. package/dist/replicas.js +1 -74
  36. package/dist/safe-migrations.js +2 -52
  37. package/dist/schema.js +1 -10
  38. package/dist/seeder.js +1 -457
  39. package/dist/sql-helpers.js +1 -50
  40. package/dist/table.js +1 -26
  41. package/dist/tools/setup.js +1 -6
  42. package/dist/trait-tables.js +8 -153
  43. package/dist/transaction-context.js +1 -62
  44. package/dist/types.js +1 -98
  45. package/dist/unique-audit.js +3 -155
  46. package/dist/utils.js +1 -285
  47. package/dist/uuid-columns.js +1 -68
  48. package/dist/validators.js +1 -122
  49. package/dist/vschema.js +2 -121
  50. package/package.json +20 -13
@@ -1,31 +1,4 @@
1
- import { log } from "@stacksjs/logging";
2
- import { env as envVars } from "@stacksjs/env";
3
- import { db } from "./utils";
4
- import { sqlHelpers } from "./sql-helpers";
5
- import { dialectCapabilities } from "./dialect";
6
- function getDbDriver() {
7
- return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
8
- }
9
- function createdAt(sql) {
10
- return `created_at ${sql.datetime}`;
11
- }
12
- function updatedAt(sql) {
13
- return `updated_at ${sql.nullableTimestamp}`;
14
- }
15
- export function traitTableNames() {
16
- return [
17
- "commentables",
18
- "taggables",
19
- "categorizables",
20
- "commentable_upvotes",
21
- "taggable_models",
22
- "categorizable_models"
23
- ];
24
- }
25
- export const UNSCOPED_OWNER_ID = 0;
26
- export function commentablesTableSql(sql) {
27
- const { pkColumn, boolTrue } = sql;
28
- return `CREATE TABLE IF NOT EXISTS commentables (
1
+ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{dialectCapabilities}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}function createdAt(sql){return`created_at ${sql.datetime}`}function updatedAt(sql){return`updated_at ${sql.nullableTimestamp}`}export function traitTableNames(){return["commentables","taggables","categorizables","commentable_upvotes","taggable_models","categorizable_models"]}export const UNSCOPED_OWNER_ID=0;export function commentablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS commentables (
29
2
  ${pkColumn},
30
3
  title VARCHAR(255) NOT NULL,
31
4
  body TEXT NOT NULL,
@@ -38,11 +11,7 @@ export function commentablesTableSql(sql) {
38
11
  is_active BOOLEAN NOT NULL DEFAULT ${boolTrue},
39
12
  ${createdAt(sql)},
40
13
  ${updatedAt(sql)}
41
- )`;
42
- }
43
- export function taggablesTableSql(sql) {
44
- const { pkColumn, boolTrue } = sql;
45
- return `CREATE TABLE IF NOT EXISTS taggables (
14
+ )`}export function taggablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS taggables (
46
15
  ${pkColumn},
47
16
  name VARCHAR(255) NOT NULL,
48
17
  slug VARCHAR(255) NOT NULL,
@@ -52,11 +21,7 @@ export function taggablesTableSql(sql) {
52
21
  taggable_type VARCHAR(255) NOT NULL,
53
22
  ${createdAt(sql)},
54
23
  ${updatedAt(sql)}
55
- )`;
56
- }
57
- export function categorizablesTableSql(sql) {
58
- const { pkColumn, boolTrue } = sql;
59
- return `CREATE TABLE IF NOT EXISTS categorizables (
24
+ )`}export function categorizablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS categorizables (
60
25
  ${pkColumn},
61
26
  name VARCHAR(255) NOT NULL,
62
27
  slug VARCHAR(255) NOT NULL,
@@ -66,141 +31,31 @@ export function categorizablesTableSql(sql) {
66
31
  categorizable_type VARCHAR(255) NOT NULL,
67
32
  ${createdAt(sql)},
68
33
  ${updatedAt(sql)}
69
- )`;
70
- }
71
- export function taggableModelsTableSql(sql) {
72
- const { pkColumn } = sql;
73
- return `CREATE TABLE IF NOT EXISTS taggable_models (
34
+ )`}export function taggableModelsTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS taggable_models (
74
35
  ${pkColumn},
75
36
  tag_id INTEGER NOT NULL,
76
37
  taggable_id INTEGER NOT NULL,
77
38
  taggable_type VARCHAR(255) NOT NULL,
78
39
  ${createdAt(sql)},
79
40
  ${updatedAt(sql)}
80
- )`;
81
- }
82
- export function categorizableModelsTableSql(sql) {
83
- const { pkColumn } = sql;
84
- return `CREATE TABLE IF NOT EXISTS categorizable_models (
41
+ )`}export function categorizableModelsTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS categorizable_models (
85
42
  ${pkColumn},
86
43
  category_id INTEGER NOT NULL,
87
44
  categorizable_id INTEGER NOT NULL,
88
45
  categorizable_type VARCHAR(255) NOT NULL,
89
46
  ${createdAt(sql)},
90
47
  ${updatedAt(sql)}
91
- )`;
92
- }
93
- export function likesTableSql(sql, table, foreignKey) {
94
- const { pkColumn } = sql;
95
- return `CREATE TABLE IF NOT EXISTS ${table} (
48
+ )`}export function likesTableSql(sql,table,foreignKey){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS ${table} (
96
49
  ${pkColumn},
97
50
  ${foreignKey} INTEGER NOT NULL,
98
51
  user_id INTEGER NOT NULL,
99
52
  ${createdAt(sql)},
100
53
  ${updatedAt(sql)},
101
54
  UNIQUE (${foreignKey}, user_id)
102
- )`;
103
- }
104
- export function commentableUpvotesTableSql(sql) {
105
- const { pkColumn } = sql;
106
- return `CREATE TABLE IF NOT EXISTS commentable_upvotes (
55
+ )`}export function commentableUpvotesTableSql(sql){const{pkColumn}=sql;return`CREATE TABLE IF NOT EXISTS commentable_upvotes (
107
56
  ${pkColumn},
108
57
  user_id INTEGER,
109
58
  upvoteable_id INTEGER NOT NULL,
110
59
  upvoteable_type VARCHAR(255) NOT NULL,
111
60
  ${createdAt(sql)}
112
- )`;
113
- }
114
- export function traitTableIndexSql() {
115
- return [
116
- "CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)",
117
- "CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)",
118
- "CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)",
119
- "CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)",
120
- "CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)",
121
- "CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)",
122
- "CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_unique ON taggable_models (tag_id, taggable_id, taggable_type)",
123
- "CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"
124
- ];
125
- }
126
- export async function likeableTargets() {
127
- const { path } = await import("@stacksjs/path"), { globSync } = await import("@stacksjs/storage"), { getTableName } = await import("@stacksjs/orm"), { getLikeableForeignKey, getUpvoteTableName } = await import("./drivers/helpers"), modelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 }), targets = new Map;
128
- for (const modelFile of modelFiles) {
129
- let model;
130
- try {
131
- model = (await import(modelFile)).default;
132
- } catch {
133
- continue;
134
- }
135
- if (!model?.traits?.likeable)
136
- continue;
137
- const tableName = await getTableName(model, modelFile);
138
- if (!tableName)
139
- continue;
140
- const table = getUpvoteTableName(model, tableName);
141
- if (!table || !/^[a-z_]\w*$/i.test(table))
142
- continue;
143
- const foreignKey = getLikeableForeignKey(model, tableName);
144
- if (!/^[a-z_]\w*$/i.test(foreignKey))
145
- continue;
146
- targets.set(table, { table, foreignKey });
147
- }
148
- return [...targets.values()];
149
- }
150
- export function indexSqlForDialect(statement, dialect) {
151
- if (dialectCapabilities(dialect).supportsCreateIndexIfNotExists)
152
- return statement;
153
- return statement.replace(/^(CREATE (?:UNIQUE )?INDEX) IF NOT EXISTS /i, "$1 ");
154
- }
155
- function isDuplicateIndexError(error) {
156
- const message = error instanceof Error ? error.message : String(error);
157
- return /duplicate key name|already exists/i.test(message);
158
- }
159
- export async function migrateTraitTables(options = {}) {
160
- const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver);
161
- if (options.verbose)
162
- log.info(`Creating polymorphic trait tables for ${dbDriver}...`);
163
- try {
164
- if (options.verbose)
165
- log.info("Creating commentables table...");
166
- await db.unsafe(commentablesTableSql(sql)).execute();
167
- if (options.verbose)
168
- log.info("Creating taggables table...");
169
- await db.unsafe(taggablesTableSql(sql)).execute();
170
- if (options.verbose)
171
- log.info("Creating categorizables table...");
172
- await db.unsafe(categorizablesTableSql(sql)).execute();
173
- if (options.verbose)
174
- log.info("Creating commentable_upvotes table...");
175
- await db.unsafe(commentableUpvotesTableSql(sql)).execute();
176
- if (options.verbose)
177
- log.info("Creating taggable_models pivot...");
178
- await db.unsafe(taggableModelsTableSql(sql)).execute();
179
- if (options.verbose)
180
- log.info("Creating categorizable_models pivot...");
181
- await db.unsafe(categorizableModelsTableSql(sql)).execute();
182
- try {
183
- for (const { table, foreignKey } of await likeableTargets()) {
184
- if (options.verbose)
185
- log.info(`Creating ${table} table...`);
186
- await db.unsafe(likesTableSql(sql, table, foreignKey)).execute();
187
- }
188
- } catch (error) {
189
- log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error ? error.message : String(error)}`);
190
- }
191
- for (const statement of traitTableIndexSql())
192
- try {
193
- await db.unsafe(indexSqlForDialect(statement, dbDriver)).execute();
194
- } catch (error) {
195
- if (!isDuplicateIndexError(error))
196
- throw error;
197
- }
198
- if (options.verbose)
199
- log.success("Polymorphic trait tables created");
200
- return { success: !0 };
201
- } catch (error) {
202
- const message = error instanceof Error ? error.message : String(error);
203
- log.error(`Failed to create polymorphic trait tables: ${message}`);
204
- return { success: !1, error: message };
205
- }
206
- }
61
+ )`}export function traitTableIndexSql(){return["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_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_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/orm"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(CREATE (?:UNIQUE )?INDEX) IF NOT EXISTS /i,"$1 ")}function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export async function migrateTraitTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{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 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}}}
@@ -1,62 +1 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
- const transactionStorage = new AsyncLocalStorage;
3
- export function isInTransaction() {
4
- return transactionStorage.getStore() !== void 0;
5
- }
6
- export function enqueueAfterCommit(callback) {
7
- const scope = transactionStorage.getStore();
8
- if (!scope)
9
- return !1;
10
- scope.pending.push(callback);
11
- return !0;
12
- }
13
- export async function runInTransactionScope(fn, options = {}) {
14
- const existing = transactionStorage.getStore();
15
- if (existing) {
16
- existing.depth += 1;
17
- try {
18
- return await fn();
19
- } finally {
20
- existing.depth -= 1;
21
- }
22
- }
23
- const scope = {
24
- pending: [],
25
- depth: 1,
26
- onError: options.onError
27
- };
28
- let result;
29
- try {
30
- result = await transactionStorage.run(scope, fn);
31
- } catch (err) {
32
- scope.pending.length = 0;
33
- throw err;
34
- }
35
- await flushScope(scope);
36
- return result;
37
- }
38
- async function flushScope(scope) {
39
- for (let i = 0;i < scope.pending.length; i++)
40
- try {
41
- await scope.pending[i]();
42
- } catch (err) {
43
- if (scope.onError)
44
- try {
45
- scope.onError(err, i);
46
- } catch {}
47
- else
48
- console.error("[transaction-context] after-commit callback threw:", err);
49
- }
50
- scope.pending.length = 0;
51
- }
52
- export async function __flushAfterCommitNow() {
53
- const scope = transactionStorage.getStore();
54
- if (!scope)
55
- return 0;
56
- const count = scope.pending.length;
57
- await flushScope(scope);
58
- return count;
59
- }
60
- export function __pendingAfterCommitCount() {
61
- return transactionStorage.getStore()?.pending.length ?? 0;
62
- }
1
+ import{AsyncLocalStorage}from"node:async_hooks";const transactionStorage=new AsyncLocalStorage;export function isInTransaction(){return transactionStorage.getStore()!==void 0}export function enqueueAfterCommit(callback){const scope=transactionStorage.getStore();if(!scope)return!1;scope.pending.push(callback);return!0}export async function runInTransactionScope(fn,options={}){const existing=transactionStorage.getStore();if(existing){existing.depth+=1;try{return await fn()}finally{existing.depth-=1}}const scope={pending:[],depth:1,onError:options.onError};let result;try{result=await transactionStorage.run(scope,fn)}catch(err){scope.pending.length=0;throw err}await flushScope(scope);return result}async function flushScope(scope){for(let i=0;i<scope.pending.length;i++)try{await scope.pending[i]()}catch(err){if(scope.onError)try{scope.onError(err,i)}catch{}else console.error("[transaction-context] after-commit callback threw:",err)}scope.pending.length=0}export async function __flushAfterCommitNow(){const scope=transactionStorage.getStore();if(!scope)return 0;const count=scope.pending.length;await flushScope(scope);return count}export function __pendingAfterCommitCount(){return transactionStorage.getStore()?.pending.length??0}
package/dist/types.js CHANGED
@@ -1,98 +1 @@
1
- const SAFE_ALIAS = /^[A-Z_][A-Z0-9_]*$/i, SAFE_COLUMN = /^[A-Z_][A-Z0-9_]*(\.[A-Z_][A-Z0-9_]*)?$/i;
2
- function assertSqlTextIdentifier(value, kind) {
3
- if (!(kind === "column" ? SAFE_COLUMN : SAFE_ALIAS).test(value))
4
- throw TypeError(`[database] refusing to interpolate unsafe ${kind} ${JSON.stringify(value)} into SQL text - expected a plain identifier (letters, digits, underscores)`);
5
- }
6
- function createSqlFragment(text, parameters) {
7
- return {
8
- sql: text,
9
- parameters,
10
- as(alias) {
11
- assertSqlTextIdentifier(alias, "alias");
12
- return createSqlFragment(`${text} AS ${alias}`, parameters);
13
- },
14
- toString() {
15
- return text;
16
- }
17
- };
18
- }
19
- export function sql(strings, ...values) {
20
- const sqlParts = [], parameters = [];
21
- for (let i = 0;i < strings.length; i++) {
22
- sqlParts.push(strings[i]);
23
- if (i < values.length)
24
- if (values[i] && typeof values[i] === "object" && "raw" in values[i])
25
- sqlParts.push(values[i].raw);
26
- else {
27
- sqlParts.push("?");
28
- parameters.push(values[i]);
29
- }
30
- }
31
- return createSqlFragment(sqlParts.join(""), parameters);
32
- }
33
- sql.raw = function raw(value) {
34
- return { raw: value };
35
- };
36
- sql.ref = function ref(column) {
37
- return { raw: column };
38
- };
39
- sql.literal = function literal(value) {
40
- return { raw: inlineSqlLiteral(value) };
41
- };
42
- const SAFE_FILTER_OPERATORS = new Set([
43
- "=",
44
- "!=",
45
- "<>",
46
- "<",
47
- "<=",
48
- ">",
49
- ">=",
50
- "like",
51
- "not like",
52
- "ilike",
53
- "not ilike",
54
- "is",
55
- "is not"
56
- ]);
57
- function inlineSqlLiteral(value) {
58
- if (value === null || value === void 0)
59
- return "NULL";
60
- if (typeof value === "number") {
61
- if (!Number.isFinite(value))
62
- throw TypeError(`[database] refusing to inline non-finite number into SQL: ${value}`);
63
- return String(value);
64
- }
65
- if (typeof value === "boolean")
66
- return value ? "1" : "0";
67
- if (typeof value === "bigint")
68
- return value.toString();
69
- return `'${String(value).replace(/'/g, "''")}'`;
70
- }
71
- function createAggregateExpression(text) {
72
- return {
73
- sql: text,
74
- as(alias) {
75
- assertSqlTextIdentifier(alias, "alias");
76
- return createAggregateExpression(`${text} AS ${alias}`);
77
- },
78
- filterWhere(column, op, value) {
79
- assertSqlTextIdentifier(column, "column");
80
- if (!SAFE_FILTER_OPERATORS.has(op.toLowerCase()))
81
- throw TypeError(`[database] refusing unsafe aggregate filter operator ${JSON.stringify(op)} - allowed: ${[...SAFE_FILTER_OPERATORS].join(", ")}`);
82
- return createAggregateExpression(`${text} FILTER (WHERE ${column} ${op} ${inlineSqlLiteral(value)})`);
83
- }
84
- };
85
- }
86
- function aggregate(name, column) {
87
- if (column !== void 0)
88
- assertSqlTextIdentifier(column, "column");
89
- return createAggregateExpression(column === void 0 ? `${name}(*)` : `${name}(${column})`);
90
- }
91
- export const aggregateFunctions = {
92
- countAll: () => aggregate("COUNT"),
93
- count: (column) => aggregate("COUNT", column),
94
- sum: (column) => aggregate("SUM", column),
95
- avg: (column) => aggregate("AVG", column),
96
- min: (column) => aggregate("MIN", column),
97
- max: (column) => aggregate("MAX", column)
98
- };
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();return`'${String(value).replace(/'/g,"''")}'`}function createAggregateExpression(text){return{sql:text,as(alias){assertSqlTextIdentifier(alias,"alias");return createAggregateExpression(`${text} AS ${alias}`)},filterWhere(column,op,value){assertSqlTextIdentifier(column,"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(`${text} FILTER (WHERE ${column} ${op} ${inlineSqlLiteral(value)})`)}}}function aggregate(name,column){if(column!==void 0)assertSqlTextIdentifier(column,"column");return createAggregateExpression(column===void 0?`${name}(*)`:`${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)};
@@ -1,142 +1,10 @@
1
- import { path } from "@stacksjs/path";
2
- import { plural, snakeCase } from "@stacksjs/strings";
3
- import { safeGlob } from "./fk-audit";
4
- export async function getDeclaredUniques() {
5
- const modelFiles = [
6
- ...safeGlob(path.userModelsPath("*.ts")),
7
- ...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))
8
- ], declared = [];
9
- for (const modelFile of modelFiles) {
10
- let model;
11
- try {
12
- model = (await import(modelFile)).default;
13
- } catch {
14
- continue;
15
- }
16
- if (!model || typeof model !== "object")
17
- continue;
18
- const table = model.table || plural(snakeCase(model.name || "")), modelName = model.name || "", attributes = model.attributes;
19
- if (attributes && typeof attributes === "object") {
20
- for (const [field, attr] of Object.entries(attributes))
21
- if (attr && typeof attr === "object" && attr.unique === !0)
22
- declared.push({
23
- table,
24
- columns: [snakeCase(field)],
25
- model: modelName,
26
- source: "attribute"
27
- });
28
- }
29
- const indexes = model.indexes;
30
- if (Array.isArray(indexes)) {
31
- for (const index of indexes)
32
- if (index && index.unique === !0 && Array.isArray(index.columns) && index.columns.length > 0)
33
- declared.push({
34
- table,
35
- columns: index.columns.map((c) => snakeCase(c)),
36
- model: modelName,
37
- source: "index",
38
- indexName: index.name
39
- });
40
- }
41
- }
42
- return declared;
43
- }
44
- export async function getLiveUniqueIndexes(dialect) {
45
- const { db } = await import("./utils"), d = dialect ?? await currentDialect();
46
- if (d === "sqlite")
47
- return getSqliteLiveUniques(db);
48
- if (d === "mysql")
49
- return getMysqlLiveUniques(db);
50
- if (d === "postgres")
51
- return getPostgresLiveUniques(db);
52
- return [];
53
- }
54
- export async function auditUniqueIndexes(dialect) {
55
- const d = dialect ?? await currentDialect();
56
- if (d !== "sqlite" && d !== "mysql" && d !== "postgres")
57
- return { supported: !1, declared: [], live: [], missing: [], skippedTables: [] };
58
- const declared = await getDeclaredUniques(), live = await getLiveUniqueIndexes(d), liveTables = await getLiveTables(d), liveByTable = new Map;
59
- for (const idx of live) {
60
- const t = idx.table.toLowerCase(), key = columnSetKey(idx.columns);
61
- if (!liveByTable.has(t))
62
- liveByTable.set(t, new Set);
63
- liveByTable.get(t).add(key);
64
- }
65
- const missing = [], skippedTables = new Set;
66
- for (const decl of declared) {
67
- const t = decl.table.toLowerCase();
68
- if (!liveTables.has(t)) {
69
- skippedTables.add(decl.table);
70
- continue;
71
- }
72
- const key = columnSetKey(decl.columns);
73
- if (!(liveByTable.get(t)?.has(key) ?? !1))
74
- missing.push(decl);
75
- }
76
- return { supported: !0, declared, live, missing, skippedTables: [...skippedTables] };
77
- }
78
- async function getLiveTables(dialect) {
79
- const { db } = await import("./utils");
80
- let rows = [];
81
- if (dialect === "sqlite") {
82
- const r = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();
83
- rows = (Array.isArray(r) ? r : []).map((x) => x.name);
84
- } else if (dialect === "mysql") {
85
- const r = await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();
86
- rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.TABLE_NAME);
87
- } else if (dialect === "postgres") {
88
- const r = await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();
89
- rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.tablename);
90
- }
91
- return new Set(rows.filter((n) => typeof n === "string" && n.length > 0).map((n) => n.toLowerCase()));
92
- }
93
- function columnSetKey(columns) {
94
- return [...columns].map((c) => c.toLowerCase()).sort().join(",");
95
- }
96
- async function currentDialect() {
97
- const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
98
- if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
99
- return driver;
100
- return "other";
101
- }
102
- async function getSqliteLiveUniques(db) {
103
- const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(), rows = Array.isArray(tables) ? tables : [], out = [];
104
- for (const row of rows) {
105
- const table = row.name;
106
- if (!table)
107
- continue;
108
- if (!/^[a-z_]\w*$/i.test(table))
109
- continue;
110
- const indexRows = await db.unsafe(`PRAGMA index_list("${table}")`).execute();
111
- for (const idx of Array.isArray(indexRows) ? indexRows : []) {
112
- const r = idx;
113
- if (Number(r.unique) !== 1 || !r.name)
114
- continue;
115
- if (!/^[a-z_]\w*$/i.test(r.name))
116
- continue;
117
- const colRows = await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(), columns = (Array.isArray(colRows) ? colRows : []).map((c) => String(c.name ?? "")).filter((c) => c.length > 0);
118
- if (columns.length > 0)
119
- out.push({ table, name: r.name, columns });
120
- }
121
- }
122
- return out;
123
- }
124
- async function getMysqlLiveUniques(db) {
125
- const rows = await db.unsafe(`
1
+ import{path}from"@stacksjs/path";import{plural,snakeCase}from"@stacksjs/strings";import{safeGlob}from"./fk-audit";export async function getDeclaredUniques(){const modelFiles=[...safeGlob(path.userModelsPath("*.ts")),...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))],declared=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model||typeof model!=="object")continue;const table=model.table||plural(snakeCase(model.name||"")),modelName=model.name||"",attributes=model.attributes;if(attributes&&typeof attributes==="object"){for(const[field,attr]of Object.entries(attributes))if(attr&&typeof attr==="object"&&attr.unique===!0)declared.push({table,columns:[snakeCase(field)],model:modelName,source:"attribute"})}const indexes=model.indexes;if(Array.isArray(indexes)){for(const index of indexes)if(index&&index.unique===!0&&Array.isArray(index.columns)&&index.columns.length>0)declared.push({table,columns:index.columns.map((c)=>snakeCase(c)),model:modelName,source:"index",indexName:index.name})}}return declared}export async function getLiveUniqueIndexes(dialect){const{db}=await import("./utils"),d=dialect??await currentDialect();if(d==="sqlite")return getSqliteLiveUniques(db);if(d==="mysql")return getMysqlLiveUniques(db);if(d==="postgres")return getPostgresLiveUniques(db);return[]}export async function auditUniqueIndexes(dialect){const d=dialect??await currentDialect();if(d!=="sqlite"&&d!=="mysql"&&d!=="postgres")return{supported:!1,declared:[],live:[],missing:[],skippedTables:[]};const declared=await getDeclaredUniques(),live=await getLiveUniqueIndexes(d),liveTables=await getLiveTables(d),liveByTable=new Map;for(const idx of live){const t=idx.table.toLowerCase(),key=columnSetKey(idx.columns);if(!liveByTable.has(t))liveByTable.set(t,new Set);liveByTable.get(t).add(key)}const missing=[],skippedTables=new Set;for(const decl of declared){const t=decl.table.toLowerCase();if(!liveTables.has(t)){skippedTables.add(decl.table);continue}const key=columnSetKey(decl.columns);if(!(liveByTable.get(t)?.has(key)??!1))missing.push(decl)}return{supported:!0,declared,live,missing,skippedTables:[...skippedTables]}}async function getLiveTables(dialect){const{db}=await import("./utils");let rows=[];if(dialect==="sqlite"){const r=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name)}else if(dialect==="mysql"){const r=await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.TABLE_NAME)}else if(dialect==="postgres"){const r=await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();rows=(Array.isArray(r)?r:[]).map((x)=>x.name??x.tablename)}return new Set(rows.filter((n)=>typeof n==="string"&&n.length>0).map((n)=>n.toLowerCase()))}function columnSetKey(columns){return[...columns].map((c)=>c.toLowerCase()).sort().join(",")}async function currentDialect(){const driver=((await import("@stacksjs/env")).env?.DB_CONNECTION??"sqlite").toLowerCase();if(driver==="sqlite"||driver==="mysql"||driver==="postgres")return driver;return"other"}async function getSqliteLiveUniques(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(),rows=Array.isArray(tables)?tables:[],out=[];for(const row of rows){const table=row.name;if(!table)continue;if(!/^[a-z_]\w*$/i.test(table))continue;const indexRows=await db.unsafe(`PRAGMA index_list("${table}")`).execute();for(const idx of Array.isArray(indexRows)?indexRows:[]){const r=idx;if(Number(r.unique)!==1||!r.name)continue;if(!/^[a-z_]\w*$/i.test(r.name))continue;const colRows=await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(),columns=(Array.isArray(colRows)?colRows:[]).map((c)=>String(c.name??"")).filter((c)=>c.length>0);if(columns.length>0)out.push({table,name:r.name,columns})}}return out}async function getMysqlLiveUniques(db){const rows=await db.unsafe(`
126
2
  SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
127
3
  FROM information_schema.STATISTICS
128
4
  WHERE TABLE_SCHEMA = DATABASE()
129
5
  AND NON_UNIQUE = 0
130
6
  ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
131
- `).execute();
132
- return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
133
- table: String(r.TABLE_NAME ?? r.table_name ?? ""),
134
- name: String(r.INDEX_NAME ?? r.index_name ?? ""),
135
- column: String(r.COLUMN_NAME ?? r.column_name ?? "")
136
- }));
137
- }
138
- async function getPostgresLiveUniques(db) {
139
- const rows = await db.unsafe(`
7
+ `).execute();return groupIndexRows(Array.isArray(rows)?rows:[],(r)=>({table:String(r.TABLE_NAME??r.table_name??""),name:String(r.INDEX_NAME??r.index_name??""),column:String(r.COLUMN_NAME??r.column_name??"")}))}async function getPostgresLiveUniques(db){const rows=await db.unsafe(`
140
8
  SELECT
141
9
  t.relname AS table_name,
142
10
  ix.relname AS index_name,
@@ -151,24 +19,4 @@ async function getPostgresLiveUniques(db) {
151
19
  WHERE i.indisunique = true
152
20
  AND n.nspname = 'public'
153
21
  ORDER BY table_name, index_name, seq_in_index
154
- `).execute();
155
- return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
156
- table: String(r.table_name ?? ""),
157
- name: String(r.index_name ?? ""),
158
- column: String(r.column_name ?? "")
159
- }));
160
- }
161
- function groupIndexRows(rows, pick) {
162
- const map = new Map;
163
- for (const row of rows) {
164
- const { table, name, column } = pick(row);
165
- if (!table || !name || !column)
166
- continue;
167
- const key = `${table} ${name}`, existing = map.get(key);
168
- if (existing)
169
- existing.columns.push(column);
170
- else
171
- map.set(key, { table, name, columns: [column] });
172
- }
173
- return [...map.values()];
174
- }
22
+ `).execute();return groupIndexRows(Array.isArray(rows)?rows:[],(r)=>({table:String(r.table_name??""),name:String(r.index_name??""),column:String(r.column_name??"")}))}function groupIndexRows(rows,pick){const map=new Map;for(const row of rows){const{table,name,column}=pick(row);if(!table||!name||!column)continue;const key=`${table} ${name}`,existing=map.get(key);if(existing)existing.columns.push(column);else map.set(key,{table,name,columns:[column]})}return[...map.values()]}