@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.
- package/dist/auth-tables.js +18 -137
- package/dist/column.js +1 -26
- package/dist/custom/audits.js +20 -54
- package/dist/custom/errors.js +16 -46
- package/dist/custom/index.js +1 -3
- package/dist/custom/jobs.js +13 -137
- package/dist/database.js +1 -181
- package/dist/datetime-columns.js +8 -85
- package/dist/ddl-constraints.js +7 -111
- package/dist/defaults.js +1 -48
- package/dist/dialect.js +1 -79
- package/dist/driver-config.js +1 -172
- package/dist/drivers/defaults/index.js +1 -1
- package/dist/drivers/defaults/traits.js +1 -29
- package/dist/drivers/dynamodb.js +1 -607
- package/dist/drivers/helpers.js +1 -206
- package/dist/drivers/index.js +1 -9
- package/dist/drivers/mysql.js +58 -299
- package/dist/drivers/postgres.js +78 -368
- package/dist/drivers/sqlite.js +61 -379
- package/dist/ensure-database.js +1 -145
- package/dist/fk-audit.js +3 -187
- package/dist/index.js +1 -64
- package/dist/managed-columns.js +1 -59
- package/dist/migration-dialect.js +4 -107
- package/dist/migration-ledger.js +1 -382
- package/dist/migration-lock.js +1 -143
- package/dist/migrations.js +15 -1118
- package/dist/model-sources.js +1 -76
- package/dist/notification-tables.js +4 -49
- package/dist/query-logger.js +2 -241
- package/dist/query-parser.js +1 -93
- package/dist/rbac-tables.js +6 -61
- package/dist/relation-columns.js +1 -66
- package/dist/replicas.js +1 -74
- package/dist/safe-migrations.js +2 -52
- package/dist/schema.js +1 -10
- package/dist/seeder.js +1 -457
- package/dist/sql-helpers.js +1 -50
- package/dist/table.js +1 -26
- package/dist/tools/setup.js +1 -6
- package/dist/trait-tables.js +8 -153
- package/dist/transaction-context.js +1 -62
- package/dist/types.js +1 -98
- package/dist/unique-audit.js +3 -155
- package/dist/utils.js +1 -285
- package/dist/uuid-columns.js +1 -68
- package/dist/validators.js +1 -122
- package/dist/vschema.js +2 -121
- package/package.json +20 -13
package/dist/model-sources.js
CHANGED
|
@@ -1,76 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
import { path } from "@stacksjs/path";
|
|
4
|
-
function collectModels(root, origin) {
|
|
5
|
-
if (!existsSync(root))
|
|
6
|
-
return [];
|
|
7
|
-
const out = [], walk = (dir) => {
|
|
8
|
-
let entries;
|
|
9
|
-
try {
|
|
10
|
-
entries = readdirSync(dir, { withFileTypes: !0 });
|
|
11
|
-
} catch {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
const full = join(dir, entry.name);
|
|
16
|
-
if (entry.isDirectory()) {
|
|
17
|
-
walk(full);
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (!entry.name.endsWith(".ts"))
|
|
21
|
-
continue;
|
|
22
|
-
if (entry.name.startsWith(".") || entry.name.startsWith("index"))
|
|
23
|
-
continue;
|
|
24
|
-
out.push({ file: full, name: entry.name.replace(/\.ts$/, ""), origin });
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
walk(root);
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
export function modelStagingDir() {
|
|
31
|
-
return path.frameworkRuntimePath("model-sources");
|
|
32
|
-
}
|
|
33
|
-
function stage(models) {
|
|
34
|
-
const dir = modelStagingDir();
|
|
35
|
-
try {
|
|
36
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
37
|
-
} catch {}
|
|
38
|
-
mkdirSync(dir, { recursive: !0 });
|
|
39
|
-
for (const model of models) {
|
|
40
|
-
const target = join(dir, `${model.name}.ts`);
|
|
41
|
-
try {
|
|
42
|
-
symlinkSync(model.file, target);
|
|
43
|
-
} catch {
|
|
44
|
-
try {
|
|
45
|
-
writeFileSync(target, readFileSync(model.file));
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return dir;
|
|
50
|
-
}
|
|
51
|
-
export function resolveModelSources(options = {}) {
|
|
52
|
-
const userRoot = options.userRoot ?? path.userModelsPath(), frameworkRoot = options.frameworkRoot ?? path.frameworkPath("defaults/app/Models"), user = collectModels(userRoot, "user"), framework = collectModels(frameworkRoot, "framework");
|
|
53
|
-
if (user.length === 0 && framework.length === 0)
|
|
54
|
-
return null;
|
|
55
|
-
const byName = new Map;
|
|
56
|
-
for (const model of framework)
|
|
57
|
-
byName.set(model.name, model);
|
|
58
|
-
for (const model of user)
|
|
59
|
-
byName.set(model.name, model);
|
|
60
|
-
const models = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)), roots = [];
|
|
61
|
-
if (user.length > 0)
|
|
62
|
-
roots.push(userRoot);
|
|
63
|
-
if (framework.length > 0)
|
|
64
|
-
roots.push(frameworkRoot);
|
|
65
|
-
const onlyUser = framework.length === 0, allFlat = models.every((m) => basename(join(m.file, "..")) === basename(onlyUser ? userRoot : frameworkRoot));
|
|
66
|
-
if (roots.length === 1 && allFlat)
|
|
67
|
-
return { dir: roots[0], models, roots, staged: !1 };
|
|
68
|
-
return { dir: stage(models), models, roots, staged: !0 };
|
|
69
|
-
}
|
|
70
|
-
export function cleanupModelStaging() {
|
|
71
|
-
const dir = modelStagingDir();
|
|
72
|
-
try {
|
|
73
|
-
if (existsSync(dir) && lstatSync(dir).isDirectory())
|
|
74
|
-
rmSync(dir, { recursive: !0, force: !0 });
|
|
75
|
-
} catch {}
|
|
76
|
-
}
|
|
1
|
+
import{existsSync,lstatSync,mkdirSync,readdirSync,readFileSync,rmSync,symlinkSync,writeFileSync}from"node:fs";import{basename,join}from"node:path";import{path}from"@stacksjs/path";function collectModels(root,origin){if(!existsSync(root))return[];const out=[],walk=(dir)=>{let entries;try{entries=readdirSync(dir,{withFileTypes:!0})}catch{return}for(const entry of entries){const full=join(dir,entry.name);if(entry.isDirectory()){walk(full);continue}if(!entry.name.endsWith(".ts"))continue;if(entry.name.startsWith(".")||entry.name.startsWith("index"))continue;out.push({file:full,name:entry.name.replace(/\.ts$/,""),origin})}};walk(root);return out}export function modelStagingDir(){return path.frameworkRuntimePath("model-sources")}function stage(models){const dir=modelStagingDir();try{rmSync(dir,{recursive:!0,force:!0})}catch{}mkdirSync(dir,{recursive:!0});for(const model of models){const target=join(dir,`${model.name}.ts`);try{symlinkSync(model.file,target)}catch{try{writeFileSync(target,readFileSync(model.file))}catch{}}}return dir}export function resolveModelSources(options={}){const userRoot=options.userRoot??path.userModelsPath(),frameworkRoot=options.frameworkRoot??path.frameworkPath("defaults/app/Models"),user=collectModels(userRoot,"user"),framework=collectModels(frameworkRoot,"framework");if(user.length===0&&framework.length===0)return null;const byName=new Map;for(const model of framework)byName.set(model.name,model);for(const model of user)byName.set(model.name,model);const models=[...byName.values()].sort((a,b)=>a.name.localeCompare(b.name)),roots=[];if(user.length>0)roots.push(userRoot);if(framework.length>0)roots.push(frameworkRoot);const onlyUser=framework.length===0,allFlat=models.every((m)=>basename(join(m.file,".."))===basename(onlyUser?userRoot:frameworkRoot));if(roots.length===1&&allFlat)return{dir:roots[0],models,roots,staged:!1};return{dir:stage(models),models,roots,staged:!0}}export function cleanupModelStaging(){const dir=modelStagingDir();try{if(existsSync(dir)&&lstatSync(dir).isDirectory())rmSync(dir,{recursive:!0,force:!0})}catch{}}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { env as envVars } from "@stacksjs/env";
|
|
3
|
-
import { db } from "./utils";
|
|
4
|
-
import { sqlHelpers } from "./sql-helpers";
|
|
5
|
-
function getDbDriver() {
|
|
6
|
-
return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
|
|
7
|
-
}
|
|
8
|
-
export function notificationsTableSql(sql) {
|
|
9
|
-
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
10
|
-
return `CREATE TABLE IF NOT EXISTS notifications (
|
|
1
|
+
import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}export function notificationsTableSql(sql){const{pkColumn,nullableTimestamp,datetime}=sql;return`CREATE TABLE IF NOT EXISTS notifications (
|
|
11
2
|
${pkColumn},
|
|
12
3
|
user_id INTEGER NOT NULL,
|
|
13
4
|
type VARCHAR(255) NOT NULL,
|
|
@@ -16,11 +7,7 @@ export function notificationsTableSql(sql) {
|
|
|
16
7
|
uuid VARCHAR(36),
|
|
17
8
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
18
9
|
updated_at ${nullableTimestamp}
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
export function notificationPreferencesTableSql(sql) {
|
|
22
|
-
const { pkColumn, boolTrue, nullableTimestamp, datetime } = sql;
|
|
23
|
-
return `CREATE TABLE IF NOT EXISTS notification_preferences (
|
|
10
|
+
)`}export function notificationPreferencesTableSql(sql){const{pkColumn,boolTrue,nullableTimestamp,datetime}=sql;return`CREATE TABLE IF NOT EXISTS notification_preferences (
|
|
24
11
|
${pkColumn},
|
|
25
12
|
user_id INTEGER NOT NULL,
|
|
26
13
|
channel VARCHAR(50) NOT NULL,
|
|
@@ -29,11 +16,7 @@ export function notificationPreferencesTableSql(sql) {
|
|
|
29
16
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
30
17
|
updated_at ${nullableTimestamp},
|
|
31
18
|
UNIQUE (user_id, channel, category)
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
export function notificationDeliveriesTableSql(sql) {
|
|
35
|
-
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
36
|
-
return `CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
19
|
+
)`}export function notificationDeliveriesTableSql(sql){const{pkColumn,nullableTimestamp,datetime}=sql;return`CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
37
20
|
${pkColumn},
|
|
38
21
|
user_id INTEGER,
|
|
39
22
|
channel VARCHAR(50) NOT NULL,
|
|
@@ -46,32 +29,4 @@ export function notificationDeliveriesTableSql(sql) {
|
|
|
46
29
|
sent_at ${nullableTimestamp},
|
|
47
30
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
48
31
|
updated_at ${nullableTimestamp}
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
export async function migrateNotificationTables(options = {}) {
|
|
52
|
-
const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver);
|
|
53
|
-
if (options.verbose)
|
|
54
|
-
log.info(`Creating notification tables for ${dbDriver}...`);
|
|
55
|
-
try {
|
|
56
|
-
if (options.verbose)
|
|
57
|
-
log.info("Creating notifications table...");
|
|
58
|
-
await db.unsafe(notificationsTableSql(sql)).execute();
|
|
59
|
-
await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id)").execute();
|
|
60
|
-
if (options.verbose)
|
|
61
|
-
log.info("Creating notification_preferences table...");
|
|
62
|
-
await db.unsafe(notificationPreferencesTableSql(sql)).execute();
|
|
63
|
-
await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_preferences_user ON notification_preferences (user_id)").execute();
|
|
64
|
-
if (options.verbose)
|
|
65
|
-
log.info("Creating notification deliveries table...");
|
|
66
|
-
await db.unsafe(notificationDeliveriesTableSql(sql)).execute();
|
|
67
|
-
await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_channel ON notification_deliveries (channel)").execute();
|
|
68
|
-
await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries (status)").execute();
|
|
69
|
-
if (options.verbose)
|
|
70
|
-
log.success("Notification tables created");
|
|
71
|
-
return { success: !0 };
|
|
72
|
-
} catch (error) {
|
|
73
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
-
log.error(`Failed to create notification tables: ${message}`);
|
|
75
|
-
return { success: !1, error: message };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
32
|
+
)`}export async function migrateNotificationTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating notification tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating notifications table...");await db.unsafe(notificationsTableSql(sql)).execute();await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id)").execute();if(options.verbose)log.info("Creating notification_preferences table...");await db.unsafe(notificationPreferencesTableSql(sql)).execute();await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_preferences_user ON notification_preferences (user_id)").execute();if(options.verbose)log.info("Creating notification deliveries table...");await db.unsafe(notificationDeliveriesTableSql(sql)).execute();await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_channel ON notification_deliveries (channel)").execute();await db.unsafe("CREATE INDEX IF NOT EXISTS idx_notification_deliveries_status ON notification_deliveries (status)").execute();if(options.verbose)log.success("Notification tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create notification tables: ${message}`);return{success:!1,error:message}}}
|
package/dist/query-logger.js
CHANGED
|
@@ -1,241 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { config } from "@stacksjs/config";
|
|
4
|
-
import { log } from "@stacksjs/logging";
|
|
5
|
-
import { parseQuery } from "./query-parser";
|
|
6
|
-
import { db } from "./utils";
|
|
7
|
-
let trackQuery = () => {};
|
|
8
|
-
export function setQueryTracker(fn) {
|
|
9
|
-
trackQuery = fn;
|
|
10
|
-
}
|
|
11
|
-
let isLogging = !1;
|
|
12
|
-
export async function logQuery(event) {
|
|
13
|
-
if (isLogging)
|
|
14
|
-
return;
|
|
15
|
-
try {
|
|
16
|
-
const { query, durationMs, error, bindings } = extractQueryInfo(event);
|
|
17
|
-
if (query)
|
|
18
|
-
try {
|
|
19
|
-
trackQuery(query, durationMs, config.database?.default || "unknown");
|
|
20
|
-
} catch {}
|
|
21
|
-
if (!config.database?.queryLogging?.enabled)
|
|
22
|
-
return;
|
|
23
|
-
const excludedQueries = config.database?.queryLogging?.excludedQueries;
|
|
24
|
-
if (!query.trim() || isExcludedQuery(query, Array.isArray(excludedQueries) ? excludedQueries : []))
|
|
25
|
-
return;
|
|
26
|
-
const status = determineQueryStatus(durationMs, error), logRecord = await createQueryLogRecord(query, durationMs, status, error, bindings);
|
|
27
|
-
if (config.database?.queryLogging?.analysis?.enabled && (status === "slow" || config.database.queryLogging.analysis.analyzeAll))
|
|
28
|
-
await enhanceWithQueryAnalysis(logRecord);
|
|
29
|
-
isLogging = !0;
|
|
30
|
-
try {
|
|
31
|
-
await storeQueryLog(logRecord);
|
|
32
|
-
} finally {
|
|
33
|
-
isLogging = !1;
|
|
34
|
-
}
|
|
35
|
-
if (status !== "completed")
|
|
36
|
-
log[status === "failed" ? "error" : "warn"](`Query ${status}:`, {
|
|
37
|
-
query: logRecord.query,
|
|
38
|
-
duration: logRecord.duration,
|
|
39
|
-
connection: logRecord.connection,
|
|
40
|
-
...error && { error }
|
|
41
|
-
});
|
|
42
|
-
} catch (err) {
|
|
43
|
-
log.error("Failed to log query:", err);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export function isExcludedQuery(query, excludedQueries) {
|
|
47
|
-
const normalizedQuery = query.toLowerCase();
|
|
48
|
-
return excludedQueries.some((pattern) => {
|
|
49
|
-
const normalizedPattern = pattern.trim().toLowerCase();
|
|
50
|
-
return normalizedPattern.length > 0 && normalizedQuery.includes(normalizedPattern);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function queryText(sql) {
|
|
54
|
-
if (typeof sql === "string")
|
|
55
|
-
return usableQueryText(sql);
|
|
56
|
-
if (sql && typeof sql === "object" && typeof sql.then === "function")
|
|
57
|
-
return "";
|
|
58
|
-
if (sql && typeof sql.toString === "function")
|
|
59
|
-
return usableQueryText(String(sql));
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
62
|
-
function usableQueryText(text) {
|
|
63
|
-
return text.startsWith("[object ") ? "" : text;
|
|
64
|
-
}
|
|
65
|
-
function extractQueryInfo(event) {
|
|
66
|
-
const query = queryText(event.query?.sql), durationMs = event.queryDurationMillis || 0, error = event.error;
|
|
67
|
-
let bindings;
|
|
68
|
-
if (event.query?.parameters)
|
|
69
|
-
try {
|
|
70
|
-
bindings = JSON.stringify(event.query.parameters);
|
|
71
|
-
} catch {
|
|
72
|
-
bindings = "[]";
|
|
73
|
-
}
|
|
74
|
-
return { query, durationMs, error, bindings };
|
|
75
|
-
}
|
|
76
|
-
function determineQueryStatus(durationMs, error) {
|
|
77
|
-
const slowThreshold = config.database?.queryLogging?.slowThreshold || 100;
|
|
78
|
-
if (error)
|
|
79
|
-
return "failed";
|
|
80
|
-
if (durationMs > slowThreshold)
|
|
81
|
-
return "slow";
|
|
82
|
-
return "completed";
|
|
83
|
-
}
|
|
84
|
-
async function createQueryLogRecord(query, durationMs, status, error, bindings) {
|
|
85
|
-
const connection = config.database.default || "unknown", normalizedQuery = parseQuery(query).normalized || query, { trace, caller } = extractTraceInfo();
|
|
86
|
-
return {
|
|
87
|
-
query,
|
|
88
|
-
normalized_query: normalizedQuery,
|
|
89
|
-
duration: durationMs,
|
|
90
|
-
connection,
|
|
91
|
-
status,
|
|
92
|
-
error: error ? String(error) : void 0,
|
|
93
|
-
executed_at: sqlDateTime(),
|
|
94
|
-
bindings,
|
|
95
|
-
trace,
|
|
96
|
-
...caller,
|
|
97
|
-
memory_usage: memoryUsage().heapUsed / 1024 / 1024
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const SECRET_PATTERNS = [
|
|
101
|
-
/\b(?:sk|pk|rk|api|secret|token|bearer|password|pwd|key)[_-]?[A-Za-z0-9]{12,}/gi,
|
|
102
|
-
/\bsk_(?:live|test)_[A-Za-z0-9]{20,}/g,
|
|
103
|
-
/\bAKIA[0-9A-Z]{16}/g,
|
|
104
|
-
/\b[A-Za-z0-9_-]{40,}\b(?=\s|$|['"])/g
|
|
105
|
-
];
|
|
106
|
-
function sanitizeStackTrace(stack) {
|
|
107
|
-
let out = stack;
|
|
108
|
-
for (const pattern of SECRET_PATTERNS)
|
|
109
|
-
out = out.replace(pattern, "<redacted>");
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
function extractTraceInfo() {
|
|
113
|
-
try {
|
|
114
|
-
const stack = Error("Stack trace capture").stack || "", callerLine = stack.split(`
|
|
115
|
-
`).slice(1).find((line) => !line.includes("query-logger.ts"));
|
|
116
|
-
let caller = {};
|
|
117
|
-
if (callerLine) {
|
|
118
|
-
const methodMatch = callerLine.match(/at (.+?) \(/), fileMatch = callerLine.match(/\((.+?):(\d+):(\d+)\)/);
|
|
119
|
-
if (methodMatch && methodMatch[1]) {
|
|
120
|
-
const methodParts = methodMatch[1].split(".");
|
|
121
|
-
caller = {
|
|
122
|
-
model: methodParts.length > 1 ? methodParts[0] : void 0,
|
|
123
|
-
method: methodParts.length > 1 ? methodParts[1] : methodParts[0]
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
if (fileMatch && fileMatch[1] && fileMatch[2])
|
|
127
|
-
caller = {
|
|
128
|
-
...caller,
|
|
129
|
-
file: fileMatch[1],
|
|
130
|
-
line: Number.parseInt(fileMatch[2], 10)
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
trace: sanitizeStackTrace(stack),
|
|
135
|
-
caller
|
|
136
|
-
};
|
|
137
|
-
} catch {
|
|
138
|
-
return { trace: "", caller: {} };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
async function enhanceWithQueryAnalysis(logRecord) {
|
|
142
|
-
try {
|
|
143
|
-
const { tables, type } = parseQuery(logRecord.query);
|
|
144
|
-
logRecord.affected_tables = JSON.stringify(tables || []);
|
|
145
|
-
if (type === "SELECT" && config.database?.queryLogging?.analysis?.explainPlan) {
|
|
146
|
-
const explainResult = await getExplainPlan(logRecord.query);
|
|
147
|
-
if (explainResult) {
|
|
148
|
-
logRecord.explain_plan = explainResult.plan;
|
|
149
|
-
logRecord.indexes_used = JSON.stringify(explainResult.indexesUsed || []);
|
|
150
|
-
logRecord.missing_indexes = JSON.stringify(explainResult.missingIndexes || []);
|
|
151
|
-
if (config.database?.queryLogging?.analysis?.suggestions)
|
|
152
|
-
logRecord.optimization_suggestions = JSON.stringify(generateOptimizationSuggestions(explainResult, logRecord));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const tags = [type];
|
|
156
|
-
if (tables && tables.length > 0)
|
|
157
|
-
tags.push(...tables.map((table) => `table:${table}`));
|
|
158
|
-
logRecord.tags = JSON.stringify(tags);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
log.debug("Error during query analysis:", error);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
const EXPLAIN_DIALECTS = new Set(["sqlite", "mysql", "postgres"]);
|
|
164
|
-
async function getExplainPlan(query) {
|
|
165
|
-
try {
|
|
166
|
-
const rawDriver = (await import("@stacksjs/config")).config?.database?.default, driver = typeof rawDriver === "string" ? rawDriver : "sqlite";
|
|
167
|
-
if (!EXPLAIN_DIALECTS.has(driver))
|
|
168
|
-
return null;
|
|
169
|
-
let sqlText;
|
|
170
|
-
if (driver === "mysql")
|
|
171
|
-
sqlText = `EXPLAIN FORMAT=JSON ${query}`;
|
|
172
|
-
else if (driver === "postgres")
|
|
173
|
-
sqlText = `EXPLAIN (FORMAT JSON) ${query}`;
|
|
174
|
-
else
|
|
175
|
-
sqlText = `EXPLAIN QUERY PLAN ${query}`;
|
|
176
|
-
const result = await db.unsafe?.(sqlText);
|
|
177
|
-
if (!result)
|
|
178
|
-
return null;
|
|
179
|
-
const rows = Array.isArray(result) ? result : result.rows ?? [], planText = JSON.stringify(rows), indexesUsed = [], missingIndexes = [];
|
|
180
|
-
if (driver === "sqlite")
|
|
181
|
-
for (const row of rows) {
|
|
182
|
-
const detail = row?.detail || "", idxMatch = detail.match(/USING (?:COVERING )?INDEX (\w+)/i);
|
|
183
|
-
if (idxMatch && idxMatch[1])
|
|
184
|
-
indexesUsed.push(idxMatch[1]);
|
|
185
|
-
else {
|
|
186
|
-
const scanMatch = detail.match(/^SCAN\s+(\w+)/i);
|
|
187
|
-
if (scanMatch && scanMatch[1])
|
|
188
|
-
missingIndexes.push(scanMatch[1]);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
else if (driver === "mysql") {
|
|
192
|
-
const text = planText;
|
|
193
|
-
for (const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))
|
|
194
|
-
if (m[1])
|
|
195
|
-
indexesUsed.push(m[1]);
|
|
196
|
-
for (const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))
|
|
197
|
-
if (m[1])
|
|
198
|
-
missingIndexes.push(m[1]);
|
|
199
|
-
} else if (driver === "postgres") {
|
|
200
|
-
const text = planText;
|
|
201
|
-
for (const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))
|
|
202
|
-
if (m[1])
|
|
203
|
-
indexesUsed.push(m[1]);
|
|
204
|
-
for (const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))
|
|
205
|
-
if (m[1])
|
|
206
|
-
missingIndexes.push(m[1]);
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
plan: planText.length > 4000 ? `${planText.slice(0, 4000)}\u2026` : planText,
|
|
210
|
-
indexesUsed: Array.from(new Set(indexesUsed)),
|
|
211
|
-
missingIndexes: Array.from(new Set(missingIndexes))
|
|
212
|
-
};
|
|
213
|
-
} catch (err) {
|
|
214
|
-
log.debug("[query-logger] EXPLAIN failed (non-fatal):", err);
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function generateOptimizationSuggestions(explainResult, logRecord) {
|
|
219
|
-
const suggestions = [];
|
|
220
|
-
if (explainResult.missingIndexes && explainResult.missingIndexes.length > 0)
|
|
221
|
-
suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);
|
|
222
|
-
if (logRecord.status === "slow") {
|
|
223
|
-
suggestions.push("Consider optimizing this query to reduce execution time");
|
|
224
|
-
if (logRecord.query.toLowerCase().includes("select *"))
|
|
225
|
-
suggestions.push("Specify only needed columns instead of using SELECT *");
|
|
226
|
-
if (!logRecord.query.toLowerCase().includes("limit"))
|
|
227
|
-
suggestions.push("Consider adding LIMIT clause to reduce result set size");
|
|
228
|
-
}
|
|
229
|
-
return suggestions;
|
|
230
|
-
}
|
|
231
|
-
async function storeQueryLog(logRecord) {
|
|
232
|
-
try {
|
|
233
|
-
await db.insertInto("query_logs").values(logRecord).execute();
|
|
234
|
-
} catch (error) {
|
|
235
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
-
if (/no such table|does not exist|doesn't exist/i.test(message) && /query_logs/i.test(message))
|
|
237
|
-
log.debug("Query logging will start after the query_logs table is migrated.");
|
|
238
|
-
else
|
|
239
|
-
log.error("Failed to store query log:", error);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
1
|
+
import{sqlDateTime}from"./sql-helpers";import{memoryUsage}from"node:process";import{config}from"@stacksjs/config";import{log}from"@stacksjs/logging";import{parseQuery}from"./query-parser";import{db}from"./utils";let trackQuery=()=>{};export function setQueryTracker(fn){trackQuery=fn}let isLogging=!1;export async function logQuery(event){if(isLogging)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);isLogging=!0;try{await storeQueryLog(logRecord)}finally{isLogging=!1}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=parseQuery(query).normalized||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}function extractTraceInfo(){try{const stack=Error("Stack trace capture").stack||"",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)}}return{trace:sanitizeStackTrace(stack),caller}}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 rows=Array.isArray(result)?result:result.rows??[],planText=JSON.stringify(rows),indexesUsed=[],missingIndexes=[];if(driver==="sqlite")for(const row of rows){const detail=row?.detail||"",idxMatch=detail.match(/USING (?:COVERING )?INDEX (\w+)/i);if(idxMatch&&idxMatch[1])indexesUsed.push(idxMatch[1]);else{const scanMatch=detail.match(/^SCAN\s+(\w+)/i);if(scanMatch&&scanMatch[1])missingIndexes.push(scanMatch[1])}}else if(driver==="mysql"){const text=planText;for(const m of text.matchAll(/"key"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"table_name"\s*:\s*"([^"]+)"[^}]*"access_type"\s*:\s*"ALL"/g))if(m[1])missingIndexes.push(m[1])}else if(driver==="postgres"){const text=planText;for(const m of text.matchAll(/"Index Name"\s*:\s*"([^"]+)"/g))if(m[1])indexesUsed.push(m[1]);for(const m of text.matchAll(/"Node Type"\s*:\s*"Seq Scan"[^}]*"Relation Name"\s*:\s*"([^"]+)"/g))if(m[1])missingIndexes.push(m[1])}return{plan:planText.length>4000?`${planText.slice(0,4000)}\u2026`:planText,indexesUsed:Array.from(new Set(indexesUsed)),missingIndexes:Array.from(new Set(missingIndexes))}}catch(err){log.debug("[query-logger] EXPLAIN failed (non-fatal):",err);return null}}function generateOptimizationSuggestions(explainResult,logRecord){const suggestions=[];if(explainResult.missingIndexes&&explainResult.missingIndexes.length>0)suggestions.push(`Consider adding index on ${explainResult.missingIndexes.join(", ")}`);if(logRecord.status==="slow"){suggestions.push("Consider optimizing this query to reduce execution time");if(logRecord.query.toLowerCase().includes("select *"))suggestions.push("Specify only needed columns instead of using SELECT *");if(!logRecord.query.toLowerCase().includes("limit"))suggestions.push("Consider adding LIMIT clause to reduce result set size")}return suggestions}async function storeQueryLog(logRecord){try{await db.insertInto("query_logs").values(logRecord).execute()}catch(error){const message=error instanceof Error?error.message:String(error);if(/no such table|does not exist|doesn't exist/i.test(message)&&/query_logs/i.test(message))log.debug("Query logging will start after the query_logs table is migrated.");else log.error("Failed to store query log:",error)}}
|
package/dist/query-parser.js
CHANGED
|
@@ -1,93 +1 @@
|
|
|
1
|
-
export function parseQuery(sql) {
|
|
2
|
-
const result = {
|
|
3
|
-
normalized: "",
|
|
4
|
-
type: "OTHER",
|
|
5
|
-
tables: []
|
|
6
|
-
};
|
|
7
|
-
if (!sql)
|
|
8
|
-
return result;
|
|
9
|
-
try {
|
|
10
|
-
const upperQuery = sql.trim().toUpperCase();
|
|
11
|
-
if (upperQuery.startsWith("SELECT"))
|
|
12
|
-
result.type = "SELECT";
|
|
13
|
-
else if (upperQuery.startsWith("INSERT"))
|
|
14
|
-
result.type = "INSERT";
|
|
15
|
-
else if (upperQuery.startsWith("UPDATE"))
|
|
16
|
-
result.type = "UPDATE";
|
|
17
|
-
else if (upperQuery.startsWith("DELETE"))
|
|
18
|
-
result.type = "DELETE";
|
|
19
|
-
result.tables = extractTables(sql, result.type);
|
|
20
|
-
result.normalized = normalizeQuery(sql);
|
|
21
|
-
return result;
|
|
22
|
-
} catch {
|
|
23
|
-
return {
|
|
24
|
-
normalized: sql,
|
|
25
|
-
type: "OTHER",
|
|
26
|
-
tables: []
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function buildIdentifierRegex(keyword, flags = "i") {
|
|
31
|
-
return new RegExp(`${keyword}\\s+(?:"([^"]+)"|\`([^\`]+)\`|\\[([^\\]]+)\\]|([\\w.]+))`, flags);
|
|
32
|
-
}
|
|
33
|
-
function pickMatchedIdentifier(match) {
|
|
34
|
-
if (!match)
|
|
35
|
-
return null;
|
|
36
|
-
const ident = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
37
|
-
if (!ident)
|
|
38
|
-
return null;
|
|
39
|
-
return ident.split(".").pop() ?? null;
|
|
40
|
-
}
|
|
41
|
-
function extractTables(sql, type) {
|
|
42
|
-
const tables = [];
|
|
43
|
-
try {
|
|
44
|
-
switch (type) {
|
|
45
|
-
case "SELECT": {
|
|
46
|
-
const fromMatch = sql.match(buildIdentifierRegex("from")), fromTable = pickMatchedIdentifier(fromMatch);
|
|
47
|
-
if (fromTable)
|
|
48
|
-
tables.push(fromTable);
|
|
49
|
-
const joinRegex = buildIdentifierRegex("join", "gi");
|
|
50
|
-
for (const m of sql.matchAll(joinRegex)) {
|
|
51
|
-
const joinTable = pickMatchedIdentifier(m);
|
|
52
|
-
if (joinTable)
|
|
53
|
-
tables.push(joinTable);
|
|
54
|
-
}
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
case "INSERT": {
|
|
58
|
-
const intoMatch = sql.match(buildIdentifierRegex("into")), intoTable = pickMatchedIdentifier(intoMatch);
|
|
59
|
-
if (intoTable)
|
|
60
|
-
tables.push(intoTable);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case "UPDATE": {
|
|
64
|
-
const updateMatch = sql.match(buildIdentifierRegex("update")), updateTable = pickMatchedIdentifier(updateMatch);
|
|
65
|
-
if (updateTable)
|
|
66
|
-
tables.push(updateTable);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
case "DELETE": {
|
|
70
|
-
const deleteFromMatch = sql.match(buildIdentifierRegex("from")), deleteTable = pickMatchedIdentifier(deleteFromMatch);
|
|
71
|
-
if (deleteTable)
|
|
72
|
-
tables.push(deleteTable);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
} catch {}
|
|
77
|
-
return [...new Set(tables)].filter(Boolean);
|
|
78
|
-
}
|
|
79
|
-
function normalizeQuery(sql) {
|
|
80
|
-
try {
|
|
81
|
-
let normalizedSql = sql;
|
|
82
|
-
normalizedSql = normalizedSql.replace(/(?<![a-zA-Z_])\b\d+\b(?![a-zA-Z_])/g, "?");
|
|
83
|
-
normalizedSql = normalizedSql.replace(/'([^']|'')*'/g, "?");
|
|
84
|
-
normalizedSql = normalizedSql.replace(/"([^"]|"")*"/g, "?");
|
|
85
|
-
normalizedSql = normalizedSql.replace(/\btrue\b/gi, "?");
|
|
86
|
-
normalizedSql = normalizedSql.replace(/\bfalse\b/gi, "?");
|
|
87
|
-
normalizedSql = normalizedSql.replace(/\bnull\b/gi, "?");
|
|
88
|
-
normalizedSql = normalizedSql.replace(/\s+/g, " ").trim();
|
|
89
|
-
return normalizedSql;
|
|
90
|
-
} catch {
|
|
91
|
-
return sql;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
export function parseQuery(sql){const result={normalized:"",type:"OTHER",tables:[]};if(!sql)return result;try{const upperQuery=sql.trim().toUpperCase();if(upperQuery.startsWith("SELECT"))result.type="SELECT";else if(upperQuery.startsWith("INSERT"))result.type="INSERT";else if(upperQuery.startsWith("UPDATE"))result.type="UPDATE";else if(upperQuery.startsWith("DELETE"))result.type="DELETE";result.tables=extractTables(sql,result.type);result.normalized=normalizeQuery(sql);return result}catch{return{normalized:sql,type:"OTHER",tables:[]}}}function buildIdentifierRegex(keyword,flags="i"){return new RegExp(`${keyword}\\s+(?:"([^"]+)"|\`([^\`]+)\`|\\[([^\\]]+)\\]|([\\w.]+))`,flags)}function pickMatchedIdentifier(match){if(!match)return null;const ident=match[1]??match[2]??match[3]??match[4];if(!ident)return null;return ident.split(".").pop()??null}function extractTables(sql,type){const tables=[];try{switch(type){case"SELECT":{const fromMatch=sql.match(buildIdentifierRegex("from")),fromTable=pickMatchedIdentifier(fromMatch);if(fromTable)tables.push(fromTable);const joinRegex=buildIdentifierRegex("join","gi");for(const m of sql.matchAll(joinRegex)){const joinTable=pickMatchedIdentifier(m);if(joinTable)tables.push(joinTable)}break}case"INSERT":{const intoMatch=sql.match(buildIdentifierRegex("into")),intoTable=pickMatchedIdentifier(intoMatch);if(intoTable)tables.push(intoTable);break}case"UPDATE":{const updateMatch=sql.match(buildIdentifierRegex("update")),updateTable=pickMatchedIdentifier(updateMatch);if(updateTable)tables.push(updateTable);break}case"DELETE":{const deleteFromMatch=sql.match(buildIdentifierRegex("from")),deleteTable=pickMatchedIdentifier(deleteFromMatch);if(deleteTable)tables.push(deleteTable);break}}}catch{}return[...new Set(tables)].filter(Boolean)}function normalizeQuery(sql){try{let normalizedSql=sql;normalizedSql=normalizedSql.replace(/(?<![a-zA-Z_])\b\d+\b(?![a-zA-Z_])/g,"?");normalizedSql=normalizedSql.replace(/'([^']|'')*'/g,"?");normalizedSql=normalizedSql.replace(/"([^"]|"")*"/g,"?");normalizedSql=normalizedSql.replace(/\btrue\b/gi,"?");normalizedSql=normalizedSql.replace(/\bfalse\b/gi,"?");normalizedSql=normalizedSql.replace(/\bnull\b/gi,"?");normalizedSql=normalizedSql.replace(/\s+/g," ").trim();return normalizedSql}catch{return sql}}
|
package/dist/rbac-tables.js
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { env as envVars } from "@stacksjs/env";
|
|
3
|
-
import { db } from "./utils";
|
|
4
|
-
import { sqlHelpers } from "./sql-helpers";
|
|
5
|
-
function getDbDriver() {
|
|
6
|
-
return envVars.DB_CONNECTION || "sqlite";
|
|
7
|
-
}
|
|
8
|
-
export function rolesTableSql(sql) {
|
|
9
|
-
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
10
|
-
return `CREATE TABLE IF NOT EXISTS roles (
|
|
1
|
+
import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";function getDbDriver(){return envVars.DB_CONNECTION||"sqlite"}export function rolesTableSql(sql){const{pkColumn,nullableTimestamp,datetime}=sql;return`CREATE TABLE IF NOT EXISTS roles (
|
|
11
2
|
${pkColumn},
|
|
12
3
|
name VARCHAR(255) NOT NULL,
|
|
13
4
|
guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
|
|
@@ -15,11 +6,7 @@ export function rolesTableSql(sql) {
|
|
|
15
6
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
16
7
|
updated_at ${nullableTimestamp},
|
|
17
8
|
UNIQUE (name, guard_name)
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
export function permissionsTableSql(sql) {
|
|
21
|
-
const { pkColumn, nullableTimestamp, datetime } = sql;
|
|
22
|
-
return `CREATE TABLE IF NOT EXISTS permissions (
|
|
9
|
+
)`}export function permissionsTableSql(sql){const{pkColumn,nullableTimestamp,datetime}=sql;return`CREATE TABLE IF NOT EXISTS permissions (
|
|
23
10
|
${pkColumn},
|
|
24
11
|
name VARCHAR(255) NOT NULL,
|
|
25
12
|
guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
|
|
@@ -27,61 +14,19 @@ export function permissionsTableSql(sql) {
|
|
|
27
14
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
28
15
|
updated_at ${nullableTimestamp},
|
|
29
16
|
UNIQUE (name, guard_name)
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
export function userRolesTableSql(sql) {
|
|
33
|
-
const { datetime } = sql;
|
|
34
|
-
return `CREATE TABLE IF NOT EXISTS user_roles (
|
|
17
|
+
)`}export function userRolesTableSql(sql){const{datetime}=sql;return`CREATE TABLE IF NOT EXISTS user_roles (
|
|
35
18
|
user_id INTEGER NOT NULL,
|
|
36
19
|
role_id INTEGER NOT NULL,
|
|
37
20
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
38
21
|
PRIMARY KEY (user_id, role_id)
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
export function userPermissionsTableSql(sql) {
|
|
42
|
-
const { datetime } = sql;
|
|
43
|
-
return `CREATE TABLE IF NOT EXISTS user_permissions (
|
|
22
|
+
)`}export function userPermissionsTableSql(sql){const{datetime}=sql;return`CREATE TABLE IF NOT EXISTS user_permissions (
|
|
44
23
|
user_id INTEGER NOT NULL,
|
|
45
24
|
permission_id INTEGER NOT NULL,
|
|
46
25
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
47
26
|
PRIMARY KEY (user_id, permission_id)
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
export function rolePermissionsTableSql(sql) {
|
|
51
|
-
const { datetime } = sql;
|
|
52
|
-
return `CREATE TABLE IF NOT EXISTS role_permissions (
|
|
27
|
+
)`}export function rolePermissionsTableSql(sql){const{datetime}=sql;return`CREATE TABLE IF NOT EXISTS role_permissions (
|
|
53
28
|
role_id INTEGER NOT NULL,
|
|
54
29
|
permission_id INTEGER NOT NULL,
|
|
55
30
|
created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
|
|
56
31
|
PRIMARY KEY (role_id, permission_id)
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
export async function migrateRbacTables(options = {}) {
|
|
60
|
-
const dbDriver = getDbDriver(), sql = sqlHelpers(dbDriver);
|
|
61
|
-
if (options.verbose)
|
|
62
|
-
log.info(`Creating RBAC tables for ${dbDriver}...`);
|
|
63
|
-
try {
|
|
64
|
-
if (options.verbose)
|
|
65
|
-
log.info("Creating roles table...");
|
|
66
|
-
await db.unsafe(rolesTableSql(sql)).execute();
|
|
67
|
-
if (options.verbose)
|
|
68
|
-
log.info("Creating permissions table...");
|
|
69
|
-
await db.unsafe(permissionsTableSql(sql)).execute();
|
|
70
|
-
if (options.verbose)
|
|
71
|
-
log.info("Creating user_roles pivot...");
|
|
72
|
-
await db.unsafe(userRolesTableSql(sql)).execute();
|
|
73
|
-
if (options.verbose)
|
|
74
|
-
log.info("Creating user_permissions pivot...");
|
|
75
|
-
await db.unsafe(userPermissionsTableSql(sql)).execute();
|
|
76
|
-
if (options.verbose)
|
|
77
|
-
log.info("Creating role_permissions pivot...");
|
|
78
|
-
await db.unsafe(rolePermissionsTableSql(sql)).execute();
|
|
79
|
-
if (options.verbose)
|
|
80
|
-
log.success("RBAC tables created");
|
|
81
|
-
return { success: !0 };
|
|
82
|
-
} catch (error) {
|
|
83
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
-
log.error(`Failed to create RBAC tables: ${message}`);
|
|
85
|
-
return { success: !1, error: message };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
32
|
+
)`}export async function migrateRbacTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating RBAC tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating roles table...");await db.unsafe(rolesTableSql(sql)).execute();if(options.verbose)log.info("Creating permissions table...");await db.unsafe(permissionsTableSql(sql)).execute();if(options.verbose)log.info("Creating user_roles pivot...");await db.unsafe(userRolesTableSql(sql)).execute();if(options.verbose)log.info("Creating user_permissions pivot...");await db.unsafe(userPermissionsTableSql(sql)).execute();if(options.verbose)log.info("Creating role_permissions pivot...");await db.unsafe(rolePermissionsTableSql(sql)).execute();if(options.verbose)log.success("RBAC tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create RBAC tables: ${message}`);return{success:!1,error:message}}}
|