@bobbykim/manguito-cms-api 0.8.0 → 0.8.2
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 +230 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -184
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
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) {
|
|
@@ -1204,29 +1281,67 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1204
1281
|
}
|
|
1205
1282
|
return rows;
|
|
1206
1283
|
}
|
|
1284
|
+
async function loadRelationsForEdit(db, registry, fields, id, row) {
|
|
1285
|
+
for (const f of fields) {
|
|
1286
|
+
if (f.db_column === null) {
|
|
1287
|
+
const comp = f.ui_component;
|
|
1288
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1289
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
1290
|
+
if (!pType) continue;
|
|
1291
|
+
row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
|
|
1292
|
+
} else if (f.db_column.junction) {
|
|
1293
|
+
const j = f.db_column.junction;
|
|
1294
|
+
const r = await db.execute(
|
|
1295
|
+
sql4`SELECT ${sql4.raw(quoteIdent3(j.right_column))} FROM ${sql4.raw(quoteIdent3(j.table_name))} WHERE ${sql4.raw(quoteIdent3(j.left_column))} = ${id}`
|
|
1296
|
+
);
|
|
1297
|
+
row[f.name] = r.rows.map((linked) => linked[j.right_column]);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
|
|
1302
|
+
const stored = { ...storedLabels };
|
|
1303
|
+
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name in body)), id, stored);
|
|
1304
|
+
return { ...stored, ...body };
|
|
1305
|
+
}
|
|
1306
|
+
async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
|
|
1307
|
+
const deltas = [];
|
|
1308
|
+
for (const f of fields) {
|
|
1309
|
+
if (!(f.name in body)) continue;
|
|
1310
|
+
if (f.db_column === null) {
|
|
1311
|
+
const comp = f.ui_component;
|
|
1312
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1313
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
1314
|
+
if (!pType) continue;
|
|
1315
|
+
deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
|
|
1316
|
+
} else if (f.db_column.junction) {
|
|
1317
|
+
const value = body[f.name];
|
|
1318
|
+
const relatedIds = Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
1319
|
+
await persistJunctionField(db, itemId, f.db_column.junction, relatedIds);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return deltas;
|
|
1323
|
+
}
|
|
1207
1324
|
async function lookupBasePathId(db, pathOrName) {
|
|
1208
1325
|
const r = await db.execute(
|
|
1209
|
-
|
|
1326
|
+
sql4`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1210
1327
|
);
|
|
1211
1328
|
return r.rows[0]?.id ?? null;
|
|
1212
1329
|
}
|
|
1213
1330
|
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
1331
|
function checkRequiredFields(fields, data) {
|
|
1221
|
-
return fields.filter((f) => f.required &&
|
|
1222
|
-
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
return
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
)
|
|
1332
|
+
return fields.filter((f) => f.required && isMissing(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1333
|
+
}
|
|
1334
|
+
async function relationInputError(c, fields, body, registry, db) {
|
|
1335
|
+
const refuse = (message, details) => c.json({ ok: false, error: { code: "VALIDATION_ERROR", message, details } }, 422);
|
|
1336
|
+
const shape = checkRelationInput(fields, body, registry);
|
|
1337
|
+
if (shape.length > 0) return refuse("Relation fields have the wrong shape", shape);
|
|
1338
|
+
const required = checkRequiredInParagraphItems(fields, body, registry);
|
|
1339
|
+
if (required.length > 0) return refuse("Required fields are missing", required);
|
|
1340
|
+
if (db) {
|
|
1341
|
+
const missing = await findMissingReferences(db, fields, body, registry);
|
|
1342
|
+
if (missing.length > 0) return refuse("Relation fields refer to items that do not exist", missing);
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1230
1345
|
}
|
|
1231
1346
|
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1232
1347
|
const searchParams = new URL(url).searchParams;
|
|
@@ -1361,26 +1476,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1361
1476
|
404
|
|
1362
1477
|
);
|
|
1363
1478
|
}
|
|
1364
|
-
if (db)
|
|
1365
|
-
const row = item;
|
|
1366
|
-
for (const f of contentType.fields) {
|
|
1367
|
-
if (f.db_column === null) {
|
|
1368
|
-
const comp = f.ui_component;
|
|
1369
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1370
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1371
|
-
if (!pType) continue;
|
|
1372
|
-
row[f.name] = oneOrMany(relationCardinality3(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
|
|
1373
|
-
} else if (f.db_column.junction) {
|
|
1374
|
-
const j = f.db_column.junction;
|
|
1375
|
-
const r = await db.execute(
|
|
1376
|
-
sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}`
|
|
1377
|
-
);
|
|
1378
|
-
row[f.name] = r.rows.map(
|
|
1379
|
-
(row2) => row2[j.right_column]
|
|
1380
|
-
);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1479
|
+
if (db) await loadRelationsForEdit(db, registry, contentType.fields, id, item);
|
|
1384
1480
|
const data = projectRow(item, typeName, projectors);
|
|
1385
1481
|
return c.json({ ok: true, data });
|
|
1386
1482
|
}
|
|
@@ -1449,8 +1545,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1449
1545
|
}
|
|
1450
1546
|
);
|
|
1451
1547
|
const writeNewItem = async (c, body) => {
|
|
1452
|
-
const
|
|
1453
|
-
if (
|
|
1548
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1549
|
+
if (inputError) return inputError;
|
|
1454
1550
|
const storageBody = fieldKeys.toStorage(body);
|
|
1455
1551
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1456
1552
|
if (fieldErrors.length > 0) {
|
|
@@ -1471,10 +1567,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1471
1567
|
});
|
|
1472
1568
|
if (publishDeny) return publishDeny;
|
|
1473
1569
|
}
|
|
1474
|
-
const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
1475
|
-
const junctionFields = contentType.fields.filter(
|
|
1476
|
-
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1477
|
-
);
|
|
1478
1570
|
const columnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
1479
1571
|
const columnData = {
|
|
1480
1572
|
published: body["published"] ?? false
|
|
@@ -1509,19 +1601,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1509
1601
|
const itemId = item["id"];
|
|
1510
1602
|
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
1511
1603
|
if (db) {
|
|
1512
|
-
|
|
1513
|
-
const comp = f.ui_component;
|
|
1514
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1515
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1516
|
-
if (!pType) continue;
|
|
1517
|
-
const items = paragraphItems(f, body[f.name]);
|
|
1518
|
-
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1519
|
-
}
|
|
1520
|
-
for (const f of junctionFields) {
|
|
1521
|
-
const junction = f.db_column.junction;
|
|
1522
|
-
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1523
|
-
await persistJunctionField(db, itemId, junction, relatedIds);
|
|
1524
|
-
}
|
|
1604
|
+
mediaDeltas.push(...await persistRelationFields(db, registry, contentType.fields, body, itemId, contentType.db.table_name));
|
|
1525
1605
|
}
|
|
1526
1606
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1527
1607
|
const data = projectRow(item, typeName, projectors);
|
|
@@ -1585,14 +1665,14 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1585
1665
|
}
|
|
1586
1666
|
);
|
|
1587
1667
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1588
|
-
const
|
|
1589
|
-
if (
|
|
1668
|
+
const inputError = await relationInputError(c, contentType.fields, body, registry, db);
|
|
1669
|
+
if (inputError) return inputError;
|
|
1590
1670
|
const storageBody = fieldKeys.toStorage(body);
|
|
1591
1671
|
if (body["published"] === true) {
|
|
1592
1672
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1593
1673
|
});
|
|
1594
1674
|
if (publishDeny) return publishDeny;
|
|
1595
|
-
const merged =
|
|
1675
|
+
const merged = await valuesForPublishCheck(db, registry, requiredFields, id, fieldKeys.toLabels(existing), body);
|
|
1596
1676
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
1597
1677
|
if (fieldErrors.length > 0) {
|
|
1598
1678
|
return c.json(
|
|
@@ -1608,10 +1688,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1608
1688
|
);
|
|
1609
1689
|
}
|
|
1610
1690
|
}
|
|
1611
|
-
const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
1612
|
-
const patchJunctionFields = contentType.fields.filter(
|
|
1613
|
-
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1614
|
-
);
|
|
1615
1691
|
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
1616
1692
|
const patchData = {};
|
|
1617
1693
|
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
@@ -1632,21 +1708,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1632
1708
|
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
1633
1709
|
];
|
|
1634
1710
|
if (db) {
|
|
1635
|
-
|
|
1636
|
-
if (!(f.name in body)) continue;
|
|
1637
|
-
const comp = f.ui_component;
|
|
1638
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1639
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1640
|
-
if (!pType) continue;
|
|
1641
|
-
const items = paragraphItems(f, body[f.name]);
|
|
1642
|
-
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1643
|
-
}
|
|
1644
|
-
for (const f of patchJunctionFields) {
|
|
1645
|
-
if (!(f.name in body)) continue;
|
|
1646
|
-
const junction = f.db_column.junction;
|
|
1647
|
-
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1648
|
-
await persistJunctionField(db, id, junction, relatedIds);
|
|
1649
|
-
}
|
|
1711
|
+
mediaDeltas.push(...await persistRelationFields(db, registry, contentType.fields, body, id, contentType.db.table_name));
|
|
1650
1712
|
}
|
|
1651
1713
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1652
1714
|
const data = projectRow(updated, typeName, projectors);
|
|
@@ -1775,6 +1837,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1775
1837
|
404
|
|
1776
1838
|
);
|
|
1777
1839
|
}
|
|
1840
|
+
if (db) await loadRelationsForEdit(db, registry, taxonomyType.fields, id, item);
|
|
1778
1841
|
const data = projectRow(item, typeName, projectors);
|
|
1779
1842
|
return c.json({ ok: true, data });
|
|
1780
1843
|
}
|
|
@@ -1784,8 +1847,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1784
1847
|
requirePermission("content:create"),
|
|
1785
1848
|
async (c) => {
|
|
1786
1849
|
const body = await c.req.json();
|
|
1787
|
-
const
|
|
1788
|
-
if (
|
|
1850
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1851
|
+
if (inputError) return inputError;
|
|
1789
1852
|
const storageBody = fieldKeys.toStorage(body);
|
|
1790
1853
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1791
1854
|
if (fieldErrors.length > 0) {
|
|
@@ -1806,7 +1869,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1806
1869
|
});
|
|
1807
1870
|
if (publishDeny) return publishDeny;
|
|
1808
1871
|
}
|
|
1809
|
-
const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
1810
1872
|
const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
1811
1873
|
const taxColumnData = {
|
|
1812
1874
|
published: body["published"] ?? false
|
|
@@ -1820,14 +1882,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1820
1882
|
const taxItemId = item["id"];
|
|
1821
1883
|
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
1822
1884
|
if (db) {
|
|
1823
|
-
|
|
1824
|
-
const comp = f.ui_component;
|
|
1825
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1826
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1827
|
-
if (!pType) continue;
|
|
1828
|
-
const items = paragraphItems(f, body[f.name]);
|
|
1829
|
-
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1830
|
-
}
|
|
1885
|
+
mediaDeltas.push(...await persistRelationFields(db, registry, taxonomyType.fields, body, taxItemId, taxonomyType.db.table_name));
|
|
1831
1886
|
}
|
|
1832
1887
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1833
1888
|
const data = projectRow(item, typeName, projectors);
|
|
@@ -1848,13 +1903,13 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1848
1903
|
404
|
|
1849
1904
|
);
|
|
1850
1905
|
}
|
|
1851
|
-
const
|
|
1852
|
-
if (
|
|
1906
|
+
const inputError = await relationInputError(c, taxonomyType.fields, body, registry, db);
|
|
1907
|
+
if (inputError) return inputError;
|
|
1853
1908
|
if (body["published"] === true) {
|
|
1854
1909
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1855
1910
|
});
|
|
1856
1911
|
if (publishDeny) return publishDeny;
|
|
1857
|
-
const merged =
|
|
1912
|
+
const merged = await valuesForPublishCheck(db, registry, requiredFields, id, fieldKeys.toLabels(existing), body);
|
|
1858
1913
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
1859
1914
|
if (fieldErrors.length > 0) {
|
|
1860
1915
|
return c.json(
|
|
@@ -1870,7 +1925,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1870
1925
|
);
|
|
1871
1926
|
}
|
|
1872
1927
|
}
|
|
1873
|
-
const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
1874
1928
|
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
1875
1929
|
const taxPatchData = {};
|
|
1876
1930
|
if ("published" in body) taxPatchData["published"] = body["published"];
|
|
@@ -1890,15 +1944,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1890
1944
|
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
1891
1945
|
];
|
|
1892
1946
|
if (db) {
|
|
1893
|
-
|
|
1894
|
-
if (!(f.name in body)) continue;
|
|
1895
|
-
const comp = f.ui_component;
|
|
1896
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1897
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1898
|
-
if (!pType) continue;
|
|
1899
|
-
const items = paragraphItems(f, body[f.name]);
|
|
1900
|
-
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1901
|
-
}
|
|
1947
|
+
mediaDeltas.push(...await persistRelationFields(db, registry, taxonomyType.fields, body, id, taxonomyType.db.table_name));
|
|
1902
1948
|
}
|
|
1903
1949
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1904
1950
|
const data = projectRow(updated, typeName, projectors);
|
|
@@ -2403,7 +2449,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2403
2449
|
|
|
2404
2450
|
// src/routes/admin/auth.ts
|
|
2405
2451
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2406
|
-
import { sql as
|
|
2452
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2407
2453
|
|
|
2408
2454
|
// src/auth/password.ts
|
|
2409
2455
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2469,7 +2515,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2469
2515
|
);
|
|
2470
2516
|
}
|
|
2471
2517
|
const userResult = await db.execute(
|
|
2472
|
-
|
|
2518
|
+
sql5`SELECT id, email, password_hash, role_id, token_version
|
|
2473
2519
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2474
2520
|
);
|
|
2475
2521
|
const user = userResult.rows[0];
|
|
@@ -2482,7 +2528,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2482
2528
|
return c.json(invalidCredentials, 401);
|
|
2483
2529
|
}
|
|
2484
2530
|
const roleResult = await db.execute(
|
|
2485
|
-
|
|
2531
|
+
sql5`SELECT r.name AS name, u.must_change_password
|
|
2486
2532
|
FROM roles r
|
|
2487
2533
|
JOIN users u ON u.role_id = r.id
|
|
2488
2534
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2542,7 +2588,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2542
2588
|
);
|
|
2543
2589
|
}
|
|
2544
2590
|
const rowResult = await db.execute(
|
|
2545
|
-
|
|
2591
|
+
sql5`SELECT u.token_version, r.name AS role
|
|
2546
2592
|
FROM users u
|
|
2547
2593
|
JOIN roles r ON r.id = u.role_id
|
|
2548
2594
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2566,7 +2612,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2566
2612
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2567
2613
|
const user = c.get("user");
|
|
2568
2614
|
await db.execute(
|
|
2569
|
-
|
|
2615
|
+
sql5`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2570
2616
|
);
|
|
2571
2617
|
clearAuthCookie(c, "auth_token");
|
|
2572
2618
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2575,7 +2621,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2575
2621
|
}
|
|
2576
2622
|
|
|
2577
2623
|
// src/routes/admin/users.ts
|
|
2578
|
-
import { sql as
|
|
2624
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2579
2625
|
import { randomBytes } from "crypto";
|
|
2580
2626
|
function getActingUser(c) {
|
|
2581
2627
|
return c.get("user");
|
|
@@ -2610,7 +2656,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2610
2656
|
);
|
|
2611
2657
|
}
|
|
2612
2658
|
const hashResult = await db.execute(
|
|
2613
|
-
|
|
2659
|
+
sql6`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2614
2660
|
);
|
|
2615
2661
|
const hashRow = hashResult.rows[0];
|
|
2616
2662
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2621,7 +2667,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2621
2667
|
}
|
|
2622
2668
|
const newHash = await hashPassword(newPassword);
|
|
2623
2669
|
await db.execute(
|
|
2624
|
-
|
|
2670
|
+
sql6`UPDATE users
|
|
2625
2671
|
SET password_hash = ${newHash},
|
|
2626
2672
|
must_change_password = false,
|
|
2627
2673
|
token_version = token_version + 1,
|
|
@@ -2632,7 +2678,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2632
2678
|
});
|
|
2633
2679
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2634
2680
|
const result = await db.execute(
|
|
2635
|
-
|
|
2681
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2636
2682
|
u.created_at, u.updated_at
|
|
2637
2683
|
FROM users u
|
|
2638
2684
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2643,7 +2689,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2643
2689
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2644
2690
|
const id = c.req.param("id");
|
|
2645
2691
|
const result = await db.execute(
|
|
2646
|
-
|
|
2692
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2647
2693
|
u.created_at, u.updated_at
|
|
2648
2694
|
FROM users u
|
|
2649
2695
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2688,7 +2734,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2688
2734
|
);
|
|
2689
2735
|
}
|
|
2690
2736
|
const roleResult = await db.execute(
|
|
2691
|
-
|
|
2737
|
+
sql6`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2692
2738
|
);
|
|
2693
2739
|
const roleRow = roleResult.rows[0];
|
|
2694
2740
|
if (!roleRow) {
|
|
@@ -2703,7 +2749,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2703
2749
|
const tempPassword = generateTempPassword();
|
|
2704
2750
|
const passwordHash = await hashPassword(tempPassword);
|
|
2705
2751
|
const insertResult = await db.execute(
|
|
2706
|
-
|
|
2752
|
+
sql6`INSERT INTO users
|
|
2707
2753
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2708
2754
|
created_at, updated_at)
|
|
2709
2755
|
VALUES
|
|
@@ -2758,7 +2804,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2758
2804
|
);
|
|
2759
2805
|
}
|
|
2760
2806
|
const existingResult = await db.execute(
|
|
2761
|
-
|
|
2807
|
+
sql6`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2762
2808
|
);
|
|
2763
2809
|
const existing = existingResult.rows[0];
|
|
2764
2810
|
if (!existing) {
|
|
@@ -2772,7 +2818,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2772
2818
|
let changeRole = false;
|
|
2773
2819
|
if (typeof body.role === "string" && body.role) {
|
|
2774
2820
|
const roleResult = await db.execute(
|
|
2775
|
-
|
|
2821
|
+
sql6`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2776
2822
|
);
|
|
2777
2823
|
const roleRow = roleResult.rows[0];
|
|
2778
2824
|
if (!roleRow) {
|
|
@@ -2792,7 +2838,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2792
2838
|
}
|
|
2793
2839
|
if (changeRole) {
|
|
2794
2840
|
await db.execute(
|
|
2795
|
-
|
|
2841
|
+
sql6`UPDATE users
|
|
2796
2842
|
SET email = ${newEmail},
|
|
2797
2843
|
role_id = ${newRoleId},
|
|
2798
2844
|
token_version = token_version + 1,
|
|
@@ -2801,7 +2847,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2801
2847
|
);
|
|
2802
2848
|
} else {
|
|
2803
2849
|
await db.execute(
|
|
2804
|
-
|
|
2850
|
+
sql6`UPDATE users
|
|
2805
2851
|
SET email = ${newEmail},
|
|
2806
2852
|
role_id = ${newRoleId},
|
|
2807
2853
|
updated_at = NOW()
|
|
@@ -2809,7 +2855,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2809
2855
|
);
|
|
2810
2856
|
}
|
|
2811
2857
|
const updatedResult = await db.execute(
|
|
2812
|
-
|
|
2858
|
+
sql6`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2813
2859
|
u.created_at, u.updated_at
|
|
2814
2860
|
FROM users u
|
|
2815
2861
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2846,7 +2892,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2846
2892
|
);
|
|
2847
2893
|
}
|
|
2848
2894
|
const result = await db.execute(
|
|
2849
|
-
|
|
2895
|
+
sql6`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2850
2896
|
);
|
|
2851
2897
|
if (result.rows.length === 0) {
|
|
2852
2898
|
return c.json(
|
|
@@ -2877,7 +2923,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2877
2923
|
);
|
|
2878
2924
|
}
|
|
2879
2925
|
const existingResult = await db.execute(
|
|
2880
|
-
|
|
2926
|
+
sql6`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2881
2927
|
);
|
|
2882
2928
|
const existing = existingResult.rows[0];
|
|
2883
2929
|
if (!existing) {
|
|
@@ -2889,7 +2935,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2889
2935
|
const tempPassword = generateTempPassword();
|
|
2890
2936
|
const passwordHash = await hashPassword(tempPassword);
|
|
2891
2937
|
await db.execute(
|
|
2892
|
-
|
|
2938
|
+
sql6`UPDATE users
|
|
2893
2939
|
SET password_hash = ${passwordHash},
|
|
2894
2940
|
must_change_password = true,
|
|
2895
2941
|
token_version = token_version + 1,
|
|
@@ -2902,10 +2948,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2902
2948
|
}
|
|
2903
2949
|
|
|
2904
2950
|
// src/routes/admin/config.ts
|
|
2905
|
-
import { sql as
|
|
2951
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2906
2952
|
var API_VERSION = (() => {
|
|
2907
2953
|
try {
|
|
2908
|
-
return "0.8.
|
|
2954
|
+
return "0.8.2";
|
|
2909
2955
|
} catch {
|
|
2910
2956
|
return "0.0.0";
|
|
2911
2957
|
}
|
|
@@ -2924,7 +2970,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2924
2970
|
permissions: r.permissions
|
|
2925
2971
|
}));
|
|
2926
2972
|
const userResult = await db.execute(
|
|
2927
|
-
|
|
2973
|
+
sql7`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2928
2974
|
);
|
|
2929
2975
|
const userRow = userResult.rows[0];
|
|
2930
2976
|
return c.json({
|
|
@@ -2950,7 +2996,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2950
2996
|
}
|
|
2951
2997
|
|
|
2952
2998
|
// src/routes/admin/schema.ts
|
|
2953
|
-
import { sql as
|
|
2999
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
2954
3000
|
function liveFields(fields) {
|
|
2955
3001
|
return fields.filter((f) => f.removed !== true);
|
|
2956
3002
|
}
|
|
@@ -2990,7 +3036,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2990
3036
|
const data = await Promise.all(
|
|
2991
3037
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2992
3038
|
const result = await db.execute(
|
|
2993
|
-
|
|
3039
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${ct.db.table_name}"`)}`
|
|
2994
3040
|
);
|
|
2995
3041
|
const count = Number(result.rows[0].count);
|
|
2996
3042
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -3002,7 +3048,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3002
3048
|
const data = await Promise.all(
|
|
3003
3049
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
3004
3050
|
const result = await db.execute(
|
|
3005
|
-
|
|
3051
|
+
sql8`SELECT COUNT(*)::int AS count FROM ${sql8.raw(`"${tt.db.table_name}"`)}`
|
|
3006
3052
|
);
|
|
3007
3053
|
const count = Number(result.rows[0].count);
|
|
3008
3054
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -3014,14 +3060,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3014
3060
|
|
|
3015
3061
|
// src/repositories/content.ts
|
|
3016
3062
|
import crypto from "crypto";
|
|
3017
|
-
import { sql as
|
|
3063
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3018
3064
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
3019
3065
|
function codeError(code, message) {
|
|
3020
3066
|
const err = new Error(message);
|
|
3021
3067
|
err.code = code;
|
|
3022
3068
|
return err;
|
|
3023
3069
|
}
|
|
3024
|
-
function
|
|
3070
|
+
function quoteIdent4(name) {
|
|
3025
3071
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
3026
3072
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
3027
3073
|
}
|
|
@@ -3036,43 +3082,43 @@ function quoteField(name) {
|
|
|
3036
3082
|
function buildConditions(published_only, filters, search) {
|
|
3037
3083
|
const conds = [];
|
|
3038
3084
|
if (published_only) {
|
|
3039
|
-
conds.push(
|
|
3085
|
+
conds.push(sql9`published = true`);
|
|
3040
3086
|
}
|
|
3041
3087
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
3042
3088
|
const pattern = `%${search.term.trim()}%`;
|
|
3043
|
-
const orConds = search.columns.map((col) =>
|
|
3044
|
-
conds.push(
|
|
3089
|
+
const orConds = search.columns.map((col) => sql9`${sql9.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
3090
|
+
conds.push(sql9`(${sql9.join(orConds, sql9` OR `)})`);
|
|
3045
3091
|
}
|
|
3046
3092
|
if (filters) {
|
|
3047
3093
|
for (const [field, value] of Object.entries(filters)) {
|
|
3048
|
-
const col =
|
|
3094
|
+
const col = sql9.raw(quoteField(field));
|
|
3049
3095
|
if (Array.isArray(value)) {
|
|
3050
3096
|
if (value.length === 0) continue;
|
|
3051
|
-
const inList =
|
|
3052
|
-
value.map((v) =>
|
|
3053
|
-
|
|
3097
|
+
const inList = sql9.join(
|
|
3098
|
+
value.map((v) => sql9`${v}`),
|
|
3099
|
+
sql9`, `
|
|
3054
3100
|
);
|
|
3055
|
-
conds.push(
|
|
3101
|
+
conds.push(sql9`${col} IN (${inList})`);
|
|
3056
3102
|
} else if (typeof value === "object" && value !== null) {
|
|
3057
3103
|
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(
|
|
3104
|
+
if (op.gt !== void 0) conds.push(sql9`${col} > ${op.gt}`);
|
|
3105
|
+
if (op.gte !== void 0) conds.push(sql9`${col} >= ${op.gte}`);
|
|
3106
|
+
if (op.lt !== void 0) conds.push(sql9`${col} < ${op.lt}`);
|
|
3107
|
+
if (op.lte !== void 0) conds.push(sql9`${col} <= ${op.lte}`);
|
|
3062
3108
|
} else {
|
|
3063
|
-
conds.push(
|
|
3109
|
+
conds.push(sql9`${col} = ${value}`);
|
|
3064
3110
|
}
|
|
3065
3111
|
}
|
|
3066
3112
|
}
|
|
3067
3113
|
return conds;
|
|
3068
3114
|
}
|
|
3069
3115
|
function whereFragment(conds) {
|
|
3070
|
-
return conds.length > 0 ?
|
|
3116
|
+
return conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
|
|
3071
3117
|
}
|
|
3072
3118
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
3073
3119
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
3074
3120
|
function tableRaw() {
|
|
3075
|
-
return
|
|
3121
|
+
return sql9.raw(quoteIdent4(tableName));
|
|
3076
3122
|
}
|
|
3077
3123
|
async function resolveRows(rows, include, cache) {
|
|
3078
3124
|
if (rows.length === 0) return rows;
|
|
@@ -3115,14 +3161,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3115
3161
|
const offset = (page - 1) * per_page;
|
|
3116
3162
|
const conds = buildConditions(published_only, filters, search);
|
|
3117
3163
|
const where = whereFragment(conds);
|
|
3118
|
-
const sortCol =
|
|
3119
|
-
const sortDir =
|
|
3164
|
+
const sortCol = sql9.raw(quoteIdent4(sort_by));
|
|
3165
|
+
const sortDir = sql9.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
3120
3166
|
const countResult = await db.execute(
|
|
3121
|
-
|
|
3167
|
+
sql9`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
3122
3168
|
);
|
|
3123
3169
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
3124
3170
|
const dataResult = await db.execute(
|
|
3125
|
-
|
|
3171
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
3126
3172
|
);
|
|
3127
3173
|
const rows = dataResult.rows;
|
|
3128
3174
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3143,7 +3189,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3143
3189
|
},
|
|
3144
3190
|
async findOne(id, include = []) {
|
|
3145
3191
|
const result = await db.execute(
|
|
3146
|
-
|
|
3192
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3147
3193
|
);
|
|
3148
3194
|
if (result.rows.length === 0) return null;
|
|
3149
3195
|
const rows = result.rows;
|
|
@@ -3153,7 +3199,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3153
3199
|
},
|
|
3154
3200
|
async findBySlug(slug, include = []) {
|
|
3155
3201
|
const result = await db.execute(
|
|
3156
|
-
|
|
3202
|
+
sql9`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3157
3203
|
);
|
|
3158
3204
|
if (result.rows.length === 0) return null;
|
|
3159
3205
|
const rows = result.rows;
|
|
@@ -3169,10 +3215,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3169
3215
|
updated_at: /* @__PURE__ */ new Date()
|
|
3170
3216
|
};
|
|
3171
3217
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3172
|
-
const colsSql =
|
|
3173
|
-
const valsSql =
|
|
3218
|
+
const colsSql = sql9.join(entries.map(([k]) => sql9.raw(quoteIdent4(k))), sql9`, `);
|
|
3219
|
+
const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
|
|
3174
3220
|
const result = await db.execute(
|
|
3175
|
-
|
|
3221
|
+
sql9`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3176
3222
|
);
|
|
3177
3223
|
return result.rows[0];
|
|
3178
3224
|
},
|
|
@@ -3185,25 +3231,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3185
3231
|
if (entries.length === 0) {
|
|
3186
3232
|
return this.findOne(id);
|
|
3187
3233
|
}
|
|
3188
|
-
const setClauses =
|
|
3189
|
-
entries.map(([k, v]) =>
|
|
3190
|
-
|
|
3234
|
+
const setClauses = sql9.join(
|
|
3235
|
+
entries.map(([k, v]) => sql9`${sql9.raw(quoteIdent4(k))} = ${v}`),
|
|
3236
|
+
sql9`, `
|
|
3191
3237
|
);
|
|
3192
3238
|
const result = await db.execute(
|
|
3193
|
-
|
|
3239
|
+
sql9`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3194
3240
|
);
|
|
3195
3241
|
if (result.rows.length === 0) return null;
|
|
3196
3242
|
return result.rows[0];
|
|
3197
3243
|
},
|
|
3198
3244
|
async delete(id) {
|
|
3199
|
-
await db.execute(
|
|
3245
|
+
await db.execute(sql9`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3200
3246
|
},
|
|
3201
3247
|
async findAll(opts = {}) {
|
|
3202
3248
|
const { include = [], published_only } = opts;
|
|
3203
3249
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3204
3250
|
const where = whereFragment(conds);
|
|
3205
3251
|
const result = await db.execute(
|
|
3206
|
-
|
|
3252
|
+
sql9`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3207
3253
|
);
|
|
3208
3254
|
const rows = result.rows;
|
|
3209
3255
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3215,23 +3261,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3215
3261
|
|
|
3216
3262
|
// src/repositories/media.ts
|
|
3217
3263
|
import crypto2 from "crypto";
|
|
3218
|
-
import { sql as
|
|
3264
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
3219
3265
|
function createMediaRepository(db) {
|
|
3220
|
-
const TABLE =
|
|
3266
|
+
const TABLE = sql10.raw('"media"');
|
|
3221
3267
|
return {
|
|
3222
3268
|
async findMany(opts = {}) {
|
|
3223
3269
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3224
3270
|
const offset = (page - 1) * per_page;
|
|
3225
3271
|
const conds = [];
|
|
3226
|
-
if (type) conds.push(
|
|
3227
|
-
if (orphaned) conds.push(
|
|
3228
|
-
const where = conds.length > 0 ?
|
|
3229
|
-
const countResult = await db.execute(
|
|
3272
|
+
if (type) conds.push(sql10`type = ${type}`);
|
|
3273
|
+
if (orphaned) conds.push(sql10`reference_count = 0`);
|
|
3274
|
+
const where = conds.length > 0 ? sql10` WHERE ${sql10.join(conds, sql10` AND `)}` : sql10``;
|
|
3275
|
+
const countResult = await db.execute(sql10`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3230
3276
|
const total = Number(
|
|
3231
3277
|
countResult.rows[0]?.["total"] ?? 0
|
|
3232
3278
|
);
|
|
3233
3279
|
const dataResult = await db.execute(
|
|
3234
|
-
|
|
3280
|
+
sql10`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3235
3281
|
);
|
|
3236
3282
|
const total_pages = Math.ceil(total / per_page);
|
|
3237
3283
|
return {
|
|
@@ -3249,7 +3295,7 @@ function createMediaRepository(db) {
|
|
|
3249
3295
|
},
|
|
3250
3296
|
async findOne(id) {
|
|
3251
3297
|
const result = await db.execute(
|
|
3252
|
-
|
|
3298
|
+
sql10`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3253
3299
|
);
|
|
3254
3300
|
if (result.rows.length === 0) return null;
|
|
3255
3301
|
return result.rows[0];
|
|
@@ -3263,10 +3309,10 @@ function createMediaRepository(db) {
|
|
|
3263
3309
|
updated_at: /* @__PURE__ */ new Date()
|
|
3264
3310
|
};
|
|
3265
3311
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3266
|
-
const colsSql =
|
|
3267
|
-
const valsSql =
|
|
3312
|
+
const colsSql = sql10.join(entries.map(([k]) => sql10.raw(`"${k}"`)), sql10`, `);
|
|
3313
|
+
const valsSql = sql10.join(entries.map(([, v]) => sql10`${v}`), sql10`, `);
|
|
3268
3314
|
const result = await db.execute(
|
|
3269
|
-
|
|
3315
|
+
sql10`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3270
3316
|
);
|
|
3271
3317
|
return result.rows[0];
|
|
3272
3318
|
},
|
|
@@ -3276,31 +3322,31 @@ function createMediaRepository(db) {
|
|
|
3276
3322
|
([k, v]) => k !== "id" && v !== void 0
|
|
3277
3323
|
);
|
|
3278
3324
|
if (entries.length === 0) return this.findOne(id);
|
|
3279
|
-
const setClauses =
|
|
3280
|
-
entries.map(([k, v]) =>
|
|
3281
|
-
|
|
3325
|
+
const setClauses = sql10.join(
|
|
3326
|
+
entries.map(([k, v]) => sql10`${sql10.raw(`"${k}"`)} = ${v}`),
|
|
3327
|
+
sql10`, `
|
|
3282
3328
|
);
|
|
3283
3329
|
const result = await db.execute(
|
|
3284
|
-
|
|
3330
|
+
sql10`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3285
3331
|
);
|
|
3286
3332
|
if (result.rows.length === 0) return null;
|
|
3287
3333
|
return result.rows[0];
|
|
3288
3334
|
},
|
|
3289
3335
|
async delete(id) {
|
|
3290
|
-
await db.execute(
|
|
3336
|
+
await db.execute(sql10`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3291
3337
|
},
|
|
3292
3338
|
async incrementReferenceCount(ids) {
|
|
3293
3339
|
if (ids.length === 0) return;
|
|
3294
|
-
const inList =
|
|
3340
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3295
3341
|
await db.execute(
|
|
3296
|
-
|
|
3342
|
+
sql10`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3297
3343
|
);
|
|
3298
3344
|
},
|
|
3299
3345
|
async decrementReferenceCount(ids) {
|
|
3300
3346
|
if (ids.length === 0) return;
|
|
3301
|
-
const inList =
|
|
3347
|
+
const inList = sql10.join(ids.map((id) => sql10`${id}`), sql10`, `);
|
|
3302
3348
|
await db.execute(
|
|
3303
|
-
|
|
3349
|
+
sql10`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3304
3350
|
);
|
|
3305
3351
|
}
|
|
3306
3352
|
};
|
|
@@ -3433,7 +3479,7 @@ function createCmsApp(options) {
|
|
|
3433
3479
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3434
3480
|
const getUserRole = async (userId) => {
|
|
3435
3481
|
const result = await db.execute(
|
|
3436
|
-
|
|
3482
|
+
sql11`SELECT r.name AS role
|
|
3437
3483
|
FROM users u
|
|
3438
3484
|
JOIN roles r ON r.id = u.role_id
|
|
3439
3485
|
WHERE u.id = ${userId}
|