@stacksjs/database 0.70.258 → 0.70.260
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 +2 -79
- 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/fk-audit.js
CHANGED
|
@@ -1,186 +1,9 @@
|
|
|
1
|
-
import { existsSync } from "
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { plural, snakeCase } from "@stacksjs/strings";
|
|
4
|
-
import { path } from "@stacksjs/path";
|
|
5
|
-
import { globSync } from "@stacksjs/storage";
|
|
6
|
-
export function safeGlob(pattern) {
|
|
7
|
-
const metaIdx = pattern.search(/[*?[]/), root = metaIdx === -1 ? dirname(pattern) : dirname(pattern.slice(0, metaIdx));
|
|
8
|
-
if (!existsSync(root))
|
|
9
|
-
return [];
|
|
10
|
-
try {
|
|
11
|
-
return globSync(pattern, { absolute: !0 });
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function modelTable(model) {
|
|
17
|
-
return model.table || plural(snakeCase(model.name || ""));
|
|
18
|
-
}
|
|
19
|
-
function modelNameForColumn(column) {
|
|
20
|
-
return column.replace(/_id$/, "").split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
21
|
-
}
|
|
22
|
-
export function getDeclaredFKsFromModels(models) {
|
|
23
|
-
const meta = new Map(models.map((model) => [String(model.name ?? ""), {
|
|
24
|
-
primaryKey: String(model.primaryKey ?? "id"),
|
|
25
|
-
table: modelTable(model)
|
|
26
|
-
}])), declared = [], seen = new Set, push = (fk) => {
|
|
27
|
-
const key = `${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();
|
|
28
|
-
if (seen.has(key))
|
|
29
|
-
return;
|
|
30
|
-
seen.add(key);
|
|
31
|
-
declared.push(fk);
|
|
32
|
-
};
|
|
33
|
-
for (const model of models) {
|
|
34
|
-
const fromTable = modelTable(model), modelName = String(model.name ?? ""), attributes = model.attributes ?? {};
|
|
35
|
-
for (const [attributeName, attribute] of Object.entries(attributes)) {
|
|
36
|
-
const fromColumn = snakeCase(attributeName);
|
|
37
|
-
if (!fromColumn.endsWith("_id") || attribute.foreignKey === !1)
|
|
38
|
-
continue;
|
|
39
|
-
if (attribute.foreignKey && typeof attribute.foreignKey === "object") {
|
|
40
|
-
push({
|
|
41
|
-
fromTable,
|
|
42
|
-
fromColumn,
|
|
43
|
-
toTable: attribute.foreignKey.table,
|
|
44
|
-
toColumn: attribute.foreignKey.column ?? "id",
|
|
45
|
-
model: modelName
|
|
46
|
-
});
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
const related = meta.get(modelNameForColumn(fromColumn));
|
|
50
|
-
if (related)
|
|
51
|
-
push({ fromTable, fromColumn, toTable: related.table, toColumn: related.primaryKey, model: modelName });
|
|
52
|
-
}
|
|
53
|
-
const belongsTo = model.belongsTo, relations = Array.isArray(belongsTo) ? belongsTo : belongsTo && typeof belongsTo === "object" ? Object.keys(belongsTo).map((name) => ({ model: name })) : [];
|
|
54
|
-
for (const entry of relations) {
|
|
55
|
-
const relatedName = typeof entry === "string" ? entry : String(entry.model ?? "");
|
|
56
|
-
if (!relatedName)
|
|
57
|
-
continue;
|
|
58
|
-
const fromColumn = typeof entry === "object" && entry.foreignKey ? String(entry.foreignKey) : `${snakeCase(relatedName)}_id`;
|
|
59
|
-
if (Object.keys(attributes).some((attribute) => snakeCase(attribute) === fromColumn))
|
|
60
|
-
continue;
|
|
61
|
-
const related = meta.get(relatedName);
|
|
62
|
-
if (related)
|
|
63
|
-
push({ fromTable, fromColumn, toTable: related.table, toColumn: related.primaryKey, model: modelName });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return declared;
|
|
67
|
-
}
|
|
68
|
-
export async function getDeclaredFKs() {
|
|
69
|
-
const userModelFiles = safeGlob(path.userModelsPath("*.ts")), modelFiles = userModelFiles.length > 0 ? userModelFiles : safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts")), models = [];
|
|
70
|
-
for (const modelFile of modelFiles) {
|
|
71
|
-
let model;
|
|
72
|
-
try {
|
|
73
|
-
model = (await import(modelFile)).default;
|
|
74
|
-
} catch {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (model && typeof model === "object")
|
|
78
|
-
models.push(model);
|
|
79
|
-
}
|
|
80
|
-
return getDeclaredFKsFromModels(models);
|
|
81
|
-
}
|
|
82
|
-
export async function getLiveFKs() {
|
|
83
|
-
const { db } = await import("./utils"), dialect = await currentDialect();
|
|
84
|
-
if (dialect === "sqlite")
|
|
85
|
-
return getSqliteLiveFKs(db);
|
|
86
|
-
if (dialect === "mysql")
|
|
87
|
-
return getMysqlLiveFKs(db);
|
|
88
|
-
if (dialect === "postgres")
|
|
89
|
-
return getPostgresLiveFKs(db);
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
export async function auditForeignKeys() {
|
|
93
|
-
const declared = await getDeclaredFKs(), live = await getLiveFKs(), liveKeys = new Set(live.map((fk) => `${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)), missing = declared.filter((d) => {
|
|
94
|
-
const key = `${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;
|
|
95
|
-
return !liveKeys.has(key);
|
|
96
|
-
});
|
|
97
|
-
return { declared, live, missing };
|
|
98
|
-
}
|
|
99
|
-
export async function findFkOrphans(dialect) {
|
|
100
|
-
if ((dialect ?? await currentDialect()) !== "sqlite")
|
|
101
|
-
return { supported: !1, total: 0, orphans: [] };
|
|
102
|
-
const { db } = await import("./utils"), rows = await db.unsafe("PRAGMA foreign_key_check").execute(), checkRows = Array.isArray(rows) ? rows : [], fkListCache = new Map;
|
|
103
|
-
async function fkListFor(table) {
|
|
104
|
-
if (fkListCache.has(table))
|
|
105
|
-
return fkListCache.get(table);
|
|
106
|
-
if (!/^[a-z_]\w*$/i.test(table)) {
|
|
107
|
-
fkListCache.set(table, []);
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const list = await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(), arr = Array.isArray(list) ? list : [];
|
|
111
|
-
fkListCache.set(table, arr);
|
|
112
|
-
return arr;
|
|
113
|
-
}
|
|
114
|
-
const grouped = new Map;
|
|
115
|
-
for (const raw of checkRows) {
|
|
116
|
-
const r = raw, table = String(r.table ?? ""), parent = String(r.parent ?? "");
|
|
117
|
-
if (!table || !parent)
|
|
118
|
-
continue;
|
|
119
|
-
const fkid = Number(r.fkid ?? 0);
|
|
120
|
-
let column = "";
|
|
121
|
-
const fkList = await fkListFor(table);
|
|
122
|
-
for (const fk of fkList) {
|
|
123
|
-
const f = fk;
|
|
124
|
-
if (Number(f.id) === fkid && f.from) {
|
|
125
|
-
column = String(f.from);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const key = `${table}\x00${parent}\x00${fkid}`;
|
|
130
|
-
let entry = grouped.get(key);
|
|
131
|
-
if (!entry) {
|
|
132
|
-
entry = { table, column, parent, count: 0, sampleRowids: [] };
|
|
133
|
-
grouped.set(key, entry);
|
|
134
|
-
}
|
|
135
|
-
entry.count++;
|
|
136
|
-
if (entry.sampleRowids.length < 5 && typeof r.rowid === "number")
|
|
137
|
-
entry.sampleRowids.push(r.rowid);
|
|
138
|
-
}
|
|
139
|
-
const orphans = [...grouped.values()];
|
|
140
|
-
return { supported: !0, total: orphans.reduce((sum, o) => sum + o.count, 0), orphans };
|
|
141
|
-
}
|
|
142
|
-
async function currentDialect() {
|
|
143
|
-
const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
|
|
144
|
-
if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
|
|
145
|
-
return driver;
|
|
146
|
-
return "other";
|
|
147
|
-
}
|
|
148
|
-
async function getSqliteLiveFKs(db) {
|
|
149
|
-
const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"), rows = Array.isArray(tables) ? tables : [], fks = [];
|
|
150
|
-
for (const row of rows) {
|
|
151
|
-
const fromTable = row.name;
|
|
152
|
-
if (!fromTable)
|
|
153
|
-
continue;
|
|
154
|
-
if (!/^[a-z_][\w]*$/i.test(fromTable))
|
|
155
|
-
continue;
|
|
156
|
-
const fkRows = await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);
|
|
157
|
-
for (const fk of Array.isArray(fkRows) ? fkRows : []) {
|
|
158
|
-
const r = fk;
|
|
159
|
-
if ((r.seq ?? 0) !== 0)
|
|
160
|
-
continue;
|
|
161
|
-
if (!r.from || !r.to || !r.table)
|
|
162
|
-
continue;
|
|
163
|
-
fks.push({ fromTable, fromColumn: r.from, toTable: r.table, toColumn: r.to });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return fks;
|
|
167
|
-
}
|
|
168
|
-
async function getMysqlLiveFKs(db) {
|
|
169
|
-
const rows = await db.unsafe(`
|
|
1
|
+
import{existsSync}from"node:fs";import{dirname}from"node:path";import{plural,snakeCase}from"@stacksjs/strings";import{path}from"@stacksjs/path";import{globSync}from"@stacksjs/storage";export function safeGlob(pattern){const metaIdx=pattern.search(/[*?[]/),root=metaIdx===-1?dirname(pattern):dirname(pattern.slice(0,metaIdx));if(!existsSync(root))return[];try{return globSync(pattern,{absolute:!0})}catch{return[]}}function modelTable(model){return model.table||plural(snakeCase(model.name||""))}function modelNameForColumn(column){return column.replace(/_id$/,"").split("_").map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join("")}export function getDeclaredFKsFromModels(models){const meta=new Map(models.map((model)=>[String(model.name??""),{primaryKey:String(model.primaryKey??"id"),table:modelTable(model)}])),declared=[],seen=new Set,push=(fk)=>{const key=`${fk.fromTable}.${fk.fromColumn}\u2192${fk.toTable}.${fk.toColumn}`.toLowerCase();if(seen.has(key))return;seen.add(key);declared.push(fk)};for(const model of models){const fromTable=modelTable(model),modelName=String(model.name??""),attributes=model.attributes??{};for(const[attributeName,attribute]of Object.entries(attributes)){const fromColumn=snakeCase(attributeName);if(!fromColumn.endsWith("_id")||attribute.foreignKey===!1)continue;if(attribute.foreignKey&&typeof attribute.foreignKey==="object"){push({fromTable,fromColumn,toTable:attribute.foreignKey.table,toColumn:attribute.foreignKey.column??"id",model:modelName});continue}const related=meta.get(modelNameForColumn(fromColumn));if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}const belongsTo=model.belongsTo,relations=Array.isArray(belongsTo)?belongsTo:belongsTo&&typeof belongsTo==="object"?Object.keys(belongsTo).map((name)=>({model:name})):[];for(const entry of relations){const relatedName=typeof entry==="string"?entry:String(entry.model??"");if(!relatedName)continue;const fromColumn=typeof entry==="object"&&entry.foreignKey?String(entry.foreignKey):`${snakeCase(relatedName)}_id`;if(Object.keys(attributes).some((attribute)=>snakeCase(attribute)===fromColumn))continue;const related=meta.get(relatedName);if(related)push({fromTable,fromColumn,toTable:related.table,toColumn:related.primaryKey,model:modelName})}}return declared}export async function getDeclaredFKs(){const userModelFiles=safeGlob(path.userModelsPath("*.ts")),modelFiles=userModelFiles.length>0?userModelFiles:safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts")),models=[];for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(model&&typeof model==="object")models.push(model)}return getDeclaredFKsFromModels(models)}export async function getLiveFKs(){const{db}=await import("./utils"),dialect=await currentDialect();if(dialect==="sqlite")return getSqliteLiveFKs(db);if(dialect==="mysql")return getMysqlLiveFKs(db);if(dialect==="postgres")return getPostgresLiveFKs(db);return[]}export async function auditForeignKeys(){const declared=await getDeclaredFKs(),live=await getLiveFKs(),liveKeys=new Set(live.map((fk)=>`${fk.fromTable.toLowerCase()}.${fk.fromColumn.toLowerCase()}\u2192${fk.toTable.toLowerCase()}.${fk.toColumn.toLowerCase()}`)),missing=declared.filter((d)=>{const key=`${d.fromTable.toLowerCase()}.${d.fromColumn.toLowerCase()}\u2192${d.toTable.toLowerCase()}.${d.toColumn.toLowerCase()}`;return!liveKeys.has(key)});return{declared,live,missing}}export async function findFkOrphans(dialect){if((dialect??await currentDialect())!=="sqlite")return{supported:!1,total:0,orphans:[]};const{db}=await import("./utils"),rows=await db.unsafe("PRAGMA foreign_key_check").execute(),checkRows=Array.isArray(rows)?rows:[],fkListCache=new Map;async function fkListFor(table){if(fkListCache.has(table))return fkListCache.get(table);if(!/^[a-z_]\w*$/i.test(table)){fkListCache.set(table,[]);return[]}const list=await db.unsafe(`PRAGMA foreign_key_list("${table}")`).execute(),arr=Array.isArray(list)?list:[];fkListCache.set(table,arr);return arr}const grouped=new Map;for(const raw of checkRows){const r=raw,table=String(r.table??""),parent=String(r.parent??"");if(!table||!parent)continue;const fkid=Number(r.fkid??0);let column="";const fkList=await fkListFor(table);for(const fk of fkList){const f=fk;if(Number(f.id)===fkid&&f.from){column=String(f.from);break}}const key=`${table}\x00${parent}\x00${fkid}`;let entry=grouped.get(key);if(!entry){entry={table,column,parent,count:0,sampleRowids:[]};grouped.set(key,entry)}entry.count++;if(entry.sampleRowids.length<5&&typeof r.rowid==="number")entry.sampleRowids.push(r.rowid)}const orphans=[...grouped.values()];return{supported:!0,total:orphans.reduce((sum,o)=>sum+o.count,0),orphans}}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 getSqliteLiveFKs(db){const tables=await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"),rows=Array.isArray(tables)?tables:[],fks=[];for(const row of rows){const fromTable=row.name;if(!fromTable)continue;if(!/^[a-z_][\w]*$/i.test(fromTable))continue;const fkRows=await db.unsafe(`PRAGMA foreign_key_list("${fromTable}")`);for(const fk of Array.isArray(fkRows)?fkRows:[]){const r=fk;if((r.seq??0)!==0)continue;if(!r.from||!r.to||!r.table)continue;fks.push({fromTable,fromColumn:r.from,toTable:r.table,toColumn:r.to})}}return fks}async function getMysqlLiveFKs(db){const rows=await db.unsafe(`
|
|
170
2
|
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
171
3
|
FROM information_schema.KEY_COLUMN_USAGE
|
|
172
4
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
173
5
|
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
174
|
-
`);
|
|
175
|
-
return (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
176
|
-
fromTable: String(row.TABLE_NAME ?? row.table_name ?? ""),
|
|
177
|
-
fromColumn: String(row.COLUMN_NAME ?? row.column_name ?? ""),
|
|
178
|
-
toTable: String(row.REFERENCED_TABLE_NAME ?? row.referenced_table_name ?? ""),
|
|
179
|
-
toColumn: String(row.REFERENCED_COLUMN_NAME ?? row.referenced_column_name ?? "")
|
|
180
|
-
})).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
|
|
181
|
-
}
|
|
182
|
-
async function getPostgresLiveFKs(db) {
|
|
183
|
-
const rows = await db.unsafe(`
|
|
6
|
+
`);return(Array.isArray(rows)?rows:[]).map((row)=>({fromTable:String(row.TABLE_NAME??row.table_name??""),fromColumn:String(row.COLUMN_NAME??row.column_name??""),toTable:String(row.REFERENCED_TABLE_NAME??row.referenced_table_name??""),toColumn:String(row.REFERENCED_COLUMN_NAME??row.referenced_column_name??"")})).filter((fk)=>fk.fromTable&&fk.fromColumn&&fk.toTable&&fk.toColumn)}async function getPostgresLiveFKs(db){const rows=await db.unsafe(`
|
|
184
7
|
SELECT
|
|
185
8
|
kcu.table_name AS from_table,
|
|
186
9
|
kcu.column_name AS from_column,
|
|
@@ -194,11 +17,4 @@ async function getPostgresLiveFKs(db) {
|
|
|
194
17
|
ON ccu.constraint_name = rc.constraint_name
|
|
195
18
|
AND ccu.constraint_schema = rc.constraint_schema
|
|
196
19
|
WHERE rc.constraint_schema = 'public'
|
|
197
|
-
`);
|
|
198
|
-
return (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
199
|
-
fromTable: String(row.from_table ?? ""),
|
|
200
|
-
fromColumn: String(row.from_column ?? ""),
|
|
201
|
-
toTable: String(row.to_table ?? ""),
|
|
202
|
-
toColumn: String(row.to_column ?? "")
|
|
203
|
-
})).filter((fk) => fk.fromTable && fk.fromColumn && fk.toTable && fk.toColumn);
|
|
204
|
-
}
|
|
20
|
+
`);return(Array.isArray(rows)?rows:[]).map((row)=>({fromTable:String(row.from_table??""),fromColumn:String(row.from_column??""),toTable:String(row.to_table??""),toColumn:String(row.to_column??"")})).filter((fk)=>fk.fromTable&&fk.fromColumn&&fk.toTable&&fk.toColumn)}
|
package/dist/index.js
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
Database,
|
|
3
|
-
createDatabase,
|
|
4
|
-
createMysqlDatabase,
|
|
5
|
-
createPostgresDatabase,
|
|
6
|
-
createSqliteDatabase
|
|
7
|
-
} from "./database";
|
|
8
|
-
export {
|
|
9
|
-
detectDriver,
|
|
10
|
-
driverDefaults,
|
|
11
|
-
getConfigFromEnv,
|
|
12
|
-
getConnectionString,
|
|
13
|
-
mergeWithDefaults,
|
|
14
|
-
validateDriverConfig
|
|
15
|
-
} from "./driver-config";
|
|
16
|
-
export * from "./utils";
|
|
17
|
-
export * from "./types";
|
|
18
|
-
export * from "./migrations";
|
|
19
|
-
export { setQueryTracker, logQuery } from "./query-logger";
|
|
20
|
-
export { addColumnSafely, backfillInBatches, renameColumnSafely } from "./safe-migrations";
|
|
21
|
-
export * from "./seeder";
|
|
22
|
-
export * from "./drivers";
|
|
23
|
-
export * from "./custom";
|
|
24
|
-
export * from "./auth-tables";
|
|
25
|
-
export * from "./uuid-columns";
|
|
26
|
-
export * from "./managed-columns";
|
|
27
|
-
export * from "./relation-columns";
|
|
28
|
-
export { migrateNotificationTables } from "./notification-tables";
|
|
29
|
-
export { migrateRbacTables } from "./rbac-tables";
|
|
30
|
-
export * from "./trait-tables";
|
|
31
|
-
export * from "./datetime-columns";
|
|
32
|
-
export * from "./dialect";
|
|
33
|
-
export * from "./replicas";
|
|
34
|
-
export * from "./ddl-constraints";
|
|
35
|
-
export * from "./vschema";
|
|
36
|
-
export * from "./sql-helpers";
|
|
37
|
-
export * from "./defaults";
|
|
38
|
-
export * from "./migration-dialect";
|
|
39
|
-
export * from "./migration-ledger";
|
|
40
|
-
export * from "./model-sources";
|
|
41
|
-
export * from "./ensure-database";
|
|
42
|
-
export { auditForeignKeys, findFkOrphans, getDeclaredFKs, getLiveFKs } from "./fk-audit";
|
|
43
|
-
export { auditUniqueIndexes, getDeclaredUniques, getLiveUniqueIndexes } from "./unique-audit";
|
|
44
|
-
export {
|
|
45
|
-
__flushAfterCommitNow,
|
|
46
|
-
__pendingAfterCommitCount,
|
|
47
|
-
enqueueAfterCommit,
|
|
48
|
-
isInTransaction,
|
|
49
|
-
runInTransactionScope
|
|
50
|
-
} from "./transaction-context";
|
|
51
|
-
export {
|
|
52
|
-
createQueryBuilder,
|
|
53
|
-
setConfig
|
|
54
|
-
} from "@stacksjs/query-builder";
|
|
55
|
-
export {
|
|
56
|
-
createDynamo,
|
|
57
|
-
dynamo,
|
|
58
|
-
EntityQueryBuilder,
|
|
59
|
-
generateKeyPattern,
|
|
60
|
-
parseKeyPattern,
|
|
61
|
-
buildKey,
|
|
62
|
-
marshall,
|
|
63
|
-
unmarshall
|
|
64
|
-
} from "./drivers/dynamodb";
|
|
1
|
+
export{Database,createDatabase,createMysqlDatabase,createPostgresDatabase,createSqliteDatabase}from"./database";export{detectDriver,driverDefaults,getConfigFromEnv,getConnectionString,mergeWithDefaults,validateDriverConfig}from"./driver-config";export*from"./utils";export*from"./types";export*from"./migrations";export{setQueryTracker,logQuery}from"./query-logger";export{addColumnSafely,backfillInBatches,renameColumnSafely}from"./safe-migrations";export*from"./seeder";export*from"./drivers";export*from"./custom";export*from"./auth-tables";export*from"./uuid-columns";export*from"./managed-columns";export*from"./relation-columns";export{migrateNotificationTables}from"./notification-tables";export{migrateRbacTables}from"./rbac-tables";export*from"./trait-tables";export*from"./datetime-columns";export*from"./dialect";export*from"./replicas";export*from"./ddl-constraints";export*from"./vschema";export*from"./sql-helpers";export*from"./defaults";export*from"./migration-dialect";export*from"./migration-ledger";export*from"./model-sources";export*from"./ensure-database";export{auditForeignKeys,findFkOrphans,getDeclaredFKs,getLiveFKs}from"./fk-audit";export{auditUniqueIndexes,getDeclaredUniques,getLiveUniqueIndexes}from"./unique-audit";export{__flushAfterCommitNow,__pendingAfterCommitCount,enqueueAfterCommit,isInTransaction,runInTransactionScope}from"./transaction-context";export{createQueryBuilder,setConfig}from"@stacksjs/query-builder";export{createDynamo,dynamo,EntityQueryBuilder,generateKeyPattern,parseKeyPattern,buildKey,marshall,unmarshall}from"./drivers/dynamodb";
|
package/dist/managed-columns.js
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
export const USERS_GUARANTEED_COLUMNS = [
|
|
2
|
-
"email_verified_at",
|
|
3
|
-
"password_changed_at",
|
|
4
|
-
"two_factor_secret",
|
|
5
|
-
"two_factor_enabled",
|
|
6
|
-
"two_factor_last_used_step",
|
|
7
|
-
"stripe_id"
|
|
8
|
-
];
|
|
9
|
-
export async function frameworkManagedColumns() {
|
|
10
|
-
const managed = new Map;
|
|
11
|
-
managed.set("users", new Set(USERS_GUARANTEED_COLUMNS));
|
|
12
|
-
const add = (table, column) => {
|
|
13
|
-
const columns = managed.get(table) ?? new Set;
|
|
14
|
-
columns.add(column);
|
|
15
|
-
managed.set(table, columns);
|
|
16
|
-
};
|
|
17
|
-
try {
|
|
18
|
-
const { findUuidTables } = await import("./uuid-columns");
|
|
19
|
-
for (const table of await findUuidTables())
|
|
20
|
-
add(table, "uuid");
|
|
21
|
-
} catch {}
|
|
22
|
-
try {
|
|
23
|
-
const { findRelationForeignKeys } = await import("./relation-columns");
|
|
24
|
-
for (const [table, columns] of await findRelationForeignKeys())
|
|
25
|
-
for (const column of columns)
|
|
26
|
-
add(table, column);
|
|
27
|
-
} catch {}
|
|
28
|
-
return managed;
|
|
29
|
-
}
|
|
30
|
-
export function isManagedColumnDrop(op, managed) {
|
|
31
|
-
return op.kind === "drop_column" && op.column != null && (managed.get(op.table)?.has(op.column) ?? !1);
|
|
32
|
-
}
|
|
33
|
-
export function withoutManagedColumnDrops(operations, managed) {
|
|
34
|
-
const suppressed = new Set(operations.filter((op) => isManagedColumnDrop(op, managed)).map((op) => op.sql ? normalizeSql(op.sql) : "").filter((sql) => sql.length > 0));
|
|
35
|
-
return operations.filter((op) => {
|
|
36
|
-
if (isManagedColumnDrop(op, managed))
|
|
37
|
-
return !1;
|
|
38
|
-
return !(op.sql && suppressed.has(normalizeSql(op.sql)));
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function normalizeSql(sql) {
|
|
42
|
-
return sql.trim().replace(/\s+/g, " ").replace(/;+\s*$/, "");
|
|
43
|
-
}
|
|
44
|
-
const DROP_COLUMN_RE = /ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;
|
|
45
|
-
export function withoutManagedColumnDropSql(statements, managed, operations = []) {
|
|
46
|
-
const protectedSql = new Set(operations.filter((op) => isManagedColumnDrop(op, managed)).map((op) => normalizeSql(op.sql))), removed = [];
|
|
47
|
-
return { statements: statements.filter((statement) => {
|
|
48
|
-
if (protectedSql.has(normalizeSql(statement))) {
|
|
49
|
-
removed.push(statement);
|
|
50
|
-
return !1;
|
|
51
|
-
}
|
|
52
|
-
const match = statement.match(DROP_COLUMN_RE);
|
|
53
|
-
if (match?.[1] && match[2] && (managed.get(match[1])?.has(match[2]) ?? !1)) {
|
|
54
|
-
removed.push(statement);
|
|
55
|
-
return !1;
|
|
56
|
-
}
|
|
57
|
-
return !0;
|
|
58
|
-
}), removed };
|
|
59
|
-
}
|
|
1
|
+
export const USERS_GUARANTEED_COLUMNS=["email_verified_at","password_changed_at","two_factor_secret","two_factor_enabled","two_factor_last_used_step","stripe_id"];export async function frameworkManagedColumns(){const managed=new Map;managed.set("users",new Set(USERS_GUARANTEED_COLUMNS));const add=(table,column)=>{const columns=managed.get(table)??new Set;columns.add(column);managed.set(table,columns)};try{const{findUuidTables}=await import("./uuid-columns");for(const table of await findUuidTables())add(table,"uuid")}catch{}try{const{findRelationForeignKeys}=await import("./relation-columns");for(const[table,columns]of await findRelationForeignKeys())for(const column of columns)add(table,column)}catch{}return managed}export function isManagedColumnDrop(op,managed){return op.kind==="drop_column"&&op.column!=null&&(managed.get(op.table)?.has(op.column)??!1)}export function withoutManagedColumnDrops(operations,managed){const suppressed=new Set(operations.filter((op)=>isManagedColumnDrop(op,managed)).map((op)=>op.sql?normalizeSql(op.sql):"").filter((sql)=>sql.length>0));return operations.filter((op)=>{if(isManagedColumnDrop(op,managed))return!1;return!(op.sql&&suppressed.has(normalizeSql(op.sql)))})}function normalizeSql(sql){return sql.trim().replace(/\s+/g," ").replace(/;+\s*$/,"")}const DROP_COLUMN_RE=/ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?["'`]?(\w+)["'`]?/i;export function withoutManagedColumnDropSql(statements,managed,operations=[]){const protectedSql=new Set(operations.filter((op)=>isManagedColumnDrop(op,managed)).map((op)=>normalizeSql(op.sql))),removed=[];return{statements:statements.filter((statement)=>{if(protectedSql.has(normalizeSql(statement))){removed.push(statement);return!1}const match=statement.match(DROP_COLUMN_RE);if(match?.[1]&&match[2]&&(managed.get(match[1])?.has(match[2])??!1)){removed.push(statement);return!1}return!0}),removed}}
|
|
@@ -1,107 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const MARKERS = [
|
|
4
|
-
|
|
5
|
-
{ dialect: "sqlite", pattern: /\bWITHOUT\s+ROWID\b/i, label: "WITHOUT ROWID" },
|
|
6
|
-
{ dialect: "sqlite", pattern: /\bPRAGMA\b/i, label: "PRAGMA" },
|
|
7
|
-
{ dialect: "mysql", pattern: /\bAUTO_INCREMENT\b/i, label: "AUTO_INCREMENT" },
|
|
8
|
-
{ dialect: "mysql", pattern: /\bENGINE\s*=/i, label: "ENGINE=" },
|
|
9
|
-
{ dialect: "postgres", pattern: /\bBIGSERIAL\b/i, label: "BIGSERIAL" },
|
|
10
|
-
{ dialect: "postgres", pattern: /\bSERIAL\b/i, label: "SERIAL" },
|
|
11
|
-
{ dialect: "postgres", pattern: /\bCREATE\s+TYPE\b/i, label: "CREATE TYPE" },
|
|
12
|
-
{ dialect: "postgres", pattern: /\bGENERATED\s+(?:ALWAYS|BY\s+DEFAULT)\s+AS\s+IDENTITY\b/i, label: "GENERATED AS IDENTITY" }
|
|
13
|
-
];
|
|
14
|
-
export function stripSqlNoise(sql) {
|
|
15
|
-
let out = "", i = 0;
|
|
16
|
-
const blank = (text) => text.replace(/[^\n]/g, " ");
|
|
17
|
-
while (i < sql.length) {
|
|
18
|
-
const rest = sql.slice(i), line = rest.match(/^--[^\n]*/);
|
|
19
|
-
if (line) {
|
|
20
|
-
out += blank(line[0]);
|
|
21
|
-
i += line[0].length;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (rest.startsWith("/*")) {
|
|
25
|
-
const end = rest.indexOf("*/"), chunk = end === -1 ? rest : rest.slice(0, end + 2);
|
|
26
|
-
out += blank(chunk);
|
|
27
|
-
i += chunk.length;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
const quote = rest[0];
|
|
31
|
-
if (quote === '"' || quote === "'" || quote === "`") {
|
|
32
|
-
let j = 1;
|
|
33
|
-
while (j < rest.length && rest[j] !== quote)
|
|
34
|
-
j++;
|
|
35
|
-
const chunk = rest.slice(0, Math.min(j + 1, rest.length));
|
|
36
|
-
out += blank(chunk);
|
|
37
|
-
i += chunk.length;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
out += sql[i];
|
|
41
|
-
i += 1;
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
44
|
-
}
|
|
45
|
-
export function classifyMigrationSql(sql, file) {
|
|
46
|
-
const lines = stripSqlNoise(sql).split(`
|
|
47
|
-
`), rawLines = sql.split(`
|
|
48
|
-
`), found = [];
|
|
49
|
-
for (let index = 0;index < lines.length; index++)
|
|
50
|
-
for (const { dialect, pattern, label } of MARKERS)
|
|
51
|
-
if (pattern.test(lines[index] ?? ""))
|
|
52
|
-
found.push({
|
|
53
|
-
dialect,
|
|
54
|
-
marker: label,
|
|
55
|
-
file,
|
|
56
|
-
line: index + 1,
|
|
57
|
-
snippet: (rawLines[index] ?? "").trim().slice(0, 120)
|
|
58
|
-
});
|
|
59
|
-
return found;
|
|
60
|
-
}
|
|
61
|
-
export function auditMigrationCorpus(options) {
|
|
62
|
-
const { dir, target } = options;
|
|
63
|
-
if (!existsSync(dir))
|
|
64
|
-
return { total: 0, inferred: null, incompatible: [], empty: !0 };
|
|
65
|
-
let files;
|
|
66
|
-
try {
|
|
67
|
-
files = readdirSync(dir).filter((f) => f.endsWith(".sql")).sort();
|
|
68
|
-
} catch {
|
|
69
|
-
return { total: 0, inferred: null, incompatible: [], empty: !0 };
|
|
70
|
-
}
|
|
71
|
-
const all = [];
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
let sql;
|
|
74
|
-
try {
|
|
75
|
-
sql = readFileSync(join(dir, file), "utf8");
|
|
76
|
-
} catch {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
all.push(...classifyMigrationSql(sql, file));
|
|
80
|
-
}
|
|
81
|
-
const dialects = new Set(all.map((m) => m.dialect));
|
|
82
|
-
return {
|
|
83
|
-
total: files.length,
|
|
84
|
-
inferred: dialects.size === 1 ? [...dialects][0] ?? null : null,
|
|
85
|
-
incompatible: all.filter((m) => m.dialect !== target),
|
|
86
|
-
empty: files.length === 0
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
export const DIALECT_OVERRIDE_ENV = "STACKS_ALLOW_DIALECT_MISMATCH";
|
|
90
|
-
export function formatMigrationDialectError(audit, target, dir) {
|
|
91
|
-
const affected = new Set(audit.incompatible.map((m) => m.file)), sample = audit.incompatible.slice(0, 3), inferred = audit.inferred ? `${audit.inferred}-flavoured` : "written for a different database";
|
|
92
|
-
return [
|
|
93
|
-
`The migration files in ${dir} are ${inferred} and cannot run on ${target}.`,
|
|
94
|
-
`${affected.size} of ${audit.total} files use SQL that ${target} does not accept. For example:`,
|
|
95
|
-
...sample.map((m) => ` ${m.file}:${m.line} ${m.marker}`),
|
|
96
|
-
"",
|
|
97
|
-
"Nothing was migrated, so the database is unchanged.",
|
|
98
|
-
"",
|
|
99
|
-
"Stacks ships one set of migration files, and they are emitted for a single database.",
|
|
100
|
-
"Regenerate them from your models:",
|
|
101
|
-
` ./buddy migrate:regenerate ${target}`,
|
|
102
|
-
"or point DB_CONNECTION back at the database they were written for.",
|
|
103
|
-
"",
|
|
104
|
-
`If you know this corpus is correct, re-run with ${DIALECT_OVERRIDE_ENV}=1 to proceed anyway.`
|
|
105
|
-
].join(`
|
|
106
|
-
`);
|
|
107
|
-
}
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{join}from"node:path";const MARKERS=[{dialect:"sqlite",pattern:/\bAUTOINCREMENT\b/i,label:"AUTOINCREMENT"},{dialect:"sqlite",pattern:/\bWITHOUT\s+ROWID\b/i,label:"WITHOUT ROWID"},{dialect:"sqlite",pattern:/\bPRAGMA\b/i,label:"PRAGMA"},{dialect:"mysql",pattern:/\bAUTO_INCREMENT\b/i,label:"AUTO_INCREMENT"},{dialect:"mysql",pattern:/\bENGINE\s*=/i,label:"ENGINE="},{dialect:"postgres",pattern:/\bBIGSERIAL\b/i,label:"BIGSERIAL"},{dialect:"postgres",pattern:/\bSERIAL\b/i,label:"SERIAL"},{dialect:"postgres",pattern:/\bCREATE\s+TYPE\b/i,label:"CREATE TYPE"},{dialect:"postgres",pattern:/\bGENERATED\s+(?:ALWAYS|BY\s+DEFAULT)\s+AS\s+IDENTITY\b/i,label:"GENERATED AS IDENTITY"}];export function stripSqlNoise(sql){let out="",i=0;const blank=(text)=>text.replace(/[^\n]/g," ");while(i<sql.length){const rest=sql.slice(i),line=rest.match(/^--[^\n]*/);if(line){out+=blank(line[0]);i+=line[0].length;continue}if(rest.startsWith("/*")){const end=rest.indexOf("*/"),chunk=end===-1?rest:rest.slice(0,end+2);out+=blank(chunk);i+=chunk.length;continue}const quote=rest[0];if(quote==='"'||quote==="'"||quote==="`"){let j=1;while(j<rest.length&&rest[j]!==quote)j++;const chunk=rest.slice(0,Math.min(j+1,rest.length));out+=blank(chunk);i+=chunk.length;continue}out+=sql[i];i+=1}return out}export function classifyMigrationSql(sql,file){const lines=stripSqlNoise(sql).split(`
|
|
2
|
+
`),rawLines=sql.split(`
|
|
3
|
+
`),found=[];for(let index=0;index<lines.length;index++)for(const{dialect,pattern,label}of MARKERS)if(pattern.test(lines[index]??""))found.push({dialect,marker:label,file,line:index+1,snippet:(rawLines[index]??"").trim().slice(0,120)});return found}export function auditMigrationCorpus(options){const{dir,target}=options;if(!existsSync(dir))return{total:0,inferred:null,incompatible:[],empty:!0};let files;try{files=readdirSync(dir).filter((f)=>f.endsWith(".sql")).sort()}catch{return{total:0,inferred:null,incompatible:[],empty:!0}}const all=[];for(const file of files){let sql;try{sql=readFileSync(join(dir,file),"utf8")}catch{continue}all.push(...classifyMigrationSql(sql,file))}const dialects=new Set(all.map((m)=>m.dialect));return{total:files.length,inferred:dialects.size===1?[...dialects][0]??null:null,incompatible:all.filter((m)=>m.dialect!==target),empty:files.length===0}}export const DIALECT_OVERRIDE_ENV="STACKS_ALLOW_DIALECT_MISMATCH";export function formatMigrationDialectError(audit,target,dir){const affected=new Set(audit.incompatible.map((m)=>m.file)),sample=audit.incompatible.slice(0,3),inferred=audit.inferred?`${audit.inferred}-flavoured`:"written for a different database";return[`The migration files in ${dir} are ${inferred} and cannot run on ${target}.`,`${affected.size} of ${audit.total} files use SQL that ${target} does not accept. For example:`,...sample.map((m)=>` ${m.file}:${m.line} ${m.marker}`),"","Nothing was migrated, so the database is unchanged.","","Stacks ships one set of migration files, and they are emitted for a single database.","Regenerate them from your models:",` ./buddy migrate:regenerate ${target}`,"or point DB_CONNECTION back at the database they were written for.","",`If you know this corpus is correct, re-run with ${DIALECT_OVERRIDE_ENV}=1 to proceed anyway.`].join(`
|
|
4
|
+
`)}
|