@bobbykim/manguito-cms-api 0.8.0 → 0.8.1
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.js +183 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -108
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -38,7 +38,7 @@ function createVersionedPaths(prefix, version) {
|
|
|
38
38
|
|
|
39
39
|
// src/app.ts
|
|
40
40
|
import { Hono } from "hono";
|
|
41
|
-
import { sql as
|
|
41
|
+
import { sql as sql11 } from "drizzle-orm";
|
|
42
42
|
|
|
43
43
|
// src/middleware/cors.ts
|
|
44
44
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -1049,7 +1049,7 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
1051
|
// src/routes/admin/content.ts
|
|
1052
|
-
import { sql as
|
|
1052
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
1053
1053
|
|
|
1054
1054
|
// src/references-in-use.ts
|
|
1055
1055
|
import { sql as sql2 } from "drizzle-orm";
|
|
@@ -1134,6 +1134,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
1136
|
// src/relation-input.ts
|
|
1137
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
1137
1138
|
import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
|
|
1138
1139
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1139
1140
|
function isUuid(v) {
|
|
@@ -1178,15 +1179,91 @@ function checkRelationInput(fields, body, registry, path = "") {
|
|
|
1178
1179
|
}
|
|
1179
1180
|
return errors;
|
|
1180
1181
|
}
|
|
1182
|
+
function paragraphItemsIn(fields, body, registry, path) {
|
|
1183
|
+
const out = [];
|
|
1184
|
+
for (const field of fields) {
|
|
1185
|
+
if (field.field_type !== "paragraph" || !(field.name in body)) continue;
|
|
1186
|
+
const comp = field.ui_component;
|
|
1187
|
+
const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
1188
|
+
if (!pType) continue;
|
|
1189
|
+
const value = body[field.name];
|
|
1190
|
+
const name = `${path}${field.name}`;
|
|
1191
|
+
if (relationCardinality2(field) === "one") {
|
|
1192
|
+
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
1193
|
+
} else if (Array.isArray(value)) {
|
|
1194
|
+
value.forEach((item, i) => {
|
|
1195
|
+
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
1201
|
+
function isMissing(value) {
|
|
1202
|
+
if (value === null || value === void 0) return true;
|
|
1203
|
+
if (typeof value === "string" && value.trim() === "") return true;
|
|
1204
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
1208
|
+
const errors = [];
|
|
1209
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1210
|
+
for (const f of itemFields) {
|
|
1211
|
+
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
1212
|
+
if (isMissing(item[f.name])) {
|
|
1213
|
+
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
1217
|
+
}
|
|
1218
|
+
return errors;
|
|
1219
|
+
}
|
|
1220
|
+
function quoteIdent2(name) {
|
|
1221
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1222
|
+
return `"${name}"`;
|
|
1223
|
+
}
|
|
1224
|
+
function collectReferences(fields, body, registry, path, out) {
|
|
1225
|
+
for (const f of fields) {
|
|
1226
|
+
if (!(f.name in body) || !f.db_column) continue;
|
|
1227
|
+
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
1228
|
+
if (!table) continue;
|
|
1229
|
+
const value = body[f.name];
|
|
1230
|
+
const name = `${path}${f.name}`;
|
|
1231
|
+
if (typeof value === "string") {
|
|
1232
|
+
out.push({ table, id: value, field: name });
|
|
1233
|
+
} else if (Array.isArray(value)) {
|
|
1234
|
+
value.forEach((v, i) => {
|
|
1235
|
+
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1240
|
+
collectReferences(itemFields, item, registry, prefix, out);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
async function findMissingReferences(db, fields, body, registry) {
|
|
1244
|
+
const uses = [];
|
|
1245
|
+
collectReferences(fields, body, registry, "", uses);
|
|
1246
|
+
if (uses.length === 0) return [];
|
|
1247
|
+
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
1248
|
+
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
1249
|
+
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
1250
|
+
const result = await db.execute(
|
|
1251
|
+
sql3`SELECT id FROM ${sql3.raw(quoteIdent2(table))} WHERE id IN (${sql3.join(ids.map((id) => sql3`${id}`), sql3`, `)})`
|
|
1252
|
+
);
|
|
1253
|
+
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
1254
|
+
});
|
|
1255
|
+
const found = new Map(await Promise.all(lookups));
|
|
1256
|
+
return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
|
|
1257
|
+
}
|
|
1181
1258
|
|
|
1182
1259
|
// src/routes/admin/content.ts
|
|
1183
|
-
function
|
|
1260
|
+
function quoteIdent3(name) {
|
|
1184
1261
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1185
1262
|
return `"${name}"`;
|
|
1186
1263
|
}
|
|
1187
1264
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1188
1265
|
const result = await db.execute(
|
|
1189
|
-
|
|
1266
|
+
sql4`SELECT * FROM ${sql4.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
1190
1267
|
);
|
|
1191
1268
|
const rows = result.rows;
|
|
1192
1269
|
for (const nf of pType.fields) {
|
|
@@ -1206,27 +1283,25 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1206
1283
|
}
|
|
1207
1284
|
async function lookupBasePathId(db, pathOrName) {
|
|
1208
1285
|
const r = await db.execute(
|
|
1209
|
-
|
|
1286
|
+
sql4`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1210
1287
|
);
|
|
1211
1288
|
return r.rows[0]?.id ?? null;
|
|
1212
1289
|
}
|
|
1213
1290
|
var SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1214
|
-
function isEmpty(value) {
|
|
1215
|
-
if (value === null || value === void 0) return true;
|
|
1216
|
-
if (typeof value === "string" && value.trim() === "") return true;
|
|
1217
|
-
if (Array.isArray(value) && value.length === 0) return true;
|
|
1218
|
-
return false;
|
|
1219
|
-
}
|
|
1220
1291
|
function checkRequiredFields(fields, data) {
|
|
1221
|
-
return fields.filter((f) => f.required &&
|
|
1222
|
-
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
return
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
)
|
|
1292
|
+
return fields.filter((f) => f.required && isMissing(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1293
|
+
}
|
|
1294
|
+
async function relationInputError(c, fields, body, registry, db) {
|
|
1295
|
+
const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
|
|
1296
|
+
const shape = checkRelationInput(fields, body, registry);
|
|
1297
|
+
if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
|
|
1298
|
+
const required = checkRequiredInParagraphItems(fields, body, registry);
|
|
1299
|
+
if (required.length > 0) return refuse("Required fields are missing", required);
|
|
1300
|
+
if (db) {
|
|
1301
|
+
const missing = await findMissingReferences(db, fields, body, registry);
|
|
1302
|
+
if (missing.length > 0) return refuse("Relation fields refer to items that do not exist", missing);
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1230
1305
|
}
|
|
1231
1306
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1232
1307
|
const searchParams = new URL(url).searchParams;
|
|
@@ -1373,7 +1448,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1373
1448
|
} else if (f.db_column.junction) {
|
|
1374
1449
|
const j = f.db_column.junction;
|
|
1375
1450
|
const r = await db.execute(
|
|
1376
|
-
|
|
1451
|
+
sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}`
|
|
1377
1452
|
);
|
|
1378
1453
|
row[f.name] = r.rows.map(
|
|
1379
1454
|
(row2) => row2[j.right_column]
|
|
@@ -1449,8 +1524,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1449
1524
|
}
|
|
1450
1525
|
);
|
|
1451
1526
|
const writeNewItem = async (c, body) => {
|
|
1452
|
-
const
|
|
1453
|
-
if (
|
|
1527
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1528
|
+
if (inputError) return inputError;
|
|
1454
1529
|
const storageBody = fieldKeys.toStorage(body);
|
|
1455
1530
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1456
1531
|
if (fieldErrors.length > 0) {
|
|
@@ -1585,8 +1660,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1585
1660
|
}
|
|
1586
1661
|
);
|
|
1587
1662
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1588
|
-
const
|
|
1589
|
-
if (
|
|
1663
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1664
|
+
if (inputError) return inputError;
|
|
1590
1665
|
const storageBody = fieldKeys.toStorage(body);
|
|
1591
1666
|
if (body["published"] === true) {
|
|
1592
1667
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
@@ -1784,8 +1859,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1784
1859
|
requirePermission("content:create"),
|
|
1785
1860
|
async (c) => {
|
|
1786
1861
|
const body = await c.req.json();
|
|
1787
|
-
const
|
|
1788
|
-
if (
|
|
1862
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1863
|
+
if (inputError) return inputError;
|
|
1789
1864
|
const storageBody = fieldKeys.toStorage(body);
|
|
1790
1865
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1791
1866
|
if (fieldErrors.length > 0) {
|
|
@@ -1848,8 +1923,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1848
1923
|
404
|
|
1849
1924
|
);
|
|
1850
1925
|
}
|
|
1851
|
-
const
|
|
1852
|
-
if (
|
|
1926
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1927
|
+
if (inputError) return inputError;
|
|
1853
1928
|
if (body["published"] === true) {
|
|
1854
1929
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1855
1930
|
});
|
|
@@ -2403,7 +2478,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2403
2478
|
|
|
2404
2479
|
// src/routes/admin/auth.ts
|
|
2405
2480
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2406
|
-
import { sql as
|
|
2481
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2407
2482
|
|
|
2408
2483
|
// src/auth/password.ts
|
|
2409
2484
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2469,7 +2544,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2469
2544
|
);
|
|
2470
2545
|
}
|
|
2471
2546
|
const userResult = await db.execute(
|
|
2472
|
-
|
|
2547
|
+
sql5`SELECT id, email, password_hash, role_id, token_version
|
|
2473
2548
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2474
2549
|
);
|
|
2475
2550
|
const user = userResult.rows[0];
|
|
@@ -2482,7 +2557,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2482
2557
|
return c.json(invalidCredentials, 401);
|
|
2483
2558
|
}
|
|
2484
2559
|
const roleResult = await db.execute(
|
|
2485
|
-
|
|
2560
|
+
sql5`SELECT r.name AS name, u.must_change_password
|
|
2486
2561
|
FROM roles r
|
|
2487
2562
|
JOIN users u ON u.role_id = r.id
|
|
2488
2563
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2542,7 +2617,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2542
2617
|
);
|
|
2543
2618
|
}
|
|
2544
2619
|
const rowResult = await db.execute(
|
|
2545
|
-
|
|
2620
|
+
sql5`SELECT u.token_version, r.name AS role
|
|
2546
2621
|
FROM users u
|
|
2547
2622
|
JOIN roles r ON r.id = u.role_id
|
|
2548
2623
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2566,7 +2641,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2566
2641
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2567
2642
|
const user = c.get("user");
|
|
2568
2643
|
await db.execute(
|
|
2569
|
-
|
|
2644
|
+
sql5`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2570
2645
|
);
|
|
2571
2646
|
clearAuthCookie(c, "auth_token");
|
|
2572
2647
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2575,7 +2650,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2575
2650
|
}
|
|
2576
2651
|
|
|
2577
2652
|
// src/routes/admin/users.ts
|
|
2578
|
-
import { sql as
|
|
2653
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2579
2654
|
import { randomBytes } from "crypto";
|
|
2580
2655
|
function getActingUser(c) {
|
|
2581
2656
|
return c.get("user");
|
|
@@ -2610,7 +2685,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2610
2685
|
);
|
|
2611
2686
|
}
|
|
2612
2687
|
const hashResult = await db.execute(
|
|
2613
|
-
|
|
2688
|
+
sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2614
2689
|
);
|
|
2615
2690
|
const hashRow = hashResult.rows[0];
|
|
2616
2691
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2621,7 +2696,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2621
2696
|
}
|
|
2622
2697
|
const newHash = await hashPassword(newPassword);
|
|
2623
2698
|
await db.execute(
|
|
2624
|
-
|
|
2699
|
+
sql6`UPDATE users
|
|
2625
2700
|
SET password_hash = ${newHash},
|
|
2626
2701
|
must_change_password = false,
|
|
2627
2702
|
token_version = token_version + 1,
|
|
@@ -2632,7 +2707,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2632
2707
|
});
|
|
2633
2708
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2634
2709
|
const result = await db.execute(
|
|
2635
|
-
|
|
2710
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2636
2711
|
u.created_at, u.updated_at
|
|
2637
2712
|
FROM users u
|
|
2638
2713
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2643,7 +2718,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2643
2718
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2644
2719
|
const id = c.req.param("id");
|
|
2645
2720
|
const result = await db.execute(
|
|
2646
|
-
|
|
2721
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2647
2722
|
u.created_at, u.updated_at
|
|
2648
2723
|
FROM users u
|
|
2649
2724
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2688,7 +2763,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2688
2763
|
);
|
|
2689
2764
|
}
|
|
2690
2765
|
const roleResult = await db.execute(
|
|
2691
|
-
|
|
2766
|
+
sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2692
2767
|
);
|
|
2693
2768
|
const roleRow = roleResult.rows[0];
|
|
2694
2769
|
if (!roleRow) {
|
|
@@ -2703,7 +2778,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2703
2778
|
const tempPassword = generateTempPassword();
|
|
2704
2779
|
const passwordHash = await hashPassword(tempPassword);
|
|
2705
2780
|
const insertResult = await db.execute(
|
|
2706
|
-
|
|
2781
|
+
sql6`INSERT INTO users
|
|
2707
2782
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2708
2783
|
created_at, updated_at)
|
|
2709
2784
|
VALUES
|
|
@@ -2758,7 +2833,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2758
2833
|
);
|
|
2759
2834
|
}
|
|
2760
2835
|
const existingResult = await db.execute(
|
|
2761
|
-
|
|
2836
|
+
sql6`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2762
2837
|
);
|
|
2763
2838
|
const existing = existingResult.rows[0];
|
|
2764
2839
|
if (!existing) {
|
|
@@ -2772,7 +2847,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2772
2847
|
let changeRole = false;
|
|
2773
2848
|
if (typeof body.role === "string" && body.role) {
|
|
2774
2849
|
const roleResult = await db.execute(
|
|
2775
|
-
|
|
2850
|
+
sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2776
2851
|
);
|
|
2777
2852
|
const roleRow = roleResult.rows[0];
|
|
2778
2853
|
if (!roleRow) {
|
|
@@ -2792,7 +2867,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2792
2867
|
}
|
|
2793
2868
|
if (changeRole) {
|
|
2794
2869
|
await db.execute(
|
|
2795
|
-
|
|
2870
|
+
sql6`UPDATE users
|
|
2796
2871
|
SET email = ${newEmail},
|
|
2797
2872
|
role_id = ${newRoleId},
|
|
2798
2873
|
token_version = token_version + 1,
|
|
@@ -2801,7 +2876,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2801
2876
|
);
|
|
2802
2877
|
} else {
|
|
2803
2878
|
await db.execute(
|
|
2804
|
-
|
|
2879
|
+
sql6`UPDATE users
|
|
2805
2880
|
SET email = ${newEmail},
|
|
2806
2881
|
role_id = ${newRoleId},
|
|
2807
2882
|
updated_at = NOW()
|
|
@@ -2809,7 +2884,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2809
2884
|
);
|
|
2810
2885
|
}
|
|
2811
2886
|
const updatedResult = await db.execute(
|
|
2812
|
-
|
|
2887
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2813
2888
|
u.created_at, u.updated_at
|
|
2814
2889
|
FROM users u
|
|
2815
2890
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2846,7 +2921,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2846
2921
|
);
|
|
2847
2922
|
}
|
|
2848
2923
|
const result = await db.execute(
|
|
2849
|
-
|
|
2924
|
+
sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2850
2925
|
);
|
|
2851
2926
|
if (result.rows.length === 0) {
|
|
2852
2927
|
return c.json(
|
|
@@ -2877,7 +2952,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2877
2952
|
);
|
|
2878
2953
|
}
|
|
2879
2954
|
const existingResult = await db.execute(
|
|
2880
|
-
|
|
2955
|
+
sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2881
2956
|
);
|
|
2882
2957
|
const existing = existingResult.rows[0];
|
|
2883
2958
|
if (!existing) {
|
|
@@ -2889,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2889
2964
|
const tempPassword = generateTempPassword();
|
|
2890
2965
|
const passwordHash = await hashPassword(tempPassword);
|
|
2891
2966
|
await db.execute(
|
|
2892
|
-
|
|
2967
|
+
sql6`UPDATE users
|
|
2893
2968
|
SET password_hash = ${passwordHash},
|
|
2894
2969
|
must_change_password = true,
|
|
2895
2970
|
token_version = token_version + 1,
|
|
@@ -2902,10 +2977,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2902
2977
|
}
|
|
2903
2978
|
|
|
2904
2979
|
// src/routes/admin/config.ts
|
|
2905
|
-
import { sql as
|
|
2980
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2906
2981
|
var API_VERSION = (() => {
|
|
2907
2982
|
try {
|
|
2908
|
-
return "0.8.
|
|
2983
|
+
return "0.8.1";
|
|
2909
2984
|
} catch {
|
|
2910
2985
|
return "0.0.0";
|
|
2911
2986
|
}
|
|
@@ -2924,7 +2999,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2924
2999
|
permissions: r.permissions
|
|
2925
3000
|
}));
|
|
2926
3001
|
const userResult = await db.execute(
|
|
2927
|
-
|
|
3002
|
+
sql7`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2928
3003
|
);
|
|
2929
3004
|
const userRow = userResult.rows[0];
|
|
2930
3005
|
return c.json({
|
|
@@ -2950,7 +3025,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2950
3025
|
}
|
|
2951
3026
|
|
|
2952
3027
|
// src/routes/admin/schema.ts
|
|
2953
|
-
import { sql as
|
|
3028
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
2954
3029
|
function liveFields(fields) {
|
|
2955
3030
|
return fields.filter((f) => f.removed !== true);
|
|
2956
3031
|
}
|
|
@@ -2990,7 +3065,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2990
3065
|
const data = await Promise.all(
|
|
2991
3066
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2992
3067
|
const result = await db.execute(
|
|
2993
|
-
|
|
3068
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
|
|
2994
3069
|
);
|
|
2995
3070
|
const count = Number(result.rows[0].count);
|
|
2996
3071
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -3002,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3002
3077
|
const data = await Promise.all(
|
|
3003
3078
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
3004
3079
|
const result = await db.execute(
|
|
3005
|
-
|
|
3080
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
|
|
3006
3081
|
);
|
|
3007
3082
|
const count = Number(result.rows[0].count);
|
|
3008
3083
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -3014,14 +3089,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3014
3089
|
|
|
3015
3090
|
// src/repositories/content.ts
|
|
3016
3091
|
import crypto from "crypto";
|
|
3017
|
-
import { sql as
|
|
3092
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3018
3093
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
3019
3094
|
function codeError(code, message) {
|
|
3020
3095
|
const err = new Error(message);
|
|
3021
3096
|
err.code = code;
|
|
3022
3097
|
return err;
|
|
3023
3098
|
}
|
|
3024
|
-
function
|
|
3099
|
+
function quoteIdent4(name) {
|
|
3025
3100
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
3026
3101
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
3027
3102
|
}
|
|
@@ -3036,43 +3111,43 @@ function quoteField(name) {
|
|
|
3036
3111
|
function buildConditions(published_only, filters, search) {
|
|
3037
3112
|
const conds = [];
|
|
3038
3113
|
if (published_only) {
|
|
3039
|
-
conds.push(
|
|
3114
|
+
conds.push(sql9`published = true`);
|
|
3040
3115
|
}
|
|
3041
3116
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
3042
3117
|
const pattern = `%${search.term.trim()}%`;
|
|
3043
|
-
const orConds = search.columns.map((col) =>
|
|
3044
|
-
conds.push(
|
|
3118
|
+
const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
3119
|
+
conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
|
|
3045
3120
|
}
|
|
3046
3121
|
if (filters) {
|
|
3047
3122
|
for (const [field, value] of Object.entries(filters)) {
|
|
3048
|
-
const col =
|
|
3123
|
+
const col = sql9.raw(quoteField(field));
|
|
3049
3124
|
if (Array.isArray(value)) {
|
|
3050
3125
|
if (value.length === 0) continue;
|
|
3051
|
-
const inList =
|
|
3052
|
-
value.map((v) =>
|
|
3053
|
-
|
|
3126
|
+
const inList = sql9.join(
|
|
3127
|
+
value.map((v) => sql9`${v}`),
|
|
3128
|
+
sql9`, `
|
|
3054
3129
|
);
|
|
3055
|
-
conds.push(
|
|
3130
|
+
conds.push(sql9`${col} IN (${inList})`);
|
|
3056
3131
|
} else if (typeof value === "object" && value !== null) {
|
|
3057
3132
|
const op = value;
|
|
3058
|
-
if (op.gt !== void 0) conds.push(
|
|
3059
|
-
if (op.gte !== void 0) conds.push(
|
|
3060
|
-
if (op.lt !== void 0) conds.push(
|
|
3061
|
-
if (op.lte !== void 0) conds.push(
|
|
3133
|
+
if (op.gt !== void 0) conds.push(sql9`${col} > ${op.gt}`);
|
|
3134
|
+
if (op.gte !== void 0) conds.push(sql9`${col} >= ${op.gte}`);
|
|
3135
|
+
if (op.lt !== void 0) conds.push(sql9`${col} < ${op.lt}`);
|
|
3136
|
+
if (op.lte !== void 0) conds.push(sql9`${col} <= ${op.lte}`);
|
|
3062
3137
|
} else {
|
|
3063
|
-
conds.push(
|
|
3138
|
+
conds.push(sql9`${col} = ${value}`);
|
|
3064
3139
|
}
|
|
3065
3140
|
}
|
|
3066
3141
|
}
|
|
3067
3142
|
return conds;
|
|
3068
3143
|
}
|
|
3069
3144
|
function whereFragment(conds) {
|
|
3070
|
-
return conds.length > 0 ?
|
|
3145
|
+
return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
|
|
3071
3146
|
}
|
|
3072
3147
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
3073
3148
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
3074
3149
|
function tableRaw() {
|
|
3075
|
-
return
|
|
3150
|
+
return sql9.raw(quoteIdent4(tableName));
|
|
3076
3151
|
}
|
|
3077
3152
|
async function resolveRows(rows, include, cache) {
|
|
3078
3153
|
if (rows.length === 0) return rows;
|
|
@@ -3115,14 +3190,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3115
3190
|
const offset = (page - 1) * per_page;
|
|
3116
3191
|
const conds = buildConditions(published_only, filters, search);
|
|
3117
3192
|
const where = whereFragment(conds);
|
|
3118
|
-
const sortCol =
|
|
3119
|
-
const sortDir =
|
|
3193
|
+
const sortCol = sql9.raw(quoteIdent4(sort_by));
|
|
3194
|
+
const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
3120
3195
|
const countResult = await db.execute(
|
|
3121
|
-
|
|
3196
|
+
sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
3122
3197
|
);
|
|
3123
3198
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
3124
3199
|
const dataResult = await db.execute(
|
|
3125
|
-
|
|
3200
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
3126
3201
|
);
|
|
3127
3202
|
const rows = dataResult.rows;
|
|
3128
3203
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3143,7 +3218,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3143
3218
|
},
|
|
3144
3219
|
async findOne(id, include = []) {
|
|
3145
3220
|
const result = await db.execute(
|
|
3146
|
-
|
|
3221
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3147
3222
|
);
|
|
3148
3223
|
if (result.rows.length === 0) return null;
|
|
3149
3224
|
const rows = result.rows;
|
|
@@ -3153,7 +3228,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3153
3228
|
},
|
|
3154
3229
|
async findBySlug(slug, include = []) {
|
|
3155
3230
|
const result = await db.execute(
|
|
3156
|
-
|
|
3231
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3157
3232
|
);
|
|
3158
3233
|
if (result.rows.length === 0) return null;
|
|
3159
3234
|
const rows = result.rows;
|
|
@@ -3169,10 +3244,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3169
3244
|
updated_at: /* @__PURE__ */ new Date()
|
|
3170
3245
|
};
|
|
3171
3246
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3172
|
-
const colsSql =
|
|
3173
|
-
const valsSql =
|
|
3247
|
+
const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
|
|
3248
|
+
const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
|
|
3174
3249
|
const result = await db.execute(
|
|
3175
|
-
|
|
3250
|
+
sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3176
3251
|
);
|
|
3177
3252
|
return result.rows[0];
|
|
3178
3253
|
},
|
|
@@ -3185,25 +3260,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3185
3260
|
if (entries.length === 0) {
|
|
3186
3261
|
return this.findOne(id);
|
|
3187
3262
|
}
|
|
3188
|
-
const setClauses =
|
|
3189
|
-
entries.map(([k, v]) =>
|
|
3190
|
-
|
|
3263
|
+
const setClauses = sql9.join(
|
|
3264
|
+
entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
|
|
3265
|
+
sql9`, `
|
|
3191
3266
|
);
|
|
3192
3267
|
const result = await db.execute(
|
|
3193
|
-
|
|
3268
|
+
sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3194
3269
|
);
|
|
3195
3270
|
if (result.rows.length === 0) return null;
|
|
3196
3271
|
return result.rows[0];
|
|
3197
3272
|
},
|
|
3198
3273
|
async delete(id) {
|
|
3199
|
-
await db.execute(
|
|
3274
|
+
await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3200
3275
|
},
|
|
3201
3276
|
async findAll(opts = {}) {
|
|
3202
3277
|
const { include = [], published_only } = opts;
|
|
3203
3278
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3204
3279
|
const where = whereFragment(conds);
|
|
3205
3280
|
const result = await db.execute(
|
|
3206
|
-
|
|
3281
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3207
3282
|
);
|
|
3208
3283
|
const rows = result.rows;
|
|
3209
3284
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3215,23 +3290,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3215
3290
|
|
|
3216
3291
|
// src/repositories/media.ts
|
|
3217
3292
|
import crypto2 from "crypto";
|
|
3218
|
-
import { sql as
|
|
3293
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
3219
3294
|
function createMediaRepository(db) {
|
|
3220
|
-
const TABLE =
|
|
3295
|
+
const TABLE = sql10.raw('"media"');
|
|
3221
3296
|
return {
|
|
3222
3297
|
async findMany(opts = {}) {
|
|
3223
3298
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3224
3299
|
const offset = (page - 1) * per_page;
|
|
3225
3300
|
const conds = [];
|
|
3226
|
-
if (type) conds.push(
|
|
3227
|
-
if (orphaned) conds.push(
|
|
3228
|
-
const where = conds.length > 0 ?
|
|
3229
|
-
const countResult = await db.execute(
|
|
3301
|
+
if (type) conds.push(sql10`type = ${type}`);
|
|
3302
|
+
if (orphaned) conds.push(sql10`reference_count = 0`);
|
|
3303
|
+
const where = conds.length > 0 ? sql10` WHERE ${sql10.join(conds, sql10` AND `)}` : sql10``;
|
|
3304
|
+
const countResult = await db.execute(sql10`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3230
3305
|
const total = Number(
|
|
3231
3306
|
countResult.rows[0]?.["total"] ?? 0
|
|
3232
3307
|
);
|
|
3233
3308
|
const dataResult = await db.execute(
|
|
3234
|
-
|
|
3309
|
+
sql10`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3235
3310
|
);
|
|
3236
3311
|
const total_pages = Math.ceil(total / per_page);
|
|
3237
3312
|
return {
|
|
@@ -3249,7 +3324,7 @@ function createMediaRepository(db) {
|
|
|
3249
3324
|
},
|
|
3250
3325
|
async findOne(id) {
|
|
3251
3326
|
const result = await db.execute(
|
|
3252
|
-
|
|
3327
|
+
sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3253
3328
|
);
|
|
3254
3329
|
if (result.rows.length === 0) return null;
|
|
3255
3330
|
return result.rows[0];
|
|
@@ -3263,10 +3338,10 @@ function createMediaRepository(db) {
|
|
|
3263
3338
|
updated_at: /* @__PURE__ */ new Date()
|
|
3264
3339
|
};
|
|
3265
3340
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3266
|
-
const colsSql =
|
|
3267
|
-
const valsSql =
|
|
3341
|
+
const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
|
|
3342
|
+
const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
|
|
3268
3343
|
const result = await db.execute(
|
|
3269
|
-
|
|
3344
|
+
sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3270
3345
|
);
|
|
3271
3346
|
return result.rows[0];
|
|
3272
3347
|
},
|
|
@@ -3276,31 +3351,31 @@ function createMediaRepository(db) {
|
|
|
3276
3351
|
([k, v]) => k !== "id" && v !== void 0
|
|
3277
3352
|
);
|
|
3278
3353
|
if (entries.length === 0) return this.findOne(id);
|
|
3279
|
-
const setClauses =
|
|
3280
|
-
entries.map(([k, v]) =>
|
|
3281
|
-
|
|
3354
|
+
const setClauses = sql10.join(
|
|
3355
|
+
entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
|
|
3356
|
+
sql10`, `
|
|
3282
3357
|
);
|
|
3283
3358
|
const result = await db.execute(
|
|
3284
|
-
|
|
3359
|
+
sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3285
3360
|
);
|
|
3286
3361
|
if (result.rows.length === 0) return null;
|
|
3287
3362
|
return result.rows[0];
|
|
3288
3363
|
},
|
|
3289
3364
|
async delete(id) {
|
|
3290
|
-
await db.execute(
|
|
3365
|
+
await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3291
3366
|
},
|
|
3292
3367
|
async incrementReferenceCount(ids) {
|
|
3293
3368
|
if (ids.length === 0) return;
|
|
3294
|
-
const inList =
|
|
3369
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3295
3370
|
await db.execute(
|
|
3296
|
-
|
|
3371
|
+
sql10`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3297
3372
|
);
|
|
3298
3373
|
},
|
|
3299
3374
|
async decrementReferenceCount(ids) {
|
|
3300
3375
|
if (ids.length === 0) return;
|
|
3301
|
-
const inList =
|
|
3376
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3302
3377
|
await db.execute(
|
|
3303
|
-
|
|
3378
|
+
sql10`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3304
3379
|
);
|
|
3305
3380
|
}
|
|
3306
3381
|
};
|
|
@@ -3433,7 +3508,7 @@ function createCmsApp(options) {
|
|
|
3433
3508
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3434
3509
|
const getUserRole = async (userId) => {
|
|
3435
3510
|
const result = await db.execute(
|
|
3436
|
-
|
|
3511
|
+
sql11`SELECT r.name AS role
|
|
3437
3512
|
FROM users u
|
|
3438
3513
|
JOIN roles r ON r.id = u.role_id
|
|
3439
3514
|
WHERE u.id = ${userId}
|