@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.
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 +2 -79
  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
package/dist/fk-audit.js CHANGED
@@ -1,186 +1,9 @@
1
- import { existsSync } from "node:fs";
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";
@@ -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 { existsSync, readdirSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- const MARKERS = [
4
- { dialect: "sqlite", pattern: /\bAUTOINCREMENT\b/i, label: "AUTOINCREMENT" },
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
+ `)}