@bobbykim/manguito-cms-api 0.9.2 → 0.9.4
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/{chunk-ITOSMGDT.mjs → chunk-DHOVO42E.mjs} +2 -2
- package/dist/{chunk-GUZZQZSN.mjs → chunk-EFTMAG7N.mjs} +207 -46
- package/dist/chunk-EFTMAG7N.mjs.map +1 -0
- package/dist/graphql/index.js +31 -16
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-5OTVPIAF.mjs +8 -0
- package/dist/index.js +349 -287
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -320
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-GUZZQZSN.mjs.map +0 -1
- package/dist/handler-MFF32JB4.mjs +0 -8
- /package/dist/{chunk-ITOSMGDT.mjs.map → chunk-DHOVO42E.mjs.map} +0 -0
- /package/dist/{handler-MFF32JB4.mjs.map → handler-5OTVPIAF.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildRelationsMap,
|
|
3
|
+
checkRelationInput,
|
|
4
|
+
checkRequiredInParagraphItems,
|
|
3
5
|
createFieldKeyMap,
|
|
4
6
|
createFieldKeyMapFromProjection,
|
|
5
7
|
deleteParagraphField,
|
|
8
|
+
findMissingReferences,
|
|
6
9
|
isColumnBacked,
|
|
10
|
+
isMissing,
|
|
11
|
+
isPlainObject,
|
|
7
12
|
junctionOrderBy,
|
|
8
13
|
oneOrMany,
|
|
14
|
+
ownParagraphRowIds,
|
|
9
15
|
paragraphItems,
|
|
16
|
+
paragraphItemsIn,
|
|
17
|
+
paragraphTypeOf,
|
|
10
18
|
persistJunctionField,
|
|
11
19
|
persistParagraphField,
|
|
12
20
|
resolveRelationBareIds,
|
|
13
21
|
resolveRelationField
|
|
14
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-EFTMAG7N.mjs";
|
|
15
23
|
|
|
16
24
|
// src/paths.ts
|
|
17
25
|
function normalizePrefix(prefix) {
|
|
@@ -39,7 +47,7 @@ function createVersionedPaths(prefix, version) {
|
|
|
39
47
|
|
|
40
48
|
// src/app.ts
|
|
41
49
|
import { Hono } from "hono";
|
|
42
|
-
import { sql as
|
|
50
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
43
51
|
|
|
44
52
|
// src/middleware/cors.ts
|
|
45
53
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -141,7 +149,7 @@ var ERROR_STATUS_MAP = {
|
|
|
141
149
|
var errorHandler = (err, c) => {
|
|
142
150
|
console.error(err.stack ?? err.message);
|
|
143
151
|
const raw = err.code;
|
|
144
|
-
const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw
|
|
152
|
+
const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw);
|
|
145
153
|
if (!deliberate) {
|
|
146
154
|
return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
|
|
147
155
|
}
|
|
@@ -732,16 +740,16 @@ function parseFilters(url, validFields, columnFor) {
|
|
|
732
740
|
}
|
|
733
741
|
|
|
734
742
|
// src/routes/content.ts
|
|
743
|
+
function pickProgrammatic(resolved, names) {
|
|
744
|
+
const out = {};
|
|
745
|
+
for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
735
748
|
function isPublished(item) {
|
|
736
749
|
return item["published"] === true;
|
|
737
750
|
}
|
|
738
751
|
function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
|
|
739
752
|
const forResolver = resolverProjectors ?? projectors;
|
|
740
|
-
function pickProgrammatic(resolved, names) {
|
|
741
|
-
const out = {};
|
|
742
|
-
for (const name of names) if (name in resolved) out[name] = resolved[name];
|
|
743
|
-
return out;
|
|
744
|
-
}
|
|
745
753
|
async function respondItem(row, typeName, programmatic) {
|
|
746
754
|
const data = projectRow(row, typeName, projectors);
|
|
747
755
|
if (!resolver?.hasSchema(typeName)) return data;
|
|
@@ -1050,7 +1058,7 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
|
1050
1058
|
}
|
|
1051
1059
|
|
|
1052
1060
|
// src/routes/admin/content.ts
|
|
1053
|
-
import { sql as
|
|
1061
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
1054
1062
|
|
|
1055
1063
|
// src/references-in-use.ts
|
|
1056
1064
|
import { sql as sql2 } from "drizzle-orm";
|
|
@@ -1062,25 +1070,42 @@ function quoteIdent(name) {
|
|
|
1062
1070
|
function isRequiredSingleReferenceTo(f, targetType) {
|
|
1063
1071
|
return f.field_type === "reference" && f.required && relationCardinality(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
|
|
1064
1072
|
}
|
|
1073
|
+
function requiredReferencesTo(owners, targetType) {
|
|
1074
|
+
return owners.flatMap(
|
|
1075
|
+
(owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1065
1078
|
async function findRequiredReferrers(db, registry, targetType, id) {
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
];
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1079
|
+
const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
|
|
1080
|
+
const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
|
|
1081
|
+
const itemFields = requiredReferencesTo(itemOwners, targetType);
|
|
1082
|
+
const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
|
|
1083
|
+
const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
|
|
1084
|
+
const [itemCounts, paragraphCounts] = await Promise.all([
|
|
1085
|
+
Promise.all(
|
|
1086
|
+
itemFields.map(async ({ owner, f }) => {
|
|
1087
|
+
const notSelf = owner.name === targetType ? sql2` AND id <> ${id}` : sql2``;
|
|
1088
|
+
const r = await db.execute(
|
|
1089
|
+
sql2`SELECT count(*)::int AS n FROM ${sql2.raw(quoteIdent(owner.db.table_name))} WHERE ${sql2.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
|
|
1090
|
+
);
|
|
1091
|
+
return { owner, f, n: r.rows[0].n, own: 0 };
|
|
1092
|
+
})
|
|
1093
|
+
),
|
|
1094
|
+
Promise.all(
|
|
1095
|
+
paragraphFields.map(async ({ owner, f }) => {
|
|
1096
|
+
const r = await db.execute(
|
|
1097
|
+
sql2`SELECT count(*)::int AS n, count(*) FILTER (WHERE id = ANY(${sql2.param(ownIds)}::uuid[]))::int AS own FROM ${sql2.raw(quoteIdent(owner.db.table_name))} WHERE ${sql2.raw(quoteIdent(f.db_column.column_name))} = ${id}`
|
|
1098
|
+
);
|
|
1099
|
+
const { n, own } = r.rows[0];
|
|
1100
|
+
return { owner, f, n, own };
|
|
1101
|
+
})
|
|
1102
|
+
)
|
|
1103
|
+
]);
|
|
1104
|
+
const others = [];
|
|
1105
|
+
for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
|
|
1106
|
+
if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
|
|
1082
1107
|
}
|
|
1083
|
-
return
|
|
1108
|
+
return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
|
|
1084
1109
|
}
|
|
1085
1110
|
function inUseMessage(referrers) {
|
|
1086
1111
|
const total = referrers.reduce((sum, r) => sum + r.count, 0);
|
|
@@ -1098,7 +1123,7 @@ function isForeignKeyViolation(err) {
|
|
|
1098
1123
|
}
|
|
1099
1124
|
|
|
1100
1125
|
// src/routes/admin/content.ts
|
|
1101
|
-
import { relationCardinality as
|
|
1126
|
+
import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
|
|
1102
1127
|
|
|
1103
1128
|
// src/media-references.ts
|
|
1104
1129
|
function extractMediaId(value) {
|
|
@@ -1110,7 +1135,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
|
|
|
1110
1135
|
for (const f of mediaFields) {
|
|
1111
1136
|
const key = f.db_column?.column_name;
|
|
1112
1137
|
if (key === void 0 || key === "") continue;
|
|
1113
|
-
if (after !== null && !(key
|
|
1138
|
+
if (after !== null && !Object.hasOwn(after, key)) continue;
|
|
1114
1139
|
const oldId = before ? extractMediaId(before[key]) : null;
|
|
1115
1140
|
const newId = after ? extractMediaId(after[key]) : null;
|
|
1116
1141
|
if (newId === oldId) continue;
|
|
@@ -1134,131 +1159,6 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1134
1159
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
1135
1160
|
}
|
|
1136
1161
|
|
|
1137
|
-
// src/relation-input.ts
|
|
1138
|
-
import { sql as sql3 } from "drizzle-orm";
|
|
1139
|
-
import { relationCardinality as relationCardinality2 } from "@bobbykim/manguito-cms-core";
|
|
1140
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1141
|
-
function isUuid(v) {
|
|
1142
|
-
return typeof v === "string" && UUID_RE.test(v);
|
|
1143
|
-
}
|
|
1144
|
-
function isPlainObject(v) {
|
|
1145
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1146
|
-
}
|
|
1147
|
-
function checkRelationInput(fields, body, registry, path = "") {
|
|
1148
|
-
const errors = [];
|
|
1149
|
-
for (const field of fields) {
|
|
1150
|
-
if (!(field.name in body)) continue;
|
|
1151
|
-
const cardinality = relationCardinality2(field);
|
|
1152
|
-
if (cardinality === null) continue;
|
|
1153
|
-
const value = body[field.name];
|
|
1154
|
-
const name = `${path}${field.name}`;
|
|
1155
|
-
if (field.field_type === "reference") {
|
|
1156
|
-
const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
|
|
1157
|
-
if (!ok) {
|
|
1158
|
-
errors.push({
|
|
1159
|
-
field: name,
|
|
1160
|
-
message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
|
|
1166
|
-
if (items === void 0) {
|
|
1167
|
-
errors.push({
|
|
1168
|
-
field: name,
|
|
1169
|
-
message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
|
|
1170
|
-
});
|
|
1171
|
-
continue;
|
|
1172
|
-
}
|
|
1173
|
-
const pType = paragraphTypeOf(field, registry);
|
|
1174
|
-
if (!pType) continue;
|
|
1175
|
-
items.forEach((item, i) => {
|
|
1176
|
-
const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
|
|
1177
|
-
errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
|
|
1178
|
-
});
|
|
1179
|
-
}
|
|
1180
|
-
return errors;
|
|
1181
|
-
}
|
|
1182
|
-
function paragraphTypeOf(field, registry) {
|
|
1183
|
-
const comp = field.ui_component;
|
|
1184
|
-
return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
|
|
1185
|
-
}
|
|
1186
|
-
function paragraphItemsIn(fields, body, registry, path) {
|
|
1187
|
-
const out = [];
|
|
1188
|
-
for (const field of fields) {
|
|
1189
|
-
if (field.field_type !== "paragraph" || !(field.name in body)) continue;
|
|
1190
|
-
const pType = paragraphTypeOf(field, registry);
|
|
1191
|
-
if (!pType) continue;
|
|
1192
|
-
const value = body[field.name];
|
|
1193
|
-
const name = `${path}${field.name}`;
|
|
1194
|
-
if (relationCardinality2(field) === "one") {
|
|
1195
|
-
if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
|
|
1196
|
-
} else if (Array.isArray(value)) {
|
|
1197
|
-
value.forEach((item, i) => {
|
|
1198
|
-
if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
return out;
|
|
1203
|
-
}
|
|
1204
|
-
function isMissing(value) {
|
|
1205
|
-
if (value === null || value === void 0) return true;
|
|
1206
|
-
if (typeof value === "string" && value.trim() === "") return true;
|
|
1207
|
-
if (Array.isArray(value) && value.length === 0) return true;
|
|
1208
|
-
return false;
|
|
1209
|
-
}
|
|
1210
|
-
function checkRequiredInParagraphItems(fields, body, registry, path = "") {
|
|
1211
|
-
const errors = [];
|
|
1212
|
-
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1213
|
-
for (const f of itemFields) {
|
|
1214
|
-
if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
|
|
1215
|
-
if (isMissing(item[f.name])) {
|
|
1216
|
-
errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
|
|
1220
|
-
}
|
|
1221
|
-
return errors;
|
|
1222
|
-
}
|
|
1223
|
-
function quoteIdent2(name) {
|
|
1224
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1225
|
-
return `"${name}"`;
|
|
1226
|
-
}
|
|
1227
|
-
function collectReferences(fields, body, registry, path, out) {
|
|
1228
|
-
for (const f of fields) {
|
|
1229
|
-
if (!(f.name in body) || !f.db_column) continue;
|
|
1230
|
-
const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
|
|
1231
|
-
if (!table) continue;
|
|
1232
|
-
const value = body[f.name];
|
|
1233
|
-
const name = `${path}${f.name}`;
|
|
1234
|
-
if (typeof value === "string") {
|
|
1235
|
-
out.push({ table, id: value, field: name });
|
|
1236
|
-
} else if (Array.isArray(value)) {
|
|
1237
|
-
value.forEach((v, i) => {
|
|
1238
|
-
if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
|
|
1243
|
-
collectReferences(itemFields, item, registry, prefix, out);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
async function findMissingReferences(db, fields, body, registry) {
|
|
1247
|
-
const uses = [];
|
|
1248
|
-
collectReferences(fields, body, registry, "", uses);
|
|
1249
|
-
if (uses.length === 0) return [];
|
|
1250
|
-
const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
|
|
1251
|
-
const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
|
|
1252
|
-
if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
|
|
1253
|
-
const result = await db.execute(
|
|
1254
|
-
sql3`SELECT id FROM ${sql3.raw(quoteIdent2(table))} WHERE id IN (${sql3.join(ids.map((id) => sql3`${id}`), sql3`, `)})`
|
|
1255
|
-
);
|
|
1256
|
-
return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
|
|
1257
|
-
});
|
|
1258
|
-
const found = new Map(await Promise.all(lookups));
|
|
1259
|
-
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.` }));
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
1162
|
// src/field-values.ts
|
|
1263
1163
|
var VARCHAR_DEFAULT_LIMIT = 255;
|
|
1264
1164
|
var INTEGER_MIN = -2147483648;
|
|
@@ -1270,7 +1170,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
|
|
|
1270
1170
|
function emptyValuesToNull(fields, body, registry) {
|
|
1271
1171
|
const out = { ...body };
|
|
1272
1172
|
for (const f of fields) {
|
|
1273
|
-
if (!(f.name
|
|
1173
|
+
if (!Object.hasOwn(out, f.name)) continue;
|
|
1274
1174
|
const value = out[f.name];
|
|
1275
1175
|
if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
|
|
1276
1176
|
out[f.name] = null;
|
|
@@ -1375,7 +1275,7 @@ function rangeError(name, value, min, max) {
|
|
|
1375
1275
|
function checkFieldValues(fields, body, registry, path = "") {
|
|
1376
1276
|
const errors = [];
|
|
1377
1277
|
for (const f of fields) {
|
|
1378
|
-
if (!(f.name
|
|
1278
|
+
if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
|
|
1379
1279
|
const name = `${path}${f.name}`;
|
|
1380
1280
|
const message = valueError(f, body[f.name], name);
|
|
1381
1281
|
if (message) errors.push({ field: name, message });
|
|
@@ -1387,24 +1287,22 @@ function checkFieldValues(fields, body, registry, path = "") {
|
|
|
1387
1287
|
}
|
|
1388
1288
|
|
|
1389
1289
|
// src/routes/admin/content.ts
|
|
1390
|
-
function
|
|
1290
|
+
function quoteIdent2(name) {
|
|
1391
1291
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1392
1292
|
return `"${name}"`;
|
|
1393
1293
|
}
|
|
1394
1294
|
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1395
1295
|
const result = await db.execute(
|
|
1396
|
-
|
|
1296
|
+
sql3`SELECT * FROM ${sql3.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
|
|
1397
1297
|
);
|
|
1398
1298
|
const rows = result.rows;
|
|
1399
1299
|
for (const nf of pType.fields) {
|
|
1400
1300
|
if (nf.db_column !== null) continue;
|
|
1401
|
-
const
|
|
1402
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1403
|
-
const nType = registry.paragraph_types[comp.ref];
|
|
1301
|
+
const nType = paragraphTypeOf(nf, registry);
|
|
1404
1302
|
if (!nType) continue;
|
|
1405
1303
|
for (const row of rows) {
|
|
1406
1304
|
row[nf.name] = oneOrMany(
|
|
1407
|
-
|
|
1305
|
+
relationCardinality2(nf) ?? "many",
|
|
1408
1306
|
await loadParagraphRows(db, registry, nType, row["id"], nf.name)
|
|
1409
1307
|
);
|
|
1410
1308
|
}
|
|
@@ -1412,35 +1310,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
|
1412
1310
|
return rows;
|
|
1413
1311
|
}
|
|
1414
1312
|
async function loadRelationsForEdit(db, registry, fields, id, row) {
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1313
|
+
const loaded = await Promise.all(
|
|
1314
|
+
fields.map(async (f) => {
|
|
1315
|
+
if (f.db_column === null) {
|
|
1316
|
+
const pType = paragraphTypeOf(f, registry);
|
|
1317
|
+
if (!pType) return null;
|
|
1318
|
+
return [f.name, oneOrMany(relationCardinality2(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
|
|
1319
|
+
}
|
|
1320
|
+
if (f.db_column.junction) {
|
|
1321
|
+
const j = f.db_column.junction;
|
|
1322
|
+
const r = await db.execute(
|
|
1323
|
+
sql3`SELECT ${sql3.raw(quoteIdent2(j.right_column))} FROM ${sql3.raw(quoteIdent2(j.table_name))} WHERE ${sql3.raw(quoteIdent2(j.left_column))} = ${id}${junctionOrderBy(j)}`
|
|
1324
|
+
);
|
|
1325
|
+
return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
|
|
1326
|
+
}
|
|
1327
|
+
return null;
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
|
|
1430
1331
|
}
|
|
1431
1332
|
async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
|
|
1432
1333
|
const stored = { ...storedLabels };
|
|
1433
|
-
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name
|
|
1334
|
+
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
|
|
1434
1335
|
return { ...stored, ...body };
|
|
1435
1336
|
}
|
|
1436
1337
|
async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
|
|
1437
1338
|
const deltas = [];
|
|
1438
1339
|
for (const f of fields) {
|
|
1439
|
-
if (!(f.name
|
|
1340
|
+
if (!Object.hasOwn(body, f.name)) continue;
|
|
1440
1341
|
if (f.db_column === null) {
|
|
1441
|
-
const
|
|
1442
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1443
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1342
|
+
const pType = paragraphTypeOf(f, registry);
|
|
1444
1343
|
if (!pType) continue;
|
|
1445
1344
|
deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
|
|
1446
1345
|
} else if (f.db_column.junction) {
|
|
@@ -1451,9 +1350,49 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
|
|
|
1451
1350
|
}
|
|
1452
1351
|
return deltas;
|
|
1453
1352
|
}
|
|
1353
|
+
async function deleteOwnParagraphs(db, registry, fields, itemId) {
|
|
1354
|
+
const deltas = [];
|
|
1355
|
+
for (const f of fields) {
|
|
1356
|
+
const pType = paragraphTypeOf(f, registry);
|
|
1357
|
+
if (!pType) continue;
|
|
1358
|
+
deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
|
|
1359
|
+
}
|
|
1360
|
+
return deltas;
|
|
1361
|
+
}
|
|
1362
|
+
function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
|
|
1363
|
+
const stillHeld = new Set(topLevel.removed);
|
|
1364
|
+
return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
|
|
1365
|
+
}
|
|
1366
|
+
async function deleteItemCascade(db, registry, mediaRepo, target) {
|
|
1367
|
+
const { typeName, fields, mediaFields, repo, id, row } = target;
|
|
1368
|
+
const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
|
|
1369
|
+
if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
|
|
1370
|
+
const topLevel = topLevelMediaDelta(mediaFields, row, null);
|
|
1371
|
+
if (!db || !referrers.fromOwnParagraphs) {
|
|
1372
|
+
try {
|
|
1373
|
+
await repo.delete(id);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
|
|
1376
|
+
throw err;
|
|
1377
|
+
}
|
|
1378
|
+
const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
|
|
1379
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
|
|
1380
|
+
return { ok: true };
|
|
1381
|
+
}
|
|
1382
|
+
const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
|
|
1383
|
+
try {
|
|
1384
|
+
await repo.delete(id);
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
|
|
1387
|
+
if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
|
|
1388
|
+
throw err;
|
|
1389
|
+
}
|
|
1390
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
|
|
1391
|
+
return { ok: true };
|
|
1392
|
+
}
|
|
1454
1393
|
async function lookupBasePathId(db, pathOrName) {
|
|
1455
1394
|
const r = await db.execute(
|
|
1456
|
-
|
|
1395
|
+
sql3`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1457
1396
|
);
|
|
1458
1397
|
return r.rows[0]?.id ?? null;
|
|
1459
1398
|
}
|
|
@@ -1560,7 +1499,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1560
1499
|
const mediaFields = contentType.fields.filter(
|
|
1561
1500
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
1562
1501
|
);
|
|
1563
|
-
const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
|
|
1564
1502
|
const searchableColumns = [
|
|
1565
1503
|
...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
|
|
1566
1504
|
...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
|
|
@@ -1710,7 +1648,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1710
1648
|
}
|
|
1711
1649
|
for (const f of columnBackedFields) {
|
|
1712
1650
|
const key = f.db_column.column_name;
|
|
1713
|
-
if (!(key
|
|
1651
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
1714
1652
|
columnData[key] = storageBody[key];
|
|
1715
1653
|
}
|
|
1716
1654
|
if (db) {
|
|
@@ -1752,7 +1690,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1752
1690
|
404
|
|
1753
1691
|
);
|
|
1754
1692
|
}
|
|
1755
|
-
if (!contentType.only_one && "slug"
|
|
1693
|
+
if (!contentType.only_one && Object.hasOwn(body, "slug")) {
|
|
1756
1694
|
const slug = body["slug"];
|
|
1757
1695
|
if (typeof slug !== "string" || slug.trim() === "") {
|
|
1758
1696
|
return c.json(
|
|
@@ -1822,11 +1760,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1822
1760
|
}
|
|
1823
1761
|
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
1824
1762
|
const patchData = {};
|
|
1825
|
-
if (!contentType.only_one && "slug"
|
|
1826
|
-
if ("published"
|
|
1763
|
+
if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
|
|
1764
|
+
if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
|
|
1827
1765
|
for (const f of patchColumnBackedFields) {
|
|
1828
1766
|
const key = f.db_column.column_name;
|
|
1829
|
-
if (!(key
|
|
1767
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
1830
1768
|
patchData[key] = storageBody[key];
|
|
1831
1769
|
}
|
|
1832
1770
|
const updated = await repo.update(id, patchData);
|
|
@@ -1876,31 +1814,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1876
1814
|
404
|
|
1877
1815
|
);
|
|
1878
1816
|
}
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
}
|
|
1889
|
-
throw err;
|
|
1890
|
-
}
|
|
1891
|
-
const mediaDeltas = [
|
|
1892
|
-
topLevelMediaDelta(mediaFields, item, null)
|
|
1893
|
-
];
|
|
1894
|
-
if (db) {
|
|
1895
|
-
for (const f of paragraphFieldDefs) {
|
|
1896
|
-
const comp = f.ui_component;
|
|
1897
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1898
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1899
|
-
if (!pType) continue;
|
|
1900
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
1901
|
-
}
|
|
1817
|
+
const outcome = await deleteItemCascade(db, registry, mediaRepo, {
|
|
1818
|
+
typeName,
|
|
1819
|
+
fields: contentType.fields,
|
|
1820
|
+
mediaFields,
|
|
1821
|
+
repo,
|
|
1822
|
+
id,
|
|
1823
|
+
row: item
|
|
1824
|
+
});
|
|
1825
|
+
if (!outcome.ok) {
|
|
1826
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
|
|
1902
1827
|
}
|
|
1903
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1904
1828
|
return c.json({ ok: true });
|
|
1905
1829
|
}
|
|
1906
1830
|
);
|
|
@@ -1921,7 +1845,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
1921
1845
|
const mediaFields = taxonomyType.fields.filter(
|
|
1922
1846
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
1923
1847
|
);
|
|
1924
|
-
const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
1925
1848
|
const searchableColumns = [
|
|
1926
1849
|
...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
|
|
1927
1850
|
...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
|
|
@@ -2007,7 +1930,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2007
1930
|
};
|
|
2008
1931
|
for (const f of taxColumnBackedFields) {
|
|
2009
1932
|
const key = f.db_column.column_name;
|
|
2010
|
-
if (!(key
|
|
1933
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
2011
1934
|
taxColumnData[key] = storageBody[key];
|
|
2012
1935
|
}
|
|
2013
1936
|
const item = await repo.create(taxColumnData);
|
|
@@ -2059,10 +1982,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2059
1982
|
}
|
|
2060
1983
|
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
2061
1984
|
const taxPatchData = {};
|
|
2062
|
-
if ("published"
|
|
1985
|
+
if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
|
|
2063
1986
|
for (const f of taxPatchColumnBackedFields) {
|
|
2064
1987
|
const key = f.db_column.column_name;
|
|
2065
|
-
if (!(key
|
|
1988
|
+
if (!Object.hasOwn(storageBody, key)) continue;
|
|
2066
1989
|
taxPatchData[key] = storageBody[key];
|
|
2067
1990
|
}
|
|
2068
1991
|
const updated = await repo.update(id, taxPatchData);
|
|
@@ -2095,31 +2018,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
|
|
|
2095
2018
|
404
|
|
2096
2019
|
);
|
|
2097
2020
|
}
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
}
|
|
2108
|
-
throw err;
|
|
2109
|
-
}
|
|
2110
|
-
const mediaDeltas = [
|
|
2111
|
-
topLevelMediaDelta(mediaFields, item, null)
|
|
2112
|
-
];
|
|
2113
|
-
if (db) {
|
|
2114
|
-
for (const f of paragraphFieldDefs) {
|
|
2115
|
-
const comp = f.ui_component;
|
|
2116
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
2117
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
2118
|
-
if (!pType) continue;
|
|
2119
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
2120
|
-
}
|
|
2021
|
+
const outcome = await deleteItemCascade(db, registry, mediaRepo, {
|
|
2022
|
+
typeName,
|
|
2023
|
+
fields: taxonomyType.fields,
|
|
2024
|
+
mediaFields,
|
|
2025
|
+
repo,
|
|
2026
|
+
id,
|
|
2027
|
+
row: item
|
|
2028
|
+
});
|
|
2029
|
+
if (!outcome.ok) {
|
|
2030
|
+
return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
|
|
2121
2031
|
}
|
|
2122
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2123
2032
|
return c.json({ ok: true });
|
|
2124
2033
|
}
|
|
2125
2034
|
);
|
|
@@ -2581,7 +2490,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2581
2490
|
|
|
2582
2491
|
// src/routes/admin/auth.ts
|
|
2583
2492
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2584
|
-
import { sql as
|
|
2493
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2585
2494
|
|
|
2586
2495
|
// src/auth/password.ts
|
|
2587
2496
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2647,7 +2556,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2647
2556
|
);
|
|
2648
2557
|
}
|
|
2649
2558
|
const userResult = await db.execute(
|
|
2650
|
-
|
|
2559
|
+
sql4`SELECT id, email, password_hash, role_id, token_version
|
|
2651
2560
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2652
2561
|
);
|
|
2653
2562
|
const user = userResult.rows[0];
|
|
@@ -2660,7 +2569,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2660
2569
|
return c.json(invalidCredentials, 401);
|
|
2661
2570
|
}
|
|
2662
2571
|
const roleResult = await db.execute(
|
|
2663
|
-
|
|
2572
|
+
sql4`SELECT r.name AS name, u.must_change_password
|
|
2664
2573
|
FROM roles r
|
|
2665
2574
|
JOIN users u ON u.role_id = r.id
|
|
2666
2575
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2720,7 +2629,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2720
2629
|
);
|
|
2721
2630
|
}
|
|
2722
2631
|
const rowResult = await db.execute(
|
|
2723
|
-
|
|
2632
|
+
sql4`SELECT u.token_version, r.name AS role
|
|
2724
2633
|
FROM users u
|
|
2725
2634
|
JOIN roles r ON r.id = u.role_id
|
|
2726
2635
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2744,7 +2653,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2744
2653
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2745
2654
|
const user = c.get("user");
|
|
2746
2655
|
await db.execute(
|
|
2747
|
-
|
|
2656
|
+
sql4`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2748
2657
|
);
|
|
2749
2658
|
clearAuthCookie(c, "auth_token");
|
|
2750
2659
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2753,7 +2662,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2753
2662
|
}
|
|
2754
2663
|
|
|
2755
2664
|
// src/routes/admin/users.ts
|
|
2756
|
-
import { sql as
|
|
2665
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2757
2666
|
import { randomBytes } from "crypto";
|
|
2758
2667
|
function getActingUser(c) {
|
|
2759
2668
|
return c.get("user");
|
|
@@ -2788,7 +2697,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2788
2697
|
);
|
|
2789
2698
|
}
|
|
2790
2699
|
const hashResult = await db.execute(
|
|
2791
|
-
|
|
2700
|
+
sql5`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2792
2701
|
);
|
|
2793
2702
|
const hashRow = hashResult.rows[0];
|
|
2794
2703
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2799,7 +2708,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2799
2708
|
}
|
|
2800
2709
|
const newHash = await hashPassword(newPassword);
|
|
2801
2710
|
await db.execute(
|
|
2802
|
-
|
|
2711
|
+
sql5`UPDATE users
|
|
2803
2712
|
SET password_hash = ${newHash},
|
|
2804
2713
|
must_change_password = false,
|
|
2805
2714
|
token_version = token_version + 1,
|
|
@@ -2810,7 +2719,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2810
2719
|
});
|
|
2811
2720
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2812
2721
|
const result = await db.execute(
|
|
2813
|
-
|
|
2722
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2814
2723
|
u.created_at, u.updated_at
|
|
2815
2724
|
FROM users u
|
|
2816
2725
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2821,7 +2730,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2821
2730
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2822
2731
|
const id = c.req.param("id");
|
|
2823
2732
|
const result = await db.execute(
|
|
2824
|
-
|
|
2733
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2825
2734
|
u.created_at, u.updated_at
|
|
2826
2735
|
FROM users u
|
|
2827
2736
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2866,7 +2775,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2866
2775
|
);
|
|
2867
2776
|
}
|
|
2868
2777
|
const roleResult = await db.execute(
|
|
2869
|
-
|
|
2778
|
+
sql5`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2870
2779
|
);
|
|
2871
2780
|
const roleRow = roleResult.rows[0];
|
|
2872
2781
|
if (!roleRow) {
|
|
@@ -2881,7 +2790,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2881
2790
|
const tempPassword = generateTempPassword();
|
|
2882
2791
|
const passwordHash = await hashPassword(tempPassword);
|
|
2883
2792
|
const insertResult = await db.execute(
|
|
2884
|
-
|
|
2793
|
+
sql5`INSERT INTO users
|
|
2885
2794
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2886
2795
|
created_at, updated_at)
|
|
2887
2796
|
VALUES
|
|
@@ -2936,7 +2845,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2936
2845
|
);
|
|
2937
2846
|
}
|
|
2938
2847
|
const existingResult = await db.execute(
|
|
2939
|
-
|
|
2848
|
+
sql5`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2940
2849
|
);
|
|
2941
2850
|
const existing = existingResult.rows[0];
|
|
2942
2851
|
if (!existing) {
|
|
@@ -2950,7 +2859,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2950
2859
|
let changeRole = false;
|
|
2951
2860
|
if (typeof body.role === "string" && body.role) {
|
|
2952
2861
|
const roleResult = await db.execute(
|
|
2953
|
-
|
|
2862
|
+
sql5`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2954
2863
|
);
|
|
2955
2864
|
const roleRow = roleResult.rows[0];
|
|
2956
2865
|
if (!roleRow) {
|
|
@@ -2970,7 +2879,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2970
2879
|
}
|
|
2971
2880
|
if (changeRole) {
|
|
2972
2881
|
await db.execute(
|
|
2973
|
-
|
|
2882
|
+
sql5`UPDATE users
|
|
2974
2883
|
SET email = ${newEmail},
|
|
2975
2884
|
role_id = ${newRoleId},
|
|
2976
2885
|
token_version = token_version + 1,
|
|
@@ -2979,7 +2888,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2979
2888
|
);
|
|
2980
2889
|
} else {
|
|
2981
2890
|
await db.execute(
|
|
2982
|
-
|
|
2891
|
+
sql5`UPDATE users
|
|
2983
2892
|
SET email = ${newEmail},
|
|
2984
2893
|
role_id = ${newRoleId},
|
|
2985
2894
|
updated_at = NOW()
|
|
@@ -2987,7 +2896,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2987
2896
|
);
|
|
2988
2897
|
}
|
|
2989
2898
|
const updatedResult = await db.execute(
|
|
2990
|
-
|
|
2899
|
+
sql5`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2991
2900
|
u.created_at, u.updated_at
|
|
2992
2901
|
FROM users u
|
|
2993
2902
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -3024,7 +2933,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3024
2933
|
);
|
|
3025
2934
|
}
|
|
3026
2935
|
const result = await db.execute(
|
|
3027
|
-
|
|
2936
|
+
sql5`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
3028
2937
|
);
|
|
3029
2938
|
if (result.rows.length === 0) {
|
|
3030
2939
|
return c.json(
|
|
@@ -3055,7 +2964,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3055
2964
|
);
|
|
3056
2965
|
}
|
|
3057
2966
|
const existingResult = await db.execute(
|
|
3058
|
-
|
|
2967
|
+
sql5`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
3059
2968
|
);
|
|
3060
2969
|
const existing = existingResult.rows[0];
|
|
3061
2970
|
if (!existing) {
|
|
@@ -3067,7 +2976,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3067
2976
|
const tempPassword = generateTempPassword();
|
|
3068
2977
|
const passwordHash = await hashPassword(tempPassword);
|
|
3069
2978
|
await db.execute(
|
|
3070
|
-
|
|
2979
|
+
sql5`UPDATE users
|
|
3071
2980
|
SET password_hash = ${passwordHash},
|
|
3072
2981
|
must_change_password = true,
|
|
3073
2982
|
token_version = token_version + 1,
|
|
@@ -3080,10 +2989,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3080
2989
|
}
|
|
3081
2990
|
|
|
3082
2991
|
// src/routes/admin/config.ts
|
|
3083
|
-
import { sql as
|
|
2992
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
3084
2993
|
var API_VERSION = (() => {
|
|
3085
2994
|
try {
|
|
3086
|
-
return "0.9.
|
|
2995
|
+
return "0.9.4";
|
|
3087
2996
|
} catch {
|
|
3088
2997
|
return "0.0.0";
|
|
3089
2998
|
}
|
|
@@ -3102,7 +3011,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
3102
3011
|
permissions: r.permissions
|
|
3103
3012
|
}));
|
|
3104
3013
|
const userResult = await db.execute(
|
|
3105
|
-
|
|
3014
|
+
sql6`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
3106
3015
|
);
|
|
3107
3016
|
const userRow = userResult.rows[0];
|
|
3108
3017
|
return c.json({
|
|
@@ -3128,7 +3037,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
3128
3037
|
}
|
|
3129
3038
|
|
|
3130
3039
|
// src/routes/admin/schema.ts
|
|
3131
|
-
import { sql as
|
|
3040
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
3132
3041
|
function liveFields(fields) {
|
|
3133
3042
|
return fields.filter((f) => f.removed !== true);
|
|
3134
3043
|
}
|
|
@@ -3168,7 +3077,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3168
3077
|
const data = await Promise.all(
|
|
3169
3078
|
Object.values(registry.content_types).map(async (ct) => {
|
|
3170
3079
|
const result = await db.execute(
|
|
3171
|
-
|
|
3080
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${ct.db.table_name}"`)}`
|
|
3172
3081
|
);
|
|
3173
3082
|
const count = Number(result.rows[0].count);
|
|
3174
3083
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -3180,7 +3089,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3180
3089
|
const data = await Promise.all(
|
|
3181
3090
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
3182
3091
|
const result = await db.execute(
|
|
3183
|
-
|
|
3092
|
+
sql7`SELECT COUNT(*)::int AS count FROM ${sql7.raw(`"${tt.db.table_name}"`)}`
|
|
3184
3093
|
);
|
|
3185
3094
|
const count = Number(result.rows[0].count);
|
|
3186
3095
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -3192,14 +3101,14 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
3192
3101
|
|
|
3193
3102
|
// src/repositories/content.ts
|
|
3194
3103
|
import crypto from "crypto";
|
|
3195
|
-
import { sql as
|
|
3104
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
3196
3105
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
3197
3106
|
function codeError(code, message) {
|
|
3198
3107
|
const err = new Error(message);
|
|
3199
3108
|
err.code = code;
|
|
3200
3109
|
return err;
|
|
3201
3110
|
}
|
|
3202
|
-
function
|
|
3111
|
+
function quoteIdent3(name) {
|
|
3203
3112
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
3204
3113
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
3205
3114
|
}
|
|
@@ -3214,43 +3123,43 @@ function quoteField(name) {
|
|
|
3214
3123
|
function buildConditions(published_only, filters, search) {
|
|
3215
3124
|
const conds = [];
|
|
3216
3125
|
if (published_only) {
|
|
3217
|
-
conds.push(
|
|
3126
|
+
conds.push(sql8`published = true`);
|
|
3218
3127
|
}
|
|
3219
3128
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
3220
3129
|
const pattern = `%${search.term.trim()}%`;
|
|
3221
|
-
const orConds = search.columns.map((col) =>
|
|
3222
|
-
conds.push(
|
|
3130
|
+
const orConds = search.columns.map((col) => sql8`${sql8.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
3131
|
+
conds.push(sql8`(${sql8.join(orConds, sql8` OR `)})`);
|
|
3223
3132
|
}
|
|
3224
3133
|
if (filters) {
|
|
3225
3134
|
for (const [field, value] of Object.entries(filters)) {
|
|
3226
|
-
const col =
|
|
3135
|
+
const col = sql8.raw(quoteField(field));
|
|
3227
3136
|
if (Array.isArray(value)) {
|
|
3228
3137
|
if (value.length === 0) continue;
|
|
3229
|
-
const inList =
|
|
3230
|
-
value.map((v) =>
|
|
3231
|
-
|
|
3138
|
+
const inList = sql8.join(
|
|
3139
|
+
value.map((v) => sql8`${v}`),
|
|
3140
|
+
sql8`, `
|
|
3232
3141
|
);
|
|
3233
|
-
conds.push(
|
|
3142
|
+
conds.push(sql8`${col} IN (${inList})`);
|
|
3234
3143
|
} else if (typeof value === "object" && value !== null) {
|
|
3235
3144
|
const op = value;
|
|
3236
|
-
if (op.gt !== void 0) conds.push(
|
|
3237
|
-
if (op.gte !== void 0) conds.push(
|
|
3238
|
-
if (op.lt !== void 0) conds.push(
|
|
3239
|
-
if (op.lte !== void 0) conds.push(
|
|
3145
|
+
if (op.gt !== void 0) conds.push(sql8`${col} > ${op.gt}`);
|
|
3146
|
+
if (op.gte !== void 0) conds.push(sql8`${col} >= ${op.gte}`);
|
|
3147
|
+
if (op.lt !== void 0) conds.push(sql8`${col} < ${op.lt}`);
|
|
3148
|
+
if (op.lte !== void 0) conds.push(sql8`${col} <= ${op.lte}`);
|
|
3240
3149
|
} else {
|
|
3241
|
-
conds.push(
|
|
3150
|
+
conds.push(sql8`${col} = ${value}`);
|
|
3242
3151
|
}
|
|
3243
3152
|
}
|
|
3244
3153
|
}
|
|
3245
3154
|
return conds;
|
|
3246
3155
|
}
|
|
3247
3156
|
function whereFragment(conds) {
|
|
3248
|
-
return conds.length > 0 ?
|
|
3157
|
+
return conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
|
|
3249
3158
|
}
|
|
3250
3159
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
3251
3160
|
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
3252
3161
|
function tableRaw() {
|
|
3253
|
-
return
|
|
3162
|
+
return sql8.raw(quoteIdent3(tableName));
|
|
3254
3163
|
}
|
|
3255
3164
|
async function resolveRows(rows, include, cache) {
|
|
3256
3165
|
if (rows.length === 0) return rows;
|
|
@@ -3293,14 +3202,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3293
3202
|
const offset = (page - 1) * per_page;
|
|
3294
3203
|
const conds = buildConditions(published_only, filters, search);
|
|
3295
3204
|
const where = whereFragment(conds);
|
|
3296
|
-
const sortCol =
|
|
3297
|
-
const sortDir =
|
|
3205
|
+
const sortCol = sql8.raw(quoteIdent3(sort_by));
|
|
3206
|
+
const sortDir = sql8.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
3298
3207
|
const countResult = await db.execute(
|
|
3299
|
-
|
|
3208
|
+
sql8`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
3300
3209
|
);
|
|
3301
3210
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
3302
3211
|
const dataResult = await db.execute(
|
|
3303
|
-
|
|
3212
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
3304
3213
|
);
|
|
3305
3214
|
const rows = dataResult.rows;
|
|
3306
3215
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3321,7 +3230,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3321
3230
|
},
|
|
3322
3231
|
async findOne(id, include = []) {
|
|
3323
3232
|
const result = await db.execute(
|
|
3324
|
-
|
|
3233
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3325
3234
|
);
|
|
3326
3235
|
if (result.rows.length === 0) return null;
|
|
3327
3236
|
const rows = result.rows;
|
|
@@ -3331,7 +3240,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3331
3240
|
},
|
|
3332
3241
|
async findBySlug(slug, include = []) {
|
|
3333
3242
|
const result = await db.execute(
|
|
3334
|
-
|
|
3243
|
+
sql8`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3335
3244
|
);
|
|
3336
3245
|
if (result.rows.length === 0) return null;
|
|
3337
3246
|
const rows = result.rows;
|
|
@@ -3347,10 +3256,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3347
3256
|
updated_at: /* @__PURE__ */ new Date()
|
|
3348
3257
|
};
|
|
3349
3258
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3350
|
-
const colsSql =
|
|
3351
|
-
const valsSql =
|
|
3259
|
+
const colsSql = sql8.join(entries.map(([k]) => sql8.raw(quoteIdent3(k))), sql8`, `);
|
|
3260
|
+
const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
|
|
3352
3261
|
const result = await db.execute(
|
|
3353
|
-
|
|
3262
|
+
sql8`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3354
3263
|
);
|
|
3355
3264
|
return result.rows[0];
|
|
3356
3265
|
},
|
|
@@ -3363,25 +3272,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3363
3272
|
if (entries.length === 0) {
|
|
3364
3273
|
return this.findOne(id);
|
|
3365
3274
|
}
|
|
3366
|
-
const setClauses =
|
|
3367
|
-
entries.map(([k, v]) =>
|
|
3368
|
-
|
|
3275
|
+
const setClauses = sql8.join(
|
|
3276
|
+
entries.map(([k, v]) => sql8`${sql8.raw(quoteIdent3(k))} = ${v}`),
|
|
3277
|
+
sql8`, `
|
|
3369
3278
|
);
|
|
3370
3279
|
const result = await db.execute(
|
|
3371
|
-
|
|
3280
|
+
sql8`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3372
3281
|
);
|
|
3373
3282
|
if (result.rows.length === 0) return null;
|
|
3374
3283
|
return result.rows[0];
|
|
3375
3284
|
},
|
|
3376
3285
|
async delete(id) {
|
|
3377
|
-
await db.execute(
|
|
3286
|
+
await db.execute(sql8`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3378
3287
|
},
|
|
3379
3288
|
async findAll(opts = {}) {
|
|
3380
3289
|
const { include = [], published_only } = opts;
|
|
3381
3290
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3382
3291
|
const where = whereFragment(conds);
|
|
3383
3292
|
const result = await db.execute(
|
|
3384
|
-
|
|
3293
|
+
sql8`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3385
3294
|
);
|
|
3386
3295
|
const rows = result.rows;
|
|
3387
3296
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3393,23 +3302,23 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3393
3302
|
|
|
3394
3303
|
// src/repositories/media.ts
|
|
3395
3304
|
import crypto2 from "crypto";
|
|
3396
|
-
import { sql as
|
|
3305
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3397
3306
|
function createMediaRepository(db) {
|
|
3398
|
-
const TABLE =
|
|
3307
|
+
const TABLE = sql9.raw('"media"');
|
|
3399
3308
|
return {
|
|
3400
3309
|
async findMany(opts = {}) {
|
|
3401
3310
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3402
3311
|
const offset = (page - 1) * per_page;
|
|
3403
3312
|
const conds = [];
|
|
3404
|
-
if (type) conds.push(
|
|
3405
|
-
if (orphaned) conds.push(
|
|
3406
|
-
const where = conds.length > 0 ?
|
|
3407
|
-
const countResult = await db.execute(
|
|
3313
|
+
if (type) conds.push(sql9`type = ${type}`);
|
|
3314
|
+
if (orphaned) conds.push(sql9`reference_count = 0`);
|
|
3315
|
+
const where = conds.length > 0 ? sql9` WHERE ${sql9.join(conds, sql9` AND `)}` : sql9``;
|
|
3316
|
+
const countResult = await db.execute(sql9`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3408
3317
|
const total = Number(
|
|
3409
3318
|
countResult.rows[0]?.["total"] ?? 0
|
|
3410
3319
|
);
|
|
3411
3320
|
const dataResult = await db.execute(
|
|
3412
|
-
|
|
3321
|
+
sql9`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3413
3322
|
);
|
|
3414
3323
|
const total_pages = Math.ceil(total / per_page);
|
|
3415
3324
|
return {
|
|
@@ -3427,7 +3336,7 @@ function createMediaRepository(db) {
|
|
|
3427
3336
|
},
|
|
3428
3337
|
async findOne(id) {
|
|
3429
3338
|
const result = await db.execute(
|
|
3430
|
-
|
|
3339
|
+
sql9`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3431
3340
|
);
|
|
3432
3341
|
if (result.rows.length === 0) return null;
|
|
3433
3342
|
return result.rows[0];
|
|
@@ -3441,10 +3350,10 @@ function createMediaRepository(db) {
|
|
|
3441
3350
|
updated_at: /* @__PURE__ */ new Date()
|
|
3442
3351
|
};
|
|
3443
3352
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3444
|
-
const colsSql =
|
|
3445
|
-
const valsSql =
|
|
3353
|
+
const colsSql = sql9.join(entries.map(([k]) => sql9.raw(`"${k}"`)), sql9`, `);
|
|
3354
|
+
const valsSql = sql9.join(entries.map(([, v]) => sql9`${v}`), sql9`, `);
|
|
3446
3355
|
const result = await db.execute(
|
|
3447
|
-
|
|
3356
|
+
sql9`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3448
3357
|
);
|
|
3449
3358
|
return result.rows[0];
|
|
3450
3359
|
},
|
|
@@ -3454,31 +3363,31 @@ function createMediaRepository(db) {
|
|
|
3454
3363
|
([k, v]) => k !== "id" && v !== void 0
|
|
3455
3364
|
);
|
|
3456
3365
|
if (entries.length === 0) return this.findOne(id);
|
|
3457
|
-
const setClauses =
|
|
3458
|
-
entries.map(([k, v]) =>
|
|
3459
|
-
|
|
3366
|
+
const setClauses = sql9.join(
|
|
3367
|
+
entries.map(([k, v]) => sql9`${sql9.raw(`"${k}"`)} = ${v}`),
|
|
3368
|
+
sql9`, `
|
|
3460
3369
|
);
|
|
3461
3370
|
const result = await db.execute(
|
|
3462
|
-
|
|
3371
|
+
sql9`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3463
3372
|
);
|
|
3464
3373
|
if (result.rows.length === 0) return null;
|
|
3465
3374
|
return result.rows[0];
|
|
3466
3375
|
},
|
|
3467
3376
|
async delete(id) {
|
|
3468
|
-
await db.execute(
|
|
3377
|
+
await db.execute(sql9`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3469
3378
|
},
|
|
3470
3379
|
async incrementReferenceCount(ids) {
|
|
3471
3380
|
if (ids.length === 0) return;
|
|
3472
|
-
const inList =
|
|
3381
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3473
3382
|
await db.execute(
|
|
3474
|
-
|
|
3383
|
+
sql9`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3475
3384
|
);
|
|
3476
3385
|
},
|
|
3477
3386
|
async decrementReferenceCount(ids) {
|
|
3478
3387
|
if (ids.length === 0) return;
|
|
3479
|
-
const inList =
|
|
3388
|
+
const inList = sql9.join(ids.map((id) => sql9`${id}`), sql9`, `);
|
|
3480
3389
|
await db.execute(
|
|
3481
|
-
|
|
3390
|
+
sql9`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3482
3391
|
);
|
|
3483
3392
|
}
|
|
3484
3393
|
};
|
|
@@ -3611,7 +3520,7 @@ function createCmsApp(options) {
|
|
|
3611
3520
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3612
3521
|
const getUserRole = async (userId) => {
|
|
3613
3522
|
const result = await db.execute(
|
|
3614
|
-
|
|
3523
|
+
sql10`SELECT r.name AS role
|
|
3615
3524
|
FROM users u
|
|
3616
3525
|
JOIN roles r ON r.id = u.role_id
|
|
3617
3526
|
WHERE u.id = ${userId}
|
|
@@ -3781,7 +3690,7 @@ function createCmsApp(options) {
|
|
|
3781
3690
|
handler: null,
|
|
3782
3691
|
error: null
|
|
3783
3692
|
}));
|
|
3784
|
-
const ready = import("./handler-
|
|
3693
|
+
const ready = import("./handler-5OTVPIAF.mjs").then(({ createGraphQLHandler }) => {
|
|
3785
3694
|
gqlPasses.forEach((pass, i) => {
|
|
3786
3695
|
const mount = mounts[i];
|
|
3787
3696
|
try {
|