@bobbykim/manguito-cms-api 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FBS2RQI3.mjs +232 -0
- package/dist/chunk-FBS2RQI3.mjs.map +1 -0
- package/dist/chunk-QXMW74MR.mjs +589 -0
- package/dist/chunk-QXMW74MR.mjs.map +1 -0
- package/dist/graphql/index.d.mts +15 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +729 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/index.mjs +8 -0
- package/dist/graphql/index.mjs.map +1 -0
- package/dist/handler-GMCFYZY5.mjs +8 -0
- package/dist/handler-GMCFYZY5.mjs.map +1 -0
- package/dist/index.d.mts +7 -14
- package/dist/index.d.ts +7 -14
- package/dist/index.js +959 -253
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +184 -330
- package/dist/index.mjs.map +1 -1
- package/dist/resolve-Dtsk27aI.d.mts +15 -0
- package/dist/resolve-Dtsk27aI.d.ts +15 -0
- package/package.json +12 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRelationsMap,
|
|
3
|
+
deleteParagraphField,
|
|
4
|
+
persistJunctionField,
|
|
5
|
+
persistParagraphField,
|
|
6
|
+
resolveRelationBareIds,
|
|
7
|
+
resolveRelationField
|
|
8
|
+
} from "./chunk-FBS2RQI3.mjs";
|
|
9
|
+
|
|
1
10
|
// src/app.ts
|
|
2
11
|
import { Hono } from "hono";
|
|
3
|
-
import { sql as
|
|
12
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
4
13
|
|
|
5
14
|
// src/middleware/cors.ts
|
|
6
15
|
function createCorsMiddleware(corsConfig) {
|
|
@@ -31,26 +40,43 @@ function createCorsMiddleware(corsConfig) {
|
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
// src/middleware/security-headers.ts
|
|
43
|
+
var GRAPHIQL_CDN = "https://unpkg.com";
|
|
34
44
|
function createSecurityHeadersMiddleware(options = {}) {
|
|
35
45
|
const connectSrc = ["'self'", ...options.connectSrc ?? []].join(" ");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
function buildCsp(parts) {
|
|
47
|
+
return [
|
|
48
|
+
"default-src 'self'",
|
|
49
|
+
"img-src 'self' data: https:",
|
|
50
|
+
"media-src 'self' https:",
|
|
51
|
+
`style-src ${parts.styleSrc}`,
|
|
52
|
+
`script-src ${parts.scriptSrc}`,
|
|
53
|
+
"font-src 'self' data:",
|
|
54
|
+
`connect-src ${parts.connectSrc}`,
|
|
55
|
+
...parts.workerSrc ? [`worker-src ${parts.workerSrc}`] : [],
|
|
56
|
+
"frame-ancestors 'none'",
|
|
57
|
+
"base-uri 'self'",
|
|
58
|
+
"form-action 'self'"
|
|
59
|
+
].join("; ");
|
|
60
|
+
}
|
|
61
|
+
const csp = buildCsp({
|
|
62
|
+
scriptSrc: "'self'",
|
|
63
|
+
styleSrc: "'self' 'unsafe-inline'",
|
|
64
|
+
connectSrc
|
|
65
|
+
});
|
|
66
|
+
const graphiqlCsp = buildCsp({
|
|
67
|
+
scriptSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
68
|
+
styleSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
69
|
+
connectSrc: `${connectSrc} ${GRAPHIQL_CDN}`,
|
|
70
|
+
workerSrc: "'self' blob:"
|
|
71
|
+
});
|
|
72
|
+
const graphiqlPath = options.graphiqlPath;
|
|
48
73
|
return async function securityHeaders(c, next) {
|
|
49
74
|
await next();
|
|
50
75
|
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
51
76
|
c.res.headers.set("X-Frame-Options", "DENY");
|
|
52
77
|
c.res.headers.set("Referrer-Policy", "no-referrer");
|
|
53
|
-
c.
|
|
78
|
+
const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
|
|
79
|
+
c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
|
|
54
80
|
};
|
|
55
81
|
}
|
|
56
82
|
|
|
@@ -887,7 +913,7 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
887
913
|
}
|
|
888
914
|
|
|
889
915
|
// src/routes/admin/content.ts
|
|
890
|
-
import { sql as
|
|
916
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
891
917
|
|
|
892
918
|
// src/media-references.ts
|
|
893
919
|
function extractMediaId(value) {
|
|
@@ -921,235 +947,14 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
921
947
|
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
922
948
|
}
|
|
923
949
|
|
|
924
|
-
// src/relations.ts
|
|
925
|
-
import crypto from "crypto";
|
|
926
|
-
import { sql as sql2 } from "drizzle-orm";
|
|
927
|
-
function quoteIdent(name) {
|
|
928
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
929
|
-
return `"${name}"`;
|
|
930
|
-
}
|
|
931
|
-
function paragraphMediaFieldNames(pType) {
|
|
932
|
-
return new Set(
|
|
933
|
-
pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
|
|
937
|
-
const mediaColumns = pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.db_column.column_name);
|
|
938
|
-
if (mediaColumns.length === 0) return [];
|
|
939
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
940
|
-
const cols = sql2.join(mediaColumns.map((c) => sql2.raw(quoteIdent(c))), sql2`, `);
|
|
941
|
-
const result = await db.execute(
|
|
942
|
-
sql2`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
943
|
-
);
|
|
944
|
-
const ids = [];
|
|
945
|
-
for (const row of result.rows) {
|
|
946
|
-
for (const col of mediaColumns) {
|
|
947
|
-
const v = row[col];
|
|
948
|
-
if (typeof v === "string" && v !== "") ids.push(v);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
return ids;
|
|
952
|
-
}
|
|
953
|
-
async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
|
|
954
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
955
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
956
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
957
|
-
const mediaFieldNames = paragraphMediaFieldNames(pType);
|
|
958
|
-
const added = [];
|
|
959
|
-
for (let i = 0; i < items.length; i++) {
|
|
960
|
-
const pItem = items[i];
|
|
961
|
-
const data = {
|
|
962
|
-
id: crypto.randomUUID(),
|
|
963
|
-
parent_id: itemId,
|
|
964
|
-
parent_type: parentType,
|
|
965
|
-
parent_field: fieldName,
|
|
966
|
-
order: i,
|
|
967
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
968
|
-
updated_at: /* @__PURE__ */ new Date()
|
|
969
|
-
};
|
|
970
|
-
for (const pf of pType.fields) {
|
|
971
|
-
if (!pf.db_column || pf.db_column.junction) continue;
|
|
972
|
-
data[pf.db_column.column_name] = pItem[pf.name] ?? null;
|
|
973
|
-
}
|
|
974
|
-
const cols = sql2.join(Object.keys(data).map((k) => sql2.raw(quoteIdent(k))), sql2`, `);
|
|
975
|
-
const vals = sql2.join(Object.values(data).map((v) => sql2`${v}`), sql2`, `);
|
|
976
|
-
await db.execute(sql2`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
|
|
977
|
-
for (const name of mediaFieldNames) {
|
|
978
|
-
const val = pItem[name];
|
|
979
|
-
if (typeof val === "string" && val !== "") added.push(val);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
return { added, removed };
|
|
983
|
-
}
|
|
984
|
-
async function deleteParagraphField(db, itemId, fieldName, pType) {
|
|
985
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
986
|
-
const tbl = sql2.raw(quoteIdent(pType.db.table_name));
|
|
987
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
988
|
-
return { added: [], removed };
|
|
989
|
-
}
|
|
990
|
-
async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
991
|
-
const tbl = sql2.raw(quoteIdent(junction.table_name));
|
|
992
|
-
const left = sql2.raw(quoteIdent(junction.left_column));
|
|
993
|
-
const right = sql2.raw(quoteIdent(junction.right_column));
|
|
994
|
-
await db.execute(sql2`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
|
|
995
|
-
for (const rid of relatedIds) {
|
|
996
|
-
await db.execute(sql2`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
function buildRelationsMap(fields, registry) {
|
|
1000
|
-
const relations = {};
|
|
1001
|
-
for (const field of fields) {
|
|
1002
|
-
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
1003
|
-
if (!field.db_column) continue;
|
|
1004
|
-
relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
|
|
1005
|
-
} else if (field.field_type === "paragraph") {
|
|
1006
|
-
const ref = field.ui_component.ref;
|
|
1007
|
-
const pType = ref ? registry.paragraph_types[ref] : void 0;
|
|
1008
|
-
if (!pType) continue;
|
|
1009
|
-
relations[field.name] = { type: "paragraph", table: pType.db.table_name };
|
|
1010
|
-
} else if (field.field_type === "reference") {
|
|
1011
|
-
if (!field.db_column) continue;
|
|
1012
|
-
if (field.db_column.junction) {
|
|
1013
|
-
const j = field.db_column.junction;
|
|
1014
|
-
relations[field.name] = {
|
|
1015
|
-
type: "junction",
|
|
1016
|
-
table: j.right_table,
|
|
1017
|
-
junction_table: j.table_name,
|
|
1018
|
-
left_column: j.left_column,
|
|
1019
|
-
right_column: j.right_column,
|
|
1020
|
-
order_column: j.order_column
|
|
1021
|
-
};
|
|
1022
|
-
} else if (field.db_column.foreign_key) {
|
|
1023
|
-
relations[field.name] = {
|
|
1024
|
-
type: "reference",
|
|
1025
|
-
table: field.db_column.foreign_key.table,
|
|
1026
|
-
fk_column: field.db_column.column_name
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return relations;
|
|
1032
|
-
}
|
|
1033
|
-
function groupBy(items, key) {
|
|
1034
|
-
const result = {};
|
|
1035
|
-
for (const item of items) {
|
|
1036
|
-
const k = String(item[key]);
|
|
1037
|
-
if (!result[k]) result[k] = [];
|
|
1038
|
-
result[k].push(item);
|
|
1039
|
-
}
|
|
1040
|
-
return result;
|
|
1041
|
-
}
|
|
1042
|
-
async function resolveRelationField(db, rows, fieldName, rel, cache) {
|
|
1043
|
-
if (rows.length === 0) return;
|
|
1044
|
-
if (rel.type === "paragraph") {
|
|
1045
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1046
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1047
|
-
const result = await db.execute(
|
|
1048
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
|
|
1049
|
-
);
|
|
1050
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1051
|
-
for (const row of rows) {
|
|
1052
|
-
row[fieldName] = byParent[row["id"]] ?? [];
|
|
1053
|
-
}
|
|
1054
|
-
} else if (rel.type === "reference") {
|
|
1055
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1056
|
-
if (fkValues.length === 0) {
|
|
1057
|
-
for (const row of rows) row[fieldName] = null;
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
const unique = [...new Set(fkValues)];
|
|
1061
|
-
const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1062
|
-
if (uncached.length > 0) {
|
|
1063
|
-
const inList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1064
|
-
const result = await db.execute(
|
|
1065
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE id IN (${inList})`
|
|
1066
|
-
);
|
|
1067
|
-
for (const item of result.rows) {
|
|
1068
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
for (const row of rows) {
|
|
1072
|
-
const fkVal = row[rel.fk_column];
|
|
1073
|
-
row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
|
|
1074
|
-
}
|
|
1075
|
-
} else if (rel.type === "junction") {
|
|
1076
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1077
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1078
|
-
const orderBy = rel.order_column ? sql2` ORDER BY "order" ASC` : sql2``;
|
|
1079
|
-
const junctionResult = await db.execute(
|
|
1080
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.junction_table))} WHERE ${sql2.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
|
|
1081
|
-
);
|
|
1082
|
-
const jRows = junctionResult.rows;
|
|
1083
|
-
const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
|
|
1084
|
-
const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1085
|
-
if (uncached.length > 0) {
|
|
1086
|
-
const rightInList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1087
|
-
const entitiesResult = await db.execute(
|
|
1088
|
-
sql2`SELECT * FROM ${sql2.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})`
|
|
1089
|
-
);
|
|
1090
|
-
for (const item of entitiesResult.rows) {
|
|
1091
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
const byLeft = groupBy(jRows, rel.left_column);
|
|
1095
|
-
for (const row of rows) {
|
|
1096
|
-
const jEntries = byLeft[row["id"]] ?? [];
|
|
1097
|
-
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
1098
|
-
}
|
|
1099
|
-
} else if (rel.type === "media") {
|
|
1100
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1101
|
-
if (fkValues.length === 0) {
|
|
1102
|
-
for (const row of rows) row[fieldName] = null;
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const unique = [...new Set(fkValues)];
|
|
1106
|
-
const uncached = unique.filter((id) => !cache.has(`media:${id}`));
|
|
1107
|
-
if (uncached.length > 0) {
|
|
1108
|
-
const inList = sql2.join(uncached.map((id) => sql2`${id}`), sql2`, `);
|
|
1109
|
-
const result = await db.execute(sql2`SELECT * FROM "media" WHERE id IN (${inList})`);
|
|
1110
|
-
for (const item of result.rows) {
|
|
1111
|
-
cache.set(`media:${item["id"]}`, item);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
for (const row of rows) {
|
|
1115
|
-
const fkVal = row[rel.fk_column];
|
|
1116
|
-
row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
async function resolveRelationBareIds(db, rows, fieldName, rel) {
|
|
1121
|
-
if (rows.length === 0) return;
|
|
1122
|
-
if (rel.type === "paragraph") {
|
|
1123
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1124
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1125
|
-
const result = await db.execute(
|
|
1126
|
-
sql2`SELECT id, parent_id FROM ${sql2.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
|
|
1127
|
-
);
|
|
1128
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1129
|
-
for (const row of rows) {
|
|
1130
|
-
row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
|
|
1131
|
-
}
|
|
1132
|
-
} else if (rel.type === "junction") {
|
|
1133
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1134
|
-
const inList = sql2.join(parentIds.map((id) => sql2`${id}`), sql2`, `);
|
|
1135
|
-
const result = await db.execute(
|
|
1136
|
-
sql2`SELECT ${sql2.raw(quoteIdent(rel.left_column))} AS left_id, ${sql2.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql2.raw(quoteIdent(rel.junction_table))} WHERE ${sql2.raw(quoteIdent(rel.left_column))} IN (${inList})`
|
|
1137
|
-
);
|
|
1138
|
-
const byLeft = groupBy(result.rows, "left_id");
|
|
1139
|
-
for (const row of rows) {
|
|
1140
|
-
row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
950
|
// src/routes/admin/content.ts
|
|
1146
|
-
function
|
|
951
|
+
function quoteIdent(name) {
|
|
1147
952
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1148
953
|
return `"${name}"`;
|
|
1149
954
|
}
|
|
1150
955
|
async function lookupBasePathId(db, pathOrName) {
|
|
1151
956
|
const r = await db.execute(
|
|
1152
|
-
|
|
957
|
+
sql2`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
1153
958
|
);
|
|
1154
959
|
return r.rows[0]?.id ?? null;
|
|
1155
960
|
}
|
|
@@ -1299,13 +1104,13 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1299
1104
|
const pType = registry.paragraph_types[comp.ref];
|
|
1300
1105
|
if (!pType) continue;
|
|
1301
1106
|
const r = await db.execute(
|
|
1302
|
-
|
|
1107
|
+
sql2`SELECT * FROM ${sql2.raw(quoteIdent(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
|
|
1303
1108
|
);
|
|
1304
1109
|
row[f.name] = r.rows;
|
|
1305
1110
|
} else if (f.db_column.junction) {
|
|
1306
1111
|
const j = f.db_column.junction;
|
|
1307
1112
|
const r = await db.execute(
|
|
1308
|
-
|
|
1113
|
+
sql2`SELECT ${sql2.raw(quoteIdent(j.right_column))} FROM ${sql2.raw(quoteIdent(j.table_name))} WHERE ${sql2.raw(quoteIdent(j.left_column))} = ${id}`
|
|
1309
1114
|
);
|
|
1310
1115
|
row[f.name] = r.rows.map(
|
|
1311
1116
|
(row2) => row2[j.right_column]
|
|
@@ -2270,7 +2075,7 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
|
|
|
2270
2075
|
|
|
2271
2076
|
// src/routes/admin/auth.ts
|
|
2272
2077
|
import { getCookie as getCookie2 } from "hono/cookie";
|
|
2273
|
-
import { sql as
|
|
2078
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
2274
2079
|
|
|
2275
2080
|
// src/auth/password.ts
|
|
2276
2081
|
import { hashPassword, verifyPassword } from "@bobbykim/manguito-cms-core";
|
|
@@ -2336,7 +2141,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2336
2141
|
);
|
|
2337
2142
|
}
|
|
2338
2143
|
const userResult = await db.execute(
|
|
2339
|
-
|
|
2144
|
+
sql3`SELECT id, email, password_hash, role_id, token_version
|
|
2340
2145
|
FROM users WHERE email = ${email} LIMIT 1`
|
|
2341
2146
|
);
|
|
2342
2147
|
const user = userResult.rows[0];
|
|
@@ -2349,7 +2154,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2349
2154
|
return c.json(invalidCredentials, 401);
|
|
2350
2155
|
}
|
|
2351
2156
|
const roleResult = await db.execute(
|
|
2352
|
-
|
|
2157
|
+
sql3`SELECT r.name AS name, u.must_change_password
|
|
2353
2158
|
FROM roles r
|
|
2354
2159
|
JOIN users u ON u.role_id = r.id
|
|
2355
2160
|
WHERE r.id = ${user.role_id} AND u.id = ${user.id}
|
|
@@ -2409,7 +2214,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2409
2214
|
);
|
|
2410
2215
|
}
|
|
2411
2216
|
const rowResult = await db.execute(
|
|
2412
|
-
|
|
2217
|
+
sql3`SELECT u.token_version, r.name AS role
|
|
2413
2218
|
FROM users u
|
|
2414
2219
|
JOIN roles r ON r.id = u.role_id
|
|
2415
2220
|
WHERE u.id = ${payload.user_id}
|
|
@@ -2433,7 +2238,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2433
2238
|
app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
|
|
2434
2239
|
const user = c.get("user");
|
|
2435
2240
|
await db.execute(
|
|
2436
|
-
|
|
2241
|
+
sql3`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
|
|
2437
2242
|
);
|
|
2438
2243
|
clearAuthCookie(c, "auth_token");
|
|
2439
2244
|
clearAuthCookie(c, "refresh_token", "/admin/api/auth");
|
|
@@ -2442,7 +2247,7 @@ function registerAuthRoutes(app, db) {
|
|
|
2442
2247
|
}
|
|
2443
2248
|
|
|
2444
2249
|
// src/routes/admin/users.ts
|
|
2445
|
-
import { sql as
|
|
2250
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2446
2251
|
import { randomBytes } from "crypto";
|
|
2447
2252
|
function getActingUser(c) {
|
|
2448
2253
|
return c.get("user");
|
|
@@ -2477,7 +2282,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2477
2282
|
);
|
|
2478
2283
|
}
|
|
2479
2284
|
const hashResult = await db.execute(
|
|
2480
|
-
|
|
2285
|
+
sql4`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2481
2286
|
);
|
|
2482
2287
|
const hashRow = hashResult.rows[0];
|
|
2483
2288
|
if (!hashRow || !await verifyPassword(currentPassword, hashRow.password_hash)) {
|
|
@@ -2488,7 +2293,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2488
2293
|
}
|
|
2489
2294
|
const newHash = await hashPassword(newPassword);
|
|
2490
2295
|
await db.execute(
|
|
2491
|
-
|
|
2296
|
+
sql4`UPDATE users
|
|
2492
2297
|
SET password_hash = ${newHash},
|
|
2493
2298
|
must_change_password = false,
|
|
2494
2299
|
token_version = token_version + 1,
|
|
@@ -2499,7 +2304,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2499
2304
|
});
|
|
2500
2305
|
app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
|
|
2501
2306
|
const result = await db.execute(
|
|
2502
|
-
|
|
2307
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2503
2308
|
u.created_at, u.updated_at
|
|
2504
2309
|
FROM users u
|
|
2505
2310
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2510,7 +2315,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2510
2315
|
app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
|
|
2511
2316
|
const id = c.req.param("id");
|
|
2512
2317
|
const result = await db.execute(
|
|
2513
|
-
|
|
2318
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2514
2319
|
u.created_at, u.updated_at
|
|
2515
2320
|
FROM users u
|
|
2516
2321
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2555,7 +2360,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2555
2360
|
);
|
|
2556
2361
|
}
|
|
2557
2362
|
const roleResult = await db.execute(
|
|
2558
|
-
|
|
2363
|
+
sql4`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
|
|
2559
2364
|
);
|
|
2560
2365
|
const roleRow = roleResult.rows[0];
|
|
2561
2366
|
if (!roleRow) {
|
|
@@ -2570,7 +2375,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2570
2375
|
const tempPassword = generateTempPassword();
|
|
2571
2376
|
const passwordHash = await hashPassword(tempPassword);
|
|
2572
2377
|
const insertResult = await db.execute(
|
|
2573
|
-
|
|
2378
|
+
sql4`INSERT INTO users
|
|
2574
2379
|
(id, email, password_hash, role_id, must_change_password, token_version,
|
|
2575
2380
|
created_at, updated_at)
|
|
2576
2381
|
VALUES
|
|
@@ -2625,7 +2430,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2625
2430
|
);
|
|
2626
2431
|
}
|
|
2627
2432
|
const existingResult = await db.execute(
|
|
2628
|
-
|
|
2433
|
+
sql4`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
|
|
2629
2434
|
);
|
|
2630
2435
|
const existing = existingResult.rows[0];
|
|
2631
2436
|
if (!existing) {
|
|
@@ -2639,7 +2444,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2639
2444
|
let changeRole = false;
|
|
2640
2445
|
if (typeof body.role === "string" && body.role) {
|
|
2641
2446
|
const roleResult = await db.execute(
|
|
2642
|
-
|
|
2447
|
+
sql4`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
|
|
2643
2448
|
);
|
|
2644
2449
|
const roleRow = roleResult.rows[0];
|
|
2645
2450
|
if (!roleRow) {
|
|
@@ -2659,7 +2464,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2659
2464
|
}
|
|
2660
2465
|
if (changeRole) {
|
|
2661
2466
|
await db.execute(
|
|
2662
|
-
|
|
2467
|
+
sql4`UPDATE users
|
|
2663
2468
|
SET email = ${newEmail},
|
|
2664
2469
|
role_id = ${newRoleId},
|
|
2665
2470
|
token_version = token_version + 1,
|
|
@@ -2668,7 +2473,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2668
2473
|
);
|
|
2669
2474
|
} else {
|
|
2670
2475
|
await db.execute(
|
|
2671
|
-
|
|
2476
|
+
sql4`UPDATE users
|
|
2672
2477
|
SET email = ${newEmail},
|
|
2673
2478
|
role_id = ${newRoleId},
|
|
2674
2479
|
updated_at = NOW()
|
|
@@ -2676,7 +2481,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2676
2481
|
);
|
|
2677
2482
|
}
|
|
2678
2483
|
const updatedResult = await db.execute(
|
|
2679
|
-
|
|
2484
|
+
sql4`SELECT u.id, u.email, r.name AS role, u.must_change_password,
|
|
2680
2485
|
u.created_at, u.updated_at
|
|
2681
2486
|
FROM users u
|
|
2682
2487
|
JOIN roles r ON r.id = u.role_id
|
|
@@ -2713,7 +2518,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2713
2518
|
);
|
|
2714
2519
|
}
|
|
2715
2520
|
const result = await db.execute(
|
|
2716
|
-
|
|
2521
|
+
sql4`DELETE FROM users WHERE id = ${id} RETURNING id`
|
|
2717
2522
|
);
|
|
2718
2523
|
if (result.rows.length === 0) {
|
|
2719
2524
|
return c.json(
|
|
@@ -2744,7 +2549,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2744
2549
|
);
|
|
2745
2550
|
}
|
|
2746
2551
|
const existingResult = await db.execute(
|
|
2747
|
-
|
|
2552
|
+
sql4`SELECT id FROM users WHERE id = ${id} LIMIT 1`
|
|
2748
2553
|
);
|
|
2749
2554
|
const existing = existingResult.rows[0];
|
|
2750
2555
|
if (!existing) {
|
|
@@ -2756,7 +2561,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2756
2561
|
const tempPassword = generateTempPassword();
|
|
2757
2562
|
const passwordHash = await hashPassword(tempPassword);
|
|
2758
2563
|
await db.execute(
|
|
2759
|
-
|
|
2564
|
+
sql4`UPDATE users
|
|
2760
2565
|
SET password_hash = ${passwordHash},
|
|
2761
2566
|
must_change_password = true,
|
|
2762
2567
|
token_version = token_version + 1,
|
|
@@ -2769,10 +2574,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2769
2574
|
}
|
|
2770
2575
|
|
|
2771
2576
|
// src/routes/admin/config.ts
|
|
2772
|
-
import { sql as
|
|
2577
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2773
2578
|
var API_VERSION = (() => {
|
|
2774
2579
|
try {
|
|
2775
|
-
return "0.
|
|
2580
|
+
return "0.3.0";
|
|
2776
2581
|
} catch {
|
|
2777
2582
|
return "0.0.0";
|
|
2778
2583
|
}
|
|
@@ -2791,7 +2596,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2791
2596
|
permissions: r.permissions
|
|
2792
2597
|
}));
|
|
2793
2598
|
const userResult = await db.execute(
|
|
2794
|
-
|
|
2599
|
+
sql5`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
|
|
2795
2600
|
);
|
|
2796
2601
|
const userRow = userResult.rows[0];
|
|
2797
2602
|
return c.json({
|
|
@@ -2817,7 +2622,7 @@ function registerConfigRoute(app, config, registry, db) {
|
|
|
2817
2622
|
}
|
|
2818
2623
|
|
|
2819
2624
|
// src/routes/admin/schema.ts
|
|
2820
|
-
import { sql as
|
|
2625
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
2821
2626
|
function registerSchemaRoute(app, registry, db) {
|
|
2822
2627
|
app.get("/admin/api/schema", (c) => {
|
|
2823
2628
|
const content_types = Object.values(registry.content_types).map((ct) => ({
|
|
@@ -2854,7 +2659,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2854
2659
|
const data = await Promise.all(
|
|
2855
2660
|
Object.values(registry.content_types).map(async (ct) => {
|
|
2856
2661
|
const result = await db.execute(
|
|
2857
|
-
|
|
2662
|
+
sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${ct.db.table_name}"`)}`
|
|
2858
2663
|
);
|
|
2859
2664
|
const count = Number(result.rows[0].count);
|
|
2860
2665
|
return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
|
|
@@ -2866,7 +2671,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2866
2671
|
const data = await Promise.all(
|
|
2867
2672
|
Object.values(registry.taxonomy_types).map(async (tt) => {
|
|
2868
2673
|
const result = await db.execute(
|
|
2869
|
-
|
|
2674
|
+
sql6`SELECT COUNT(*)::int AS count FROM ${sql6.raw(`"${tt.db.table_name}"`)}`
|
|
2870
2675
|
);
|
|
2871
2676
|
const count = Number(result.rows[0].count);
|
|
2872
2677
|
return { name: tt.name, label: tt.label, count };
|
|
@@ -2877,15 +2682,15 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2877
2682
|
}
|
|
2878
2683
|
|
|
2879
2684
|
// src/repositories/content.ts
|
|
2880
|
-
import
|
|
2881
|
-
import { sql as
|
|
2685
|
+
import crypto from "crypto";
|
|
2686
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
2882
2687
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2883
2688
|
function codeError(code, message) {
|
|
2884
2689
|
const err = new Error(message);
|
|
2885
2690
|
err.code = code;
|
|
2886
2691
|
return err;
|
|
2887
2692
|
}
|
|
2888
|
-
function
|
|
2693
|
+
function quoteIdent2(name) {
|
|
2889
2694
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
2890
2695
|
throw new Error(`Unsafe SQL identifier: ${name}`);
|
|
2891
2696
|
}
|
|
@@ -2900,43 +2705,43 @@ function quoteField(name) {
|
|
|
2900
2705
|
function buildConditions(published_only, filters, search) {
|
|
2901
2706
|
const conds = [];
|
|
2902
2707
|
if (published_only) {
|
|
2903
|
-
conds.push(
|
|
2708
|
+
conds.push(sql7`published = true`);
|
|
2904
2709
|
}
|
|
2905
2710
|
if (search && search.term.trim() !== "" && search.columns.length > 0) {
|
|
2906
2711
|
const pattern = `%${search.term.trim()}%`;
|
|
2907
|
-
const orConds = search.columns.map((col) =>
|
|
2908
|
-
conds.push(
|
|
2712
|
+
const orConds = search.columns.map((col) => sql7`${sql7.raw(quoteField(col))} ILIKE ${pattern}`);
|
|
2713
|
+
conds.push(sql7`(${sql7.join(orConds, sql7` OR `)})`);
|
|
2909
2714
|
}
|
|
2910
2715
|
if (filters) {
|
|
2911
2716
|
for (const [field, value] of Object.entries(filters)) {
|
|
2912
|
-
const col =
|
|
2717
|
+
const col = sql7.raw(quoteField(field));
|
|
2913
2718
|
if (Array.isArray(value)) {
|
|
2914
2719
|
if (value.length === 0) continue;
|
|
2915
|
-
const inList =
|
|
2916
|
-
value.map((v) =>
|
|
2917
|
-
|
|
2720
|
+
const inList = sql7.join(
|
|
2721
|
+
value.map((v) => sql7`${v}`),
|
|
2722
|
+
sql7`, `
|
|
2918
2723
|
);
|
|
2919
|
-
conds.push(
|
|
2724
|
+
conds.push(sql7`${col} IN (${inList})`);
|
|
2920
2725
|
} else if (typeof value === "object" && value !== null) {
|
|
2921
2726
|
const op = value;
|
|
2922
|
-
if (op.gt !== void 0) conds.push(
|
|
2923
|
-
if (op.gte !== void 0) conds.push(
|
|
2924
|
-
if (op.lt !== void 0) conds.push(
|
|
2925
|
-
if (op.lte !== void 0) conds.push(
|
|
2727
|
+
if (op.gt !== void 0) conds.push(sql7`${col} > ${op.gt}`);
|
|
2728
|
+
if (op.gte !== void 0) conds.push(sql7`${col} >= ${op.gte}`);
|
|
2729
|
+
if (op.lt !== void 0) conds.push(sql7`${col} < ${op.lt}`);
|
|
2730
|
+
if (op.lte !== void 0) conds.push(sql7`${col} <= ${op.lte}`);
|
|
2926
2731
|
} else {
|
|
2927
|
-
conds.push(
|
|
2732
|
+
conds.push(sql7`${col} = ${value}`);
|
|
2928
2733
|
}
|
|
2929
2734
|
}
|
|
2930
2735
|
}
|
|
2931
2736
|
return conds;
|
|
2932
2737
|
}
|
|
2933
2738
|
function whereFragment(conds) {
|
|
2934
|
-
return conds.length > 0 ?
|
|
2739
|
+
return conds.length > 0 ? sql7` WHERE ${sql7.join(conds, sql7` AND `)}` : sql7``;
|
|
2935
2740
|
}
|
|
2936
2741
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2937
|
-
const { relations = {} } = options;
|
|
2742
|
+
const { relations = {}, publishedRelations = false } = options;
|
|
2938
2743
|
function tableRaw() {
|
|
2939
|
-
return
|
|
2744
|
+
return sql7.raw(quoteIdent2(tableName));
|
|
2940
2745
|
}
|
|
2941
2746
|
async function resolveRows(rows, include, cache) {
|
|
2942
2747
|
if (rows.length === 0) return rows;
|
|
@@ -2947,7 +2752,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2947
2752
|
}
|
|
2948
2753
|
for (const [fieldName, rel] of Object.entries(relations)) {
|
|
2949
2754
|
if (rel.type === "media") {
|
|
2950
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
2755
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
2951
2756
|
} else if (!include.includes(fieldName)) {
|
|
2952
2757
|
await resolveRelationBareIds(db, rows, fieldName, rel);
|
|
2953
2758
|
}
|
|
@@ -2955,7 +2760,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2955
2760
|
for (const fieldName of include) {
|
|
2956
2761
|
const rel = relations[fieldName];
|
|
2957
2762
|
if (rel.type !== "media") {
|
|
2958
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
2763
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
2959
2764
|
}
|
|
2960
2765
|
}
|
|
2961
2766
|
return rows;
|
|
@@ -2978,14 +2783,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2978
2783
|
const offset = (page - 1) * per_page;
|
|
2979
2784
|
const conds = buildConditions(published_only, filters, search);
|
|
2980
2785
|
const where = whereFragment(conds);
|
|
2981
|
-
const sortCol =
|
|
2982
|
-
const sortDir =
|
|
2786
|
+
const sortCol = sql7.raw(quoteIdent2(sort_by));
|
|
2787
|
+
const sortDir = sql7.raw(sort_order === "desc" ? "DESC" : "ASC");
|
|
2983
2788
|
const countResult = await db.execute(
|
|
2984
|
-
|
|
2789
|
+
sql7`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
|
|
2985
2790
|
);
|
|
2986
2791
|
const total = Number(countResult.rows[0]?.["total"] ?? 0);
|
|
2987
2792
|
const dataResult = await db.execute(
|
|
2988
|
-
|
|
2793
|
+
sql7`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
|
|
2989
2794
|
);
|
|
2990
2795
|
const rows = dataResult.rows;
|
|
2991
2796
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3006,7 +2811,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3006
2811
|
},
|
|
3007
2812
|
async findOne(id, include = []) {
|
|
3008
2813
|
const result = await db.execute(
|
|
3009
|
-
|
|
2814
|
+
sql7`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
|
|
3010
2815
|
);
|
|
3011
2816
|
if (result.rows.length === 0) return null;
|
|
3012
2817
|
const rows = result.rows;
|
|
@@ -3016,7 +2821,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3016
2821
|
},
|
|
3017
2822
|
async findBySlug(slug, include = []) {
|
|
3018
2823
|
const result = await db.execute(
|
|
3019
|
-
|
|
2824
|
+
sql7`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
|
|
3020
2825
|
);
|
|
3021
2826
|
if (result.rows.length === 0) return null;
|
|
3022
2827
|
const rows = result.rows;
|
|
@@ -3027,15 +2832,15 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3027
2832
|
async create(data) {
|
|
3028
2833
|
const record = {
|
|
3029
2834
|
...data,
|
|
3030
|
-
id:
|
|
2835
|
+
id: crypto.randomUUID(),
|
|
3031
2836
|
created_at: /* @__PURE__ */ new Date(),
|
|
3032
2837
|
updated_at: /* @__PURE__ */ new Date()
|
|
3033
2838
|
};
|
|
3034
2839
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3035
|
-
const colsSql =
|
|
3036
|
-
const valsSql =
|
|
2840
|
+
const colsSql = sql7.join(entries.map(([k]) => sql7.raw(quoteIdent2(k))), sql7`, `);
|
|
2841
|
+
const valsSql = sql7.join(entries.map(([, v]) => sql7`${v}`), sql7`, `);
|
|
3037
2842
|
const result = await db.execute(
|
|
3038
|
-
|
|
2843
|
+
sql7`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3039
2844
|
);
|
|
3040
2845
|
return result.rows[0];
|
|
3041
2846
|
},
|
|
@@ -3048,25 +2853,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3048
2853
|
if (entries.length === 0) {
|
|
3049
2854
|
return this.findOne(id);
|
|
3050
2855
|
}
|
|
3051
|
-
const setClauses =
|
|
3052
|
-
entries.map(([k, v]) =>
|
|
3053
|
-
|
|
2856
|
+
const setClauses = sql7.join(
|
|
2857
|
+
entries.map(([k, v]) => sql7`${sql7.raw(quoteIdent2(k))} = ${v}`),
|
|
2858
|
+
sql7`, `
|
|
3054
2859
|
);
|
|
3055
2860
|
const result = await db.execute(
|
|
3056
|
-
|
|
2861
|
+
sql7`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3057
2862
|
);
|
|
3058
2863
|
if (result.rows.length === 0) return null;
|
|
3059
2864
|
return result.rows[0];
|
|
3060
2865
|
},
|
|
3061
2866
|
async delete(id) {
|
|
3062
|
-
await db.execute(
|
|
2867
|
+
await db.execute(sql7`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
|
|
3063
2868
|
},
|
|
3064
2869
|
async findAll(opts = {}) {
|
|
3065
2870
|
const { include = [], published_only } = opts;
|
|
3066
2871
|
const conds = buildConditions(published_only, void 0, void 0);
|
|
3067
2872
|
const where = whereFragment(conds);
|
|
3068
2873
|
const result = await db.execute(
|
|
3069
|
-
|
|
2874
|
+
sql7`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
|
|
3070
2875
|
);
|
|
3071
2876
|
const rows = result.rows;
|
|
3072
2877
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -3077,24 +2882,24 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3077
2882
|
}
|
|
3078
2883
|
|
|
3079
2884
|
// src/repositories/media.ts
|
|
3080
|
-
import
|
|
3081
|
-
import { sql as
|
|
2885
|
+
import crypto2 from "crypto";
|
|
2886
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
3082
2887
|
function createMediaRepository(db) {
|
|
3083
|
-
const TABLE =
|
|
2888
|
+
const TABLE = sql8.raw('"media"');
|
|
3084
2889
|
return {
|
|
3085
2890
|
async findMany(opts = {}) {
|
|
3086
2891
|
const { page = 1, per_page = 10, type, orphaned } = opts;
|
|
3087
2892
|
const offset = (page - 1) * per_page;
|
|
3088
2893
|
const conds = [];
|
|
3089
|
-
if (type) conds.push(
|
|
3090
|
-
if (orphaned) conds.push(
|
|
3091
|
-
const where = conds.length > 0 ?
|
|
3092
|
-
const countResult = await db.execute(
|
|
2894
|
+
if (type) conds.push(sql8`type = ${type}`);
|
|
2895
|
+
if (orphaned) conds.push(sql8`reference_count = 0`);
|
|
2896
|
+
const where = conds.length > 0 ? sql8` WHERE ${sql8.join(conds, sql8` AND `)}` : sql8``;
|
|
2897
|
+
const countResult = await db.execute(sql8`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
|
|
3093
2898
|
const total = Number(
|
|
3094
2899
|
countResult.rows[0]?.["total"] ?? 0
|
|
3095
2900
|
);
|
|
3096
2901
|
const dataResult = await db.execute(
|
|
3097
|
-
|
|
2902
|
+
sql8`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
|
|
3098
2903
|
);
|
|
3099
2904
|
const total_pages = Math.ceil(total / per_page);
|
|
3100
2905
|
return {
|
|
@@ -3112,7 +2917,7 @@ function createMediaRepository(db) {
|
|
|
3112
2917
|
},
|
|
3113
2918
|
async findOne(id) {
|
|
3114
2919
|
const result = await db.execute(
|
|
3115
|
-
|
|
2920
|
+
sql8`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
|
|
3116
2921
|
);
|
|
3117
2922
|
if (result.rows.length === 0) return null;
|
|
3118
2923
|
return result.rows[0];
|
|
@@ -3120,16 +2925,16 @@ function createMediaRepository(db) {
|
|
|
3120
2925
|
async create(data) {
|
|
3121
2926
|
const record = {
|
|
3122
2927
|
...data,
|
|
3123
|
-
id:
|
|
2928
|
+
id: crypto2.randomUUID(),
|
|
3124
2929
|
reference_count: 0,
|
|
3125
2930
|
created_at: /* @__PURE__ */ new Date(),
|
|
3126
2931
|
updated_at: /* @__PURE__ */ new Date()
|
|
3127
2932
|
};
|
|
3128
2933
|
const entries = Object.entries(record).filter(([, v]) => v !== void 0);
|
|
3129
|
-
const colsSql =
|
|
3130
|
-
const valsSql =
|
|
2934
|
+
const colsSql = sql8.join(entries.map(([k]) => sql8.raw(`"${k}"`)), sql8`, `);
|
|
2935
|
+
const valsSql = sql8.join(entries.map(([, v]) => sql8`${v}`), sql8`, `);
|
|
3131
2936
|
const result = await db.execute(
|
|
3132
|
-
|
|
2937
|
+
sql8`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
|
|
3133
2938
|
);
|
|
3134
2939
|
return result.rows[0];
|
|
3135
2940
|
},
|
|
@@ -3139,31 +2944,31 @@ function createMediaRepository(db) {
|
|
|
3139
2944
|
([k, v]) => k !== "id" && v !== void 0
|
|
3140
2945
|
);
|
|
3141
2946
|
if (entries.length === 0) return this.findOne(id);
|
|
3142
|
-
const setClauses =
|
|
3143
|
-
entries.map(([k, v]) =>
|
|
3144
|
-
|
|
2947
|
+
const setClauses = sql8.join(
|
|
2948
|
+
entries.map(([k, v]) => sql8`${sql8.raw(`"${k}"`)} = ${v}`),
|
|
2949
|
+
sql8`, `
|
|
3145
2950
|
);
|
|
3146
2951
|
const result = await db.execute(
|
|
3147
|
-
|
|
2952
|
+
sql8`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
|
|
3148
2953
|
);
|
|
3149
2954
|
if (result.rows.length === 0) return null;
|
|
3150
2955
|
return result.rows[0];
|
|
3151
2956
|
},
|
|
3152
2957
|
async delete(id) {
|
|
3153
|
-
await db.execute(
|
|
2958
|
+
await db.execute(sql8`DELETE FROM ${TABLE} WHERE id = ${id}`);
|
|
3154
2959
|
},
|
|
3155
2960
|
async incrementReferenceCount(ids) {
|
|
3156
2961
|
if (ids.length === 0) return;
|
|
3157
|
-
const inList =
|
|
2962
|
+
const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
|
|
3158
2963
|
await db.execute(
|
|
3159
|
-
|
|
2964
|
+
sql8`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
|
|
3160
2965
|
);
|
|
3161
2966
|
},
|
|
3162
2967
|
async decrementReferenceCount(ids) {
|
|
3163
2968
|
if (ids.length === 0) return;
|
|
3164
|
-
const inList =
|
|
2969
|
+
const inList = sql8.join(ids.map((id) => sql8`${id}`), sql8`, `);
|
|
3165
2970
|
await db.execute(
|
|
3166
|
-
|
|
2971
|
+
sql8`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
|
|
3167
2972
|
);
|
|
3168
2973
|
}
|
|
3169
2974
|
};
|
|
@@ -3193,7 +2998,14 @@ function createCmsApp(options) {
|
|
|
3193
2998
|
const programmaticResolver = createProgrammaticResolver(resolverMap);
|
|
3194
2999
|
const app = new Hono();
|
|
3195
3000
|
const uploadOrigins = storage.getUploadOrigins?.() ?? [];
|
|
3196
|
-
|
|
3001
|
+
const graphiqlEnabled = options.graphql?.enabled === true && options.graphql.graphiql === true;
|
|
3002
|
+
app.use(
|
|
3003
|
+
"*",
|
|
3004
|
+
createSecurityHeadersMiddleware({
|
|
3005
|
+
connectSrc: uploadOrigins,
|
|
3006
|
+
...graphiqlEnabled && { graphiqlPath: "/graphql" }
|
|
3007
|
+
})
|
|
3008
|
+
);
|
|
3197
3009
|
app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
|
|
3198
3010
|
app.onError(errorHandler);
|
|
3199
3011
|
const listRateLimit = resolveListRateLimit(rateLimit);
|
|
@@ -3201,7 +3013,7 @@ function createCmsApp(options) {
|
|
|
3201
3013
|
const requirePermission = createPermissionMiddleware(rolesRegistry);
|
|
3202
3014
|
const getUserRole = async (userId) => {
|
|
3203
3015
|
const result = await db.execute(
|
|
3204
|
-
|
|
3016
|
+
sql9`SELECT r.name AS role
|
|
3205
3017
|
FROM users u
|
|
3206
3018
|
JOIN roles r ON r.id = u.role_id
|
|
3207
3019
|
WHERE u.id = ${userId}
|
|
@@ -3228,7 +3040,8 @@ function createCmsApp(options) {
|
|
|
3228
3040
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
3229
3041
|
typeName,
|
|
3230
3042
|
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
3231
|
-
relations: buildRelationsMap(ct.fields, registry)
|
|
3043
|
+
relations: buildRelationsMap(ct.fields, registry),
|
|
3044
|
+
publishedRelations: true
|
|
3232
3045
|
})
|
|
3233
3046
|
])
|
|
3234
3047
|
);
|
|
@@ -3236,7 +3049,8 @@ function createCmsApp(options) {
|
|
|
3236
3049
|
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
3237
3050
|
typeName,
|
|
3238
3051
|
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
3239
|
-
relations: buildRelationsMap(tt.fields, registry)
|
|
3052
|
+
relations: buildRelationsMap(tt.fields, registry),
|
|
3053
|
+
publishedRelations: true
|
|
3240
3054
|
})
|
|
3241
3055
|
])
|
|
3242
3056
|
);
|
|
@@ -3277,6 +3091,35 @@ function createCmsApp(options) {
|
|
|
3277
3091
|
);
|
|
3278
3092
|
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
|
|
3279
3093
|
registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
|
|
3094
|
+
if (options.graphql?.enabled) {
|
|
3095
|
+
const gqlOptions = options.graphql;
|
|
3096
|
+
let gqlHandler = null;
|
|
3097
|
+
let initError = null;
|
|
3098
|
+
const ready = import("./handler-GMCFYZY5.mjs").then(({ createGraphQLHandler }) => {
|
|
3099
|
+
gqlHandler = createGraphQLHandler(registry, publicRepos, programmaticResolver, db, gqlOptions);
|
|
3100
|
+
}).catch((err) => {
|
|
3101
|
+
initError = err;
|
|
3102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3103
|
+
process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
|
|
3104
|
+
`);
|
|
3105
|
+
});
|
|
3106
|
+
const invokeGraphQL = async (c) => {
|
|
3107
|
+
if (!gqlHandler && !initError) await ready;
|
|
3108
|
+
if (!gqlHandler) {
|
|
3109
|
+
return c.json(
|
|
3110
|
+
{ ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
|
|
3111
|
+
500
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
return gqlHandler(c, async () => {
|
|
3115
|
+
});
|
|
3116
|
+
};
|
|
3117
|
+
if (listRateLimit) {
|
|
3118
|
+
app.all("/graphql", listRateLimit, invokeGraphQL);
|
|
3119
|
+
} else {
|
|
3120
|
+
app.all("/graphql", invokeGraphQL);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3280
3123
|
registerConfigRoute(
|
|
3281
3124
|
app,
|
|
3282
3125
|
{
|
|
@@ -3323,13 +3166,24 @@ function createServer(options = {}) {
|
|
|
3323
3166
|
}
|
|
3324
3167
|
|
|
3325
3168
|
// src/index.ts
|
|
3169
|
+
function resolveGraphQL(cfg) {
|
|
3170
|
+
const devDefault = process.env["NODE_ENV"] !== "production";
|
|
3171
|
+
return {
|
|
3172
|
+
enabled: cfg.enabled ?? false,
|
|
3173
|
+
maxDepth: cfg.maxDepth ?? 8,
|
|
3174
|
+
maxComplexity: cfg.maxComplexity ?? 1e3,
|
|
3175
|
+
graphiql: cfg.graphiql ?? devDefault,
|
|
3176
|
+
introspection: cfg.introspection ?? cfg.graphiql ?? devDefault
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3326
3179
|
function createAPIAdapter(options = {}) {
|
|
3327
3180
|
const prefix = options.prefix ?? "/api";
|
|
3328
3181
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
3329
3182
|
return {
|
|
3330
3183
|
prefix,
|
|
3331
3184
|
...media !== void 0 && { media },
|
|
3332
|
-
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
|
|
3185
|
+
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
|
|
3186
|
+
...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
|
|
3333
3187
|
};
|
|
3334
3188
|
}
|
|
3335
3189
|
export {
|