@bobbykim/manguito-cms-db 0.1.0
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/index.d.mts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +770 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +727 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
// src/adapters/postgres.ts
|
|
5
|
+
import { drizzle as drizzleNode } from "drizzle-orm/node-postgres";
|
|
6
|
+
import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";
|
|
7
|
+
import { neon } from "@neondatabase/serverless";
|
|
8
|
+
import { Pool } from "pg";
|
|
9
|
+
import { sql } from "drizzle-orm";
|
|
10
|
+
function createPostgresAdapter(options = {}) {
|
|
11
|
+
const url = options.url ?? process.env["DB_URL"];
|
|
12
|
+
if (!url) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"DB_URL_MISSING: No database URL provided. Set the DB_URL environment variable or pass a url option."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (!/^postgres(?:ql)?:\/\//i.test(url)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"DB_URL_INVALID: Database URL must begin with postgres:// or postgresql://"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const isNeon = options.serverless ?? url.includes("neon.tech");
|
|
23
|
+
const poolConfig = {
|
|
24
|
+
max: options.pool?.max ?? 10,
|
|
25
|
+
idle_timeout: options.pool?.idle_timeout ?? 30,
|
|
26
|
+
connect_timeout: options.pool?.connect_timeout ?? 10
|
|
27
|
+
};
|
|
28
|
+
let db = null;
|
|
29
|
+
let pool = null;
|
|
30
|
+
let connected = false;
|
|
31
|
+
return {
|
|
32
|
+
type: "postgres",
|
|
33
|
+
async connect() {
|
|
34
|
+
if (isNeon) {
|
|
35
|
+
const sqlClient = neon(url);
|
|
36
|
+
db = drizzleNeon(sqlClient);
|
|
37
|
+
} else {
|
|
38
|
+
pool = new Pool({
|
|
39
|
+
connectionString: url,
|
|
40
|
+
max: poolConfig.max,
|
|
41
|
+
idleTimeoutMillis: poolConfig.idle_timeout * 1e3,
|
|
42
|
+
connectionTimeoutMillis: poolConfig.connect_timeout * 1e3
|
|
43
|
+
});
|
|
44
|
+
const client = await pool.connect();
|
|
45
|
+
client.release();
|
|
46
|
+
db = drizzleNode(pool);
|
|
47
|
+
}
|
|
48
|
+
connected = true;
|
|
49
|
+
},
|
|
50
|
+
async disconnect() {
|
|
51
|
+
if (pool) {
|
|
52
|
+
await pool.end();
|
|
53
|
+
pool = null;
|
|
54
|
+
}
|
|
55
|
+
db = null;
|
|
56
|
+
connected = false;
|
|
57
|
+
},
|
|
58
|
+
isConnected() {
|
|
59
|
+
return connected;
|
|
60
|
+
},
|
|
61
|
+
getDb() {
|
|
62
|
+
if (!db) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"DB not connected \u2014 call connect() before accessing the database."
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return db;
|
|
68
|
+
},
|
|
69
|
+
async getTableNames() {
|
|
70
|
+
if (!db) throw new Error("DB not connected");
|
|
71
|
+
const result = await db.execute(
|
|
72
|
+
sql`SELECT table_name
|
|
73
|
+
FROM information_schema.tables
|
|
74
|
+
WHERE table_schema = 'public'
|
|
75
|
+
ORDER BY table_name`
|
|
76
|
+
);
|
|
77
|
+
return result.rows.map((r) => r.table_name);
|
|
78
|
+
},
|
|
79
|
+
async tableExists(name) {
|
|
80
|
+
if (!db) throw new Error("DB not connected");
|
|
81
|
+
const result = await db.execute(
|
|
82
|
+
sql`SELECT 1
|
|
83
|
+
FROM information_schema.tables
|
|
84
|
+
WHERE table_schema = 'public'
|
|
85
|
+
AND table_name = ${name}
|
|
86
|
+
LIMIT 1`
|
|
87
|
+
);
|
|
88
|
+
return result.rows.length > 0;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/codegen/index.ts
|
|
94
|
+
function generateFileHeader() {
|
|
95
|
+
return "import * as s from 'drizzle-orm/pg-core'\nimport { sql } from 'drizzle-orm'";
|
|
96
|
+
}
|
|
97
|
+
var SYSTEM_TABLES_SECTION = `// \u2500\u2500\u2500 System Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
98
|
+
export const media = s.pgTable('media', {
|
|
99
|
+
id: s.uuid('id').primaryKey().defaultRandom(),
|
|
100
|
+
type: s.varchar('type', { length: 50 }).notNull(),
|
|
101
|
+
url: s.varchar('url', { length: 2048 }).notNull(),
|
|
102
|
+
mime_type: s.varchar('mime_type', { length: 255 }).notNull(),
|
|
103
|
+
alt: s.varchar('alt', { length: 255 }),
|
|
104
|
+
file_size: s.integer('file_size').notNull(),
|
|
105
|
+
width: s.integer('width'),
|
|
106
|
+
height: s.integer('height'),
|
|
107
|
+
duration: s.integer('duration'),
|
|
108
|
+
reference_count: s.integer('reference_count').notNull().default(0),
|
|
109
|
+
created_at: s.timestamp('created_at').defaultNow().notNull(),
|
|
110
|
+
updated_at: s.timestamp('updated_at').defaultNow().notNull(),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
export const base_paths = s.pgTable('base_paths', {
|
|
114
|
+
id: s.uuid('id').primaryKey().defaultRandom(),
|
|
115
|
+
name: s.varchar('name', { length: 255 }).notNull().unique(),
|
|
116
|
+
path: s.varchar('path', { length: 1024 }).notNull().unique(),
|
|
117
|
+
created_at: s.timestamp('created_at').defaultNow().notNull(),
|
|
118
|
+
updated_at: s.timestamp('updated_at').defaultNow().notNull(),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export const roles = s.pgTable('roles', {
|
|
122
|
+
id: s.uuid('id').primaryKey().defaultRandom(),
|
|
123
|
+
name: s.varchar('name', { length: 255 }).notNull().unique(),
|
|
124
|
+
label: s.varchar('label', { length: 255 }).notNull(),
|
|
125
|
+
is_system: s.boolean('is_system').notNull().default(false),
|
|
126
|
+
hierarchy_level: s.integer('hierarchy_level').notNull().unique(),
|
|
127
|
+
permissions: s.text('permissions').array().notNull(),
|
|
128
|
+
created_at: s.timestamp('created_at').defaultNow().notNull(),
|
|
129
|
+
updated_at: s.timestamp('updated_at').defaultNow().notNull(),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
export const users = s.pgTable('users', {
|
|
133
|
+
id: s.uuid('id').primaryKey().defaultRandom(),
|
|
134
|
+
email: s.varchar('email', { length: 255 }).notNull().unique(),
|
|
135
|
+
password_hash: s.varchar('password_hash', { length: 255 }).notNull(),
|
|
136
|
+
role_id: s.uuid('role_id')
|
|
137
|
+
.notNull()
|
|
138
|
+
.references(() => roles.id, { onDelete: 'restrict' }),
|
|
139
|
+
token_version: s.integer('token_version').notNull().default(0),
|
|
140
|
+
must_change_password: s.boolean('must_change_password').notNull().default(false),
|
|
141
|
+
created_at: s.timestamp('created_at').defaultNow().notNull(),
|
|
142
|
+
updated_at: s.timestamp('updated_at').defaultNow().notNull(),
|
|
143
|
+
})`;
|
|
144
|
+
function generateSystemFieldColumn(field) {
|
|
145
|
+
const { name, db_type, primary_key, default: defaultVal, nullable } = field;
|
|
146
|
+
const col = buildSystemBase(name, db_type, primary_key, defaultVal);
|
|
147
|
+
return primary_key || nullable ? col : col + ".notNull()";
|
|
148
|
+
}
|
|
149
|
+
function buildSystemBase(name, db_type, primary_key, defaultVal) {
|
|
150
|
+
switch (db_type) {
|
|
151
|
+
case "uuid":
|
|
152
|
+
return primary_key ? `s.uuid('${name}').primaryKey().defaultRandom()` : `s.uuid('${name}')`;
|
|
153
|
+
case "varchar":
|
|
154
|
+
return `s.varchar('${name}')`;
|
|
155
|
+
case "boolean": {
|
|
156
|
+
const suffix = defaultVal === "false" ? ".default(false)" : defaultVal === "true" ? ".default(true)" : "";
|
|
157
|
+
return `s.boolean('${name}')${suffix}`;
|
|
158
|
+
}
|
|
159
|
+
case "timestamp":
|
|
160
|
+
return defaultVal === "now()" ? `s.timestamp('${name}').defaultNow()` : `s.timestamp('${name}')`;
|
|
161
|
+
case "integer":
|
|
162
|
+
return defaultVal !== void 0 ? `s.integer('${name}').default(${Number(defaultVal)})` : `s.integer('${name}')`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function mapOnDelete(on_delete) {
|
|
166
|
+
if (on_delete === "CASCADE") return "cascade";
|
|
167
|
+
if (on_delete === "SET NULL") return "set null";
|
|
168
|
+
return "restrict";
|
|
169
|
+
}
|
|
170
|
+
function generateFieldColumn(field) {
|
|
171
|
+
if (field.field_type === "paragraph" || field.db_column === null) return null;
|
|
172
|
+
const col = field.db_column;
|
|
173
|
+
if (col.junction) return null;
|
|
174
|
+
const n = col.column_name;
|
|
175
|
+
const isEnum = col.check_constraint !== void 0 && col.check_constraint.length > 0;
|
|
176
|
+
let expr = buildFieldBase(n, col.column_type, isEnum, field.validation.limit);
|
|
177
|
+
if (!col.nullable) expr += ".notNull()";
|
|
178
|
+
if (col.foreign_key) {
|
|
179
|
+
const onDelete = mapOnDelete(col.foreign_key.on_delete);
|
|
180
|
+
expr += `.references(() => ${col.foreign_key.table}.${col.foreign_key.column}, { onDelete: '${onDelete}' })`;
|
|
181
|
+
}
|
|
182
|
+
return expr;
|
|
183
|
+
}
|
|
184
|
+
function buildFieldBase(name, column_type, isEnum, limit) {
|
|
185
|
+
switch (column_type) {
|
|
186
|
+
case "varchar":
|
|
187
|
+
return isEnum ? `s.varchar('${name}')` : `s.varchar('${name}', { length: ${limit ?? 255} })`;
|
|
188
|
+
case "text":
|
|
189
|
+
return `s.text('${name}')`;
|
|
190
|
+
case "integer":
|
|
191
|
+
return `s.integer('${name}')`;
|
|
192
|
+
case "decimal":
|
|
193
|
+
return `s.decimal('${name}', { precision: 10, scale: 4 })`;
|
|
194
|
+
case "boolean":
|
|
195
|
+
return `s.boolean('${name}')`;
|
|
196
|
+
case "timestamp":
|
|
197
|
+
return `s.timestamp('${name}')`;
|
|
198
|
+
case "uuid":
|
|
199
|
+
return `s.uuid('${name}')`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function tableNameToVarName(tableName) {
|
|
203
|
+
return tableName.replace(/-/g, "_");
|
|
204
|
+
}
|
|
205
|
+
function generateTableDef(tableName, varName, systemFields, fields) {
|
|
206
|
+
const colLines = [];
|
|
207
|
+
for (const f of systemFields) {
|
|
208
|
+
colLines.push(`${f.name}: ${generateSystemFieldColumn(f)},`);
|
|
209
|
+
}
|
|
210
|
+
for (const f of fields) {
|
|
211
|
+
const colExpr = generateFieldColumn(f);
|
|
212
|
+
if (colExpr === null) continue;
|
|
213
|
+
colLines.push(`${f.db_column.column_name}: ${colExpr},`);
|
|
214
|
+
}
|
|
215
|
+
const enumFields = fields.filter(
|
|
216
|
+
(f) => (f.db_column?.check_constraint?.length ?? 0) > 0
|
|
217
|
+
);
|
|
218
|
+
if (enumFields.length === 0) {
|
|
219
|
+
const colBlock2 = colLines.map((l) => ` ${l}`).join("\n");
|
|
220
|
+
return `export const ${varName} = s.pgTable('${tableName}', {
|
|
221
|
+
${colBlock2}
|
|
222
|
+
})`;
|
|
223
|
+
}
|
|
224
|
+
const colBlock = colLines.map((l) => ` ${l}`).join("\n");
|
|
225
|
+
const constraints = enumFields.map((f) => {
|
|
226
|
+
const col = f.db_column;
|
|
227
|
+
const values = col.check_constraint.map((v) => `'${v}'`).join(", ");
|
|
228
|
+
const cname = `${col.column_name}_check`;
|
|
229
|
+
return [
|
|
230
|
+
` ${cname}: s.check(`,
|
|
231
|
+
` '${cname}',`,
|
|
232
|
+
` sql\`\${table.${col.column_name}} IN (${values})\``,
|
|
233
|
+
` ),`
|
|
234
|
+
].join("\n");
|
|
235
|
+
}).join("\n");
|
|
236
|
+
return [
|
|
237
|
+
`export const ${varName} = s.pgTable(`,
|
|
238
|
+
` '${tableName}',`,
|
|
239
|
+
` {`,
|
|
240
|
+
colBlock,
|
|
241
|
+
` },`,
|
|
242
|
+
` (table) => ({`,
|
|
243
|
+
constraints,
|
|
244
|
+
` })`,
|
|
245
|
+
`)`
|
|
246
|
+
].join("\n");
|
|
247
|
+
}
|
|
248
|
+
function orderParagraphTypes(paragraphs) {
|
|
249
|
+
const sorted = [];
|
|
250
|
+
const visited = /* @__PURE__ */ new Set();
|
|
251
|
+
function visit(name) {
|
|
252
|
+
if (visited.has(name)) return;
|
|
253
|
+
visited.add(name);
|
|
254
|
+
const paragraph = paragraphs[name];
|
|
255
|
+
if (!paragraph) return;
|
|
256
|
+
for (const field of paragraph.fields) {
|
|
257
|
+
if (field.field_type === "paragraph") {
|
|
258
|
+
const ref = field.ui_component.ref;
|
|
259
|
+
visit(ref);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
sorted.push(paragraph);
|
|
263
|
+
}
|
|
264
|
+
for (const name of Object.keys(paragraphs)) visit(name);
|
|
265
|
+
return sorted;
|
|
266
|
+
}
|
|
267
|
+
function generateJunctionTables(registry) {
|
|
268
|
+
const tables = [];
|
|
269
|
+
for (const ct of Object.values(registry.content_types)) {
|
|
270
|
+
const leftVar = tableNameToVarName(ct.db.table_name);
|
|
271
|
+
for (const jt of ct.db.junction_tables) {
|
|
272
|
+
const varName = tableNameToVarName(jt.table_name);
|
|
273
|
+
const rightVar = tableNameToVarName(jt.right_table);
|
|
274
|
+
const orderLine = jt.order_column ? ` order: s.integer('order').notNull().default(0),` : null;
|
|
275
|
+
const def = [
|
|
276
|
+
`export const ${varName} = s.pgTable(`,
|
|
277
|
+
` '${jt.table_name}',`,
|
|
278
|
+
` {`,
|
|
279
|
+
` ${jt.left_column}: s.uuid('${jt.left_column}')`,
|
|
280
|
+
` .notNull()`,
|
|
281
|
+
` .references(() => ${leftVar}.id, { onDelete: 'cascade' }),`,
|
|
282
|
+
` ${jt.right_column}: s.uuid('${jt.right_column}')`,
|
|
283
|
+
` .notNull()`,
|
|
284
|
+
` .references(() => ${rightVar}.id, { onDelete: 'cascade' }),`,
|
|
285
|
+
...orderLine ? [orderLine] : [],
|
|
286
|
+
` }`,
|
|
287
|
+
`)`
|
|
288
|
+
].join("\n");
|
|
289
|
+
tables.push(def);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (tables.length === 0) return "";
|
|
293
|
+
return "// \u2500\u2500\u2500 Junction Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n" + tables.join("\n\n");
|
|
294
|
+
}
|
|
295
|
+
function generateSchemaFile(registry) {
|
|
296
|
+
const sections = [generateFileHeader(), SYSTEM_TABLES_SECTION];
|
|
297
|
+
const taxonomyTypes = Object.values(registry.taxonomy_types);
|
|
298
|
+
if (taxonomyTypes.length > 0) {
|
|
299
|
+
const header = "// \u2500\u2500\u2500 Taxonomy Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
300
|
+
const defs = taxonomyTypes.map(
|
|
301
|
+
(t) => generateTableDef(
|
|
302
|
+
t.db.table_name,
|
|
303
|
+
tableNameToVarName(t.db.table_name),
|
|
304
|
+
t.system_fields,
|
|
305
|
+
t.fields
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
sections.push(header + "\n" + defs.join("\n\n"));
|
|
309
|
+
}
|
|
310
|
+
const paragraphTypes = orderParagraphTypes(registry.paragraph_types);
|
|
311
|
+
if (paragraphTypes.length > 0) {
|
|
312
|
+
const header = "// \u2500\u2500\u2500 Paragraph Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
313
|
+
const defs = paragraphTypes.map(
|
|
314
|
+
(p) => generateTableDef(
|
|
315
|
+
p.db.table_name,
|
|
316
|
+
tableNameToVarName(p.db.table_name),
|
|
317
|
+
p.system_fields,
|
|
318
|
+
p.fields
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
sections.push(header + "\n" + defs.join("\n\n"));
|
|
322
|
+
}
|
|
323
|
+
const contentTypes = Object.values(registry.content_types);
|
|
324
|
+
if (contentTypes.length > 0) {
|
|
325
|
+
const header = "// \u2500\u2500\u2500 Content Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
326
|
+
const defs = contentTypes.map(
|
|
327
|
+
(c) => generateTableDef(
|
|
328
|
+
c.db.table_name,
|
|
329
|
+
tableNameToVarName(c.db.table_name),
|
|
330
|
+
c.system_fields,
|
|
331
|
+
c.fields
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
sections.push(header + "\n" + defs.join("\n\n"));
|
|
335
|
+
}
|
|
336
|
+
const junctionSection = generateJunctionTables(registry);
|
|
337
|
+
if (junctionSection) sections.push(junctionSection);
|
|
338
|
+
return sections.join("\n\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/seeder/index.ts
|
|
342
|
+
import { and, eq, inArray, sql as sql2 } from "drizzle-orm";
|
|
343
|
+
import * as s from "drizzle-orm/pg-core";
|
|
344
|
+
var base_paths = s.pgTable("base_paths", {
|
|
345
|
+
id: s.uuid("id").primaryKey().defaultRandom(),
|
|
346
|
+
name: s.varchar("name", { length: 255 }).notNull().unique(),
|
|
347
|
+
path: s.varchar("path", { length: 1024 }).notNull().unique(),
|
|
348
|
+
created_at: s.timestamp("created_at").defaultNow().notNull(),
|
|
349
|
+
updated_at: s.timestamp("updated_at").defaultNow().notNull()
|
|
350
|
+
});
|
|
351
|
+
var roles = s.pgTable("roles", {
|
|
352
|
+
id: s.uuid("id").primaryKey().defaultRandom(),
|
|
353
|
+
name: s.varchar("name", { length: 255 }).notNull().unique(),
|
|
354
|
+
label: s.varchar("label", { length: 255 }).notNull(),
|
|
355
|
+
is_system: s.boolean("is_system").notNull().default(false),
|
|
356
|
+
hierarchy_level: s.integer("hierarchy_level").notNull().unique(),
|
|
357
|
+
permissions: s.text("permissions").array().notNull(),
|
|
358
|
+
created_at: s.timestamp("created_at").defaultNow().notNull(),
|
|
359
|
+
updated_at: s.timestamp("updated_at").defaultNow().notNull()
|
|
360
|
+
});
|
|
361
|
+
var users = s.pgTable("users", {
|
|
362
|
+
id: s.uuid("id").primaryKey().defaultRandom(),
|
|
363
|
+
email: s.varchar("email", { length: 255 }).notNull().unique(),
|
|
364
|
+
password_hash: s.varchar("password_hash", { length: 255 }).notNull(),
|
|
365
|
+
role_id: s.uuid("role_id").notNull().references(() => roles.id, { onDelete: "restrict" }),
|
|
366
|
+
token_version: s.integer("token_version").notNull().default(0),
|
|
367
|
+
created_at: s.timestamp("created_at").defaultNow().notNull(),
|
|
368
|
+
updated_at: s.timestamp("updated_at").defaultNow().notNull()
|
|
369
|
+
});
|
|
370
|
+
async function checkBasePathsInUse(db, basePathNames) {
|
|
371
|
+
const rows = await db.select({ id: base_paths.id, name: base_paths.name }).from(base_paths).where(inArray(base_paths.name, basePathNames));
|
|
372
|
+
if (rows.length === 0) return [];
|
|
373
|
+
const ids = rows.map((r) => `'${r.id}'`).join(", ");
|
|
374
|
+
const tablesResult = await db.execute(
|
|
375
|
+
sql2`SELECT table_name
|
|
376
|
+
FROM information_schema.columns
|
|
377
|
+
WHERE table_schema = 'public'
|
|
378
|
+
AND column_name = 'base_path_id'`
|
|
379
|
+
);
|
|
380
|
+
const inUse = [];
|
|
381
|
+
for (const row of tablesResult.rows) {
|
|
382
|
+
const tableName = row.table_name;
|
|
383
|
+
const countResult = await db.execute(
|
|
384
|
+
sql2.raw(
|
|
385
|
+
`SELECT COUNT(*) as count FROM "${tableName}"
|
|
386
|
+
WHERE base_path_id IN (${ids})`
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
const count = parseInt(
|
|
390
|
+
countResult.rows[0].count,
|
|
391
|
+
10
|
|
392
|
+
);
|
|
393
|
+
if (count > 0) inUse.push(tableName);
|
|
394
|
+
}
|
|
395
|
+
return inUse;
|
|
396
|
+
}
|
|
397
|
+
async function seedRoles(db, parsedRoles, dryRun) {
|
|
398
|
+
const existing = await db.select({ name: roles.name }).from(roles);
|
|
399
|
+
const existingNames = new Set(existing.map((r) => r.name));
|
|
400
|
+
const incomingNames = new Set(parsedRoles.roles.map((r) => r.name));
|
|
401
|
+
const toDelete = [...existingNames].filter((n) => !incomingNames.has(n));
|
|
402
|
+
if (toDelete.length > 0) {
|
|
403
|
+
const systemRoles = await db.select({ name: roles.name }).from(roles).where(and(inArray(roles.name, toDelete), eq(roles.is_system, true)));
|
|
404
|
+
if (systemRoles.length > 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`SEEDER_SYSTEM_ROLE: Cannot remove system role(s) [${systemRoles.map((r) => r.name).join(", ")}] from roles.json \u2014 they are marked is_system and are protected from deletion. Re-seed the role with is_system: false first if you intend to remove it.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const affected = await db.select({ id: users.id, email: users.email }).from(users).innerJoin(roles, eq(users.role_id, roles.id)).where(inArray(roles.name, toDelete));
|
|
410
|
+
if (affected.length > 0) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`SEEDER_ROLE_IN_USE: Cannot remove role(s) [${toDelete.join(", ")}] from roles.json \u2014 ${affected.length} user(s) are still assigned to them: ${affected.map((u) => u.email).join(", ")}. Reassign those users to another role before removing it from roles.json.`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (!dryRun) {
|
|
416
|
+
await db.delete(roles).where(inArray(roles.name, toDelete));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const inserted = [];
|
|
420
|
+
const updated = [];
|
|
421
|
+
for (const role of parsedRoles.roles) {
|
|
422
|
+
if (!existingNames.has(role.name)) inserted.push(role.name);
|
|
423
|
+
else updated.push(role.name);
|
|
424
|
+
}
|
|
425
|
+
if (!dryRun) {
|
|
426
|
+
await db.insert(roles).values(
|
|
427
|
+
parsedRoles.roles.map((r) => ({
|
|
428
|
+
name: r.name,
|
|
429
|
+
label: r.label,
|
|
430
|
+
is_system: r.is_system ?? false,
|
|
431
|
+
hierarchy_level: r.hierarchy_level,
|
|
432
|
+
permissions: r.permissions
|
|
433
|
+
}))
|
|
434
|
+
).onConflictDoUpdate({
|
|
435
|
+
target: roles.name,
|
|
436
|
+
set: {
|
|
437
|
+
label: sql2`excluded.label`,
|
|
438
|
+
is_system: sql2`excluded.is_system`,
|
|
439
|
+
hierarchy_level: sql2`excluded.hierarchy_level`,
|
|
440
|
+
permissions: sql2`excluded.permissions`,
|
|
441
|
+
updated_at: sql2`now()`
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
inserted: inserted.length,
|
|
447
|
+
updated: updated.length,
|
|
448
|
+
deleted: toDelete.length
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async function seedBasePaths(db, parsedRoutes, dryRun) {
|
|
452
|
+
const existing = await db.select({ name: base_paths.name }).from(base_paths);
|
|
453
|
+
const existingNames = new Set(existing.map((r) => r.name));
|
|
454
|
+
const incomingNames = new Set(parsedRoutes.base_paths.map((r) => r.name));
|
|
455
|
+
const toDelete = [...existingNames].filter((n) => !incomingNames.has(n));
|
|
456
|
+
if (toDelete.length > 0) {
|
|
457
|
+
const inUse = await checkBasePathsInUse(db, toDelete);
|
|
458
|
+
if (inUse.length > 0) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`SEEDER_BASE_PATH_IN_USE: Cannot remove base path(s) [${toDelete.join(", ")}] from routes.json \u2014 content items are still referencing them (${inUse.join(", ")}). Update or delete that content before removing the base path.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (!dryRun) {
|
|
464
|
+
await db.delete(base_paths).where(inArray(base_paths.name, toDelete));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const inserted = [];
|
|
468
|
+
const updated = [];
|
|
469
|
+
for (const bp of parsedRoutes.base_paths) {
|
|
470
|
+
if (!existingNames.has(bp.name)) inserted.push(bp.name);
|
|
471
|
+
else updated.push(bp.name);
|
|
472
|
+
}
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
await db.insert(base_paths).values(parsedRoutes.base_paths).onConflictDoUpdate({
|
|
475
|
+
target: base_paths.name,
|
|
476
|
+
set: {
|
|
477
|
+
path: sql2`excluded.path`,
|
|
478
|
+
updated_at: sql2`now()`
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
inserted: inserted.length,
|
|
484
|
+
updated: updated.length,
|
|
485
|
+
deleted: toDelete.length
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
async function seedSystemTables(db, registry, options = {}) {
|
|
489
|
+
const dryRun = options.dryRun ?? false;
|
|
490
|
+
const rolesResult = await seedRoles(db, registry.roles, dryRun);
|
|
491
|
+
const basePathsResult = await seedBasePaths(db, registry.routes, dryRun);
|
|
492
|
+
return {
|
|
493
|
+
roles: rolesResult,
|
|
494
|
+
base_paths: basePathsResult
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/migrations/index.ts
|
|
499
|
+
import { execSync, spawnSync } from "child_process";
|
|
500
|
+
import crypto from "crypto";
|
|
501
|
+
import path from "path";
|
|
502
|
+
import fs from "fs/promises";
|
|
503
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
504
|
+
function resolvedPathEnv() {
|
|
505
|
+
const root = process.cwd();
|
|
506
|
+
const localBin = path.join(root, "node_modules", ".bin");
|
|
507
|
+
const existing = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean).map((entry) => path.isAbsolute(entry) ? entry : path.resolve(root, entry));
|
|
508
|
+
return { ...process.env, PATH: [localBin, ...existing].join(path.delimiter) };
|
|
509
|
+
}
|
|
510
|
+
async function runDevMigration(configPath) {
|
|
511
|
+
execSync(`drizzle-kit push --config=${configPath}`, {
|
|
512
|
+
stdio: "inherit",
|
|
513
|
+
cwd: path.dirname(configPath),
|
|
514
|
+
env: resolvedPathEnv()
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
async function generateMigration(configPath, migrationsFolder) {
|
|
518
|
+
let beforeFiles = [];
|
|
519
|
+
try {
|
|
520
|
+
const entries = await fs.readdir(migrationsFolder);
|
|
521
|
+
beforeFiles = entries.filter((f) => f.endsWith(".sql"));
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
execSync(`drizzle-kit generate --config=${configPath}`, {
|
|
525
|
+
stdio: "inherit",
|
|
526
|
+
cwd: path.dirname(configPath),
|
|
527
|
+
env: resolvedPathEnv()
|
|
528
|
+
});
|
|
529
|
+
let afterFiles = [];
|
|
530
|
+
try {
|
|
531
|
+
const entries = await fs.readdir(migrationsFolder);
|
|
532
|
+
afterFiles = entries.filter((f) => f.endsWith(".sql"));
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
const beforeSet = new Set(beforeFiles);
|
|
536
|
+
return afterFiles.filter((f) => !beforeSet.has(f));
|
|
537
|
+
}
|
|
538
|
+
async function applyMigrations(configPath, db, options) {
|
|
539
|
+
const { migrationsTable, migrationsFolder } = options;
|
|
540
|
+
await db.execute(
|
|
541
|
+
sql3.raw(
|
|
542
|
+
`CREATE TABLE IF NOT EXISTS "${migrationsTable}" (id integer, hash text NOT NULL, created_at bigint)`
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
await db.execute(
|
|
546
|
+
sql3.raw(`ALTER TABLE "${migrationsTable}" ADD COLUMN IF NOT EXISTS id integer`)
|
|
547
|
+
);
|
|
548
|
+
const statusBefore = await getMigrationStatus(db, options);
|
|
549
|
+
if (statusBefore.pending.length > 0) {
|
|
550
|
+
let journalEntries = [];
|
|
551
|
+
try {
|
|
552
|
+
const journalPath = path.join(migrationsFolder, "meta", "_journal.json");
|
|
553
|
+
const raw = await fs.readFile(journalPath, "utf-8");
|
|
554
|
+
journalEntries = JSON.parse(raw).entries;
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
for (const pendingFile of statusBefore.pending) {
|
|
558
|
+
const sqlPath = path.join(migrationsFolder, pendingFile);
|
|
559
|
+
let sqlContent;
|
|
560
|
+
try {
|
|
561
|
+
sqlContent = await fs.readFile(sqlPath, "utf-8");
|
|
562
|
+
} catch {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const tableMatch = sqlContent.match(/CREATE TABLE (?:IF NOT EXISTS )?"([^"]+)"/);
|
|
566
|
+
if (!tableMatch) continue;
|
|
567
|
+
const tableName = tableMatch[1];
|
|
568
|
+
const checkResult = await db.execute(
|
|
569
|
+
sql3.raw(`SELECT to_regclass('"${tableName}"') AS t`)
|
|
570
|
+
);
|
|
571
|
+
const tableExists = checkResult.rows[0]?.t !== null;
|
|
572
|
+
if (!tableExists) continue;
|
|
573
|
+
const entry = journalEntries.find((e) => `${e.tag}.sql` === pendingFile);
|
|
574
|
+
if (!entry) continue;
|
|
575
|
+
const hash = crypto.createHash("sha256").update(sqlContent).digest("hex");
|
|
576
|
+
await db.execute(
|
|
577
|
+
sql3.raw(
|
|
578
|
+
`INSERT INTO "${migrationsTable}" (hash, created_at) SELECT '${hash}', ${entry.when} WHERE NOT EXISTS (SELECT 1 FROM "${migrationsTable}" WHERE created_at = ${entry.when})`
|
|
579
|
+
)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const statusAfter = await getMigrationStatus(db, options);
|
|
584
|
+
if (statusAfter.pending.length === 0) {
|
|
585
|
+
return { applied: statusAfter.applied.length, skipped: 0 };
|
|
586
|
+
}
|
|
587
|
+
const result = spawnSync(
|
|
588
|
+
"drizzle-kit",
|
|
589
|
+
["migrate", `--config=${configPath}`],
|
|
590
|
+
{
|
|
591
|
+
cwd: path.dirname(configPath),
|
|
592
|
+
// inherit lets drizzle-kit write its spinner and errors directly to the
|
|
593
|
+
// terminal in real time — capturing stdout/stderr swallows ANSI overwrites
|
|
594
|
+
// and hides the actual error message.
|
|
595
|
+
stdio: "inherit",
|
|
596
|
+
env: { ...resolvedPathEnv(), NODE_NO_WARNINGS: "1" }
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
if (result.error) {
|
|
600
|
+
throw new Error(`Failed to spawn drizzle-kit: ${result.error.message}`);
|
|
601
|
+
}
|
|
602
|
+
if (result.status !== 0) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`drizzle-kit migrate exited with status ${String(result.status ?? 1)} \u2014 see output above for details`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const status = await getMigrationStatus(db, options);
|
|
608
|
+
return {
|
|
609
|
+
applied: status.applied.length,
|
|
610
|
+
skipped: 0
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
async function getMigrationStatus(db, options) {
|
|
614
|
+
const { migrationsTable, migrationsFolder } = options;
|
|
615
|
+
let journalEntries;
|
|
616
|
+
try {
|
|
617
|
+
const journalPath = path.join(migrationsFolder, "meta", "_journal.json");
|
|
618
|
+
const raw = await fs.readFile(journalPath, "utf-8");
|
|
619
|
+
const journal = JSON.parse(raw);
|
|
620
|
+
journalEntries = journal.entries;
|
|
621
|
+
} catch {
|
|
622
|
+
return { applied: [], pending: [] };
|
|
623
|
+
}
|
|
624
|
+
const appliedWhenSet = /* @__PURE__ */ new Set();
|
|
625
|
+
try {
|
|
626
|
+
const result = await db.execute(
|
|
627
|
+
sql3.raw(`SELECT created_at FROM "${migrationsTable}"`)
|
|
628
|
+
);
|
|
629
|
+
for (const row of result.rows) {
|
|
630
|
+
appliedWhenSet.add(Number(row.created_at));
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
const applied = [];
|
|
635
|
+
const pending = [];
|
|
636
|
+
for (const entry of journalEntries) {
|
|
637
|
+
const filename = `${entry.tag}.sql`;
|
|
638
|
+
if (appliedWhenSet.has(entry.when)) {
|
|
639
|
+
applied.push(filename);
|
|
640
|
+
} else {
|
|
641
|
+
pending.push(filename);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { applied, pending };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/migrations/scanner.ts
|
|
648
|
+
import { readFileSync } from "fs";
|
|
649
|
+
import path2 from "path";
|
|
650
|
+
function formatDropColumn(line) {
|
|
651
|
+
const upper = line.toUpperCase();
|
|
652
|
+
const tableStart = upper.indexOf("ALTER TABLE") + "ALTER TABLE".length;
|
|
653
|
+
const dropIdx = upper.indexOf("DROP COLUMN");
|
|
654
|
+
const colStart = dropIdx + "DROP COLUMN".length;
|
|
655
|
+
if (tableStart < 0 || dropIdx < 0) return line.trim();
|
|
656
|
+
const tableName = line.slice(tableStart, dropIdx).trim().replace(/"/g, "").replace(/;/g, "");
|
|
657
|
+
const colName = (line.slice(colStart).trim().replace(/"/g, "").split(/\s+/)[0] ?? "").replace(/;/g, "");
|
|
658
|
+
if (!tableName || !colName) return line.trim();
|
|
659
|
+
return `DROP COLUMN ${tableName}.${colName}`;
|
|
660
|
+
}
|
|
661
|
+
function formatDropTable(line) {
|
|
662
|
+
const upper = line.toUpperCase();
|
|
663
|
+
const tableStart = upper.indexOf("DROP TABLE") + "DROP TABLE".length;
|
|
664
|
+
if (tableStart < "DROP TABLE".length) return line.trim();
|
|
665
|
+
const tableName = (line.slice(tableStart).trim().replace(/"/g, "").split(/\s+/)[0] ?? "").replace(/;/g, "");
|
|
666
|
+
if (!tableName) return line.trim();
|
|
667
|
+
return `DROP TABLE ${tableName}`;
|
|
668
|
+
}
|
|
669
|
+
function formatAlterColumnType(line) {
|
|
670
|
+
const upper = line.toUpperCase();
|
|
671
|
+
const tableStart = upper.indexOf("ALTER TABLE") + "ALTER TABLE".length;
|
|
672
|
+
const alterColIdx = upper.indexOf("ALTER COLUMN");
|
|
673
|
+
const colStart = alterColIdx + "ALTER COLUMN".length;
|
|
674
|
+
const typeIdx = upper.indexOf(" TYPE ", colStart);
|
|
675
|
+
if (tableStart < 0 || alterColIdx < 0 || typeIdx < 0) return line.trim();
|
|
676
|
+
const tableName = line.slice(tableStart, alterColIdx).trim().replace(/"/g, "");
|
|
677
|
+
const colName = line.slice(colStart, typeIdx).trim().replace(/"/g, "");
|
|
678
|
+
const newType = (line.slice(typeIdx + " TYPE ".length).trim().split(/\s+/)[0] ?? "").replace(/;/g, "");
|
|
679
|
+
if (!tableName || !colName || !newType) return line.trim();
|
|
680
|
+
return `ALTER COLUMN ${tableName}.${colName} TYPE ${newType}`;
|
|
681
|
+
}
|
|
682
|
+
function scanMigrationFiles(filePaths) {
|
|
683
|
+
const operations = [];
|
|
684
|
+
for (const filePath of filePaths) {
|
|
685
|
+
const content = readFileSync(filePath, "utf-8");
|
|
686
|
+
const lines = content.split("\n");
|
|
687
|
+
const filename = path2.basename(filePath);
|
|
688
|
+
for (const line of lines) {
|
|
689
|
+
const upper = line.trim().toUpperCase();
|
|
690
|
+
if (upper.includes("DROP COLUMN")) {
|
|
691
|
+
operations.push({
|
|
692
|
+
file: filename,
|
|
693
|
+
operation: formatDropColumn(line),
|
|
694
|
+
pattern: "DROP_COLUMN"
|
|
695
|
+
});
|
|
696
|
+
} else if (upper.includes("DROP TABLE")) {
|
|
697
|
+
operations.push({
|
|
698
|
+
file: filename,
|
|
699
|
+
operation: formatDropTable(line),
|
|
700
|
+
pattern: "DROP_TABLE"
|
|
701
|
+
});
|
|
702
|
+
} else if (upper.includes("ALTER COLUMN") && upper.includes(" TYPE ")) {
|
|
703
|
+
operations.push({
|
|
704
|
+
file: filename,
|
|
705
|
+
operation: formatAlterColumnType(line),
|
|
706
|
+
pattern: "ALTER_COLUMN_TYPE"
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
hasDestructiveOperations: operations.length > 0,
|
|
713
|
+
operations
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
export {
|
|
717
|
+
applyMigrations,
|
|
718
|
+
createPostgresAdapter,
|
|
719
|
+
generateMigration,
|
|
720
|
+
generateSchemaFile,
|
|
721
|
+
getMigrationStatus,
|
|
722
|
+
runDevMigration,
|
|
723
|
+
scanMigrationFiles,
|
|
724
|
+
seedSystemTables,
|
|
725
|
+
sql4 as sql
|
|
726
|
+
};
|
|
727
|
+
//# sourceMappingURL=index.mjs.map
|