@bobbykim/manguito-cms-api 0.3.3 → 0.4.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-TK4DNRFF.mjs → chunk-4F2CRXPG.mjs} +76 -12
- package/dist/chunk-4F2CRXPG.mjs.map +1 -0
- package/dist/{chunk-GM55HGWA.mjs → chunk-B5LWIR4U.mjs} +32 -20
- package/dist/chunk-B5LWIR4U.mjs.map +1 -0
- package/dist/graphql/index.d.mts +14 -1
- package/dist/graphql/index.d.ts +14 -1
- package/dist/graphql/index.js +70 -28
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-TPEULQ5W.mjs +8 -0
- package/dist/index.js +253 -125
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GM55HGWA.mjs.map +0 -1
- package/dist/chunk-TK4DNRFF.mjs.map +0 -1
- package/dist/handler-CGHZG3J5.mjs +0 -8
- /package/dist/{handler-CGHZG3J5.mjs.map → handler-TPEULQ5W.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildRelationsMap,
|
|
3
|
+
createFieldKeyMap,
|
|
3
4
|
deleteParagraphField,
|
|
5
|
+
isColumnBacked,
|
|
4
6
|
persistJunctionField,
|
|
5
7
|
persistParagraphField,
|
|
6
8
|
resolveRelationBareIds,
|
|
7
9
|
resolveRelationField
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-4F2CRXPG.mjs";
|
|
11
|
+
|
|
12
|
+
// src/paths.ts
|
|
13
|
+
function normalizePrefix(prefix) {
|
|
14
|
+
if (prefix === void 0) return "/api";
|
|
15
|
+
const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
16
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
17
|
+
return trimmed === "" ? "/api" : trimmed;
|
|
18
|
+
}
|
|
19
|
+
function createPublicPaths(prefix) {
|
|
20
|
+
return {
|
|
21
|
+
collection: (basePath) => `${prefix}/${basePath}`,
|
|
22
|
+
item: (basePath) => `${prefix}/${basePath}/:slug`,
|
|
23
|
+
taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
|
|
24
|
+
taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
|
|
25
|
+
mediaCollection: () => `${prefix}/media`,
|
|
26
|
+
mediaItem: () => `${prefix}/media/:id`,
|
|
27
|
+
openapi: () => `${prefix}/openapi.json`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
9
30
|
|
|
10
31
|
// src/app.ts
|
|
11
32
|
import { Hono } from "hono";
|
|
@@ -605,28 +626,31 @@ function parseInclude(includeParam) {
|
|
|
605
626
|
if (!includeParam) return [];
|
|
606
627
|
return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
607
628
|
}
|
|
608
|
-
function parseFilters(url, validFields) {
|
|
629
|
+
function parseFilters(url, validFields, columnFor) {
|
|
609
630
|
const { searchParams } = new URL(url);
|
|
610
631
|
const filters = {};
|
|
632
|
+
const toKey = (label) => columnFor?.(label) ?? label;
|
|
611
633
|
for (const [key, value] of searchParams.entries()) {
|
|
612
634
|
const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
|
|
613
635
|
const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
|
|
614
636
|
if (simpleMatch) {
|
|
615
637
|
const field = simpleMatch[1];
|
|
616
638
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
617
|
-
const
|
|
639
|
+
const storageKey = toKey(field);
|
|
640
|
+
const existing = filters[storageKey];
|
|
618
641
|
if (existing !== void 0) {
|
|
619
|
-
filters[
|
|
642
|
+
filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
620
643
|
} else {
|
|
621
|
-
filters[
|
|
644
|
+
filters[storageKey] = value;
|
|
622
645
|
}
|
|
623
646
|
} else if (opMatch) {
|
|
624
647
|
const field = opMatch[1];
|
|
625
648
|
const operator = opMatch[2];
|
|
626
649
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
627
650
|
if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
|
|
628
|
-
const
|
|
629
|
-
|
|
651
|
+
const storageKey = toKey(field);
|
|
652
|
+
const existing = filters[storageKey] ?? {};
|
|
653
|
+
filters[storageKey] = { ...existing, [operator]: value };
|
|
630
654
|
}
|
|
631
655
|
}
|
|
632
656
|
return { ok: true, filters };
|
|
@@ -636,7 +660,7 @@ function parseFilters(url, validFields) {
|
|
|
636
660
|
function isPublished(item) {
|
|
637
661
|
return item["published"] === true;
|
|
638
662
|
}
|
|
639
|
-
function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
|
|
663
|
+
function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
|
|
640
664
|
function registerListRoute(path, handler) {
|
|
641
665
|
if (listRateLimit) {
|
|
642
666
|
app.get(path, listRateLimit, handler);
|
|
@@ -644,7 +668,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
644
668
|
app.get(path, handler);
|
|
645
669
|
}
|
|
646
670
|
}
|
|
647
|
-
registerListRoute("
|
|
671
|
+
registerListRoute(paths.collection("content"), (c) => {
|
|
648
672
|
const data = Object.values(registry.content_types).map((ct) => ({
|
|
649
673
|
name: ct.name,
|
|
650
674
|
label: ct.label,
|
|
@@ -652,7 +676,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
652
676
|
}));
|
|
653
677
|
return c.json({ ok: true, data });
|
|
654
678
|
});
|
|
655
|
-
registerListRoute("
|
|
679
|
+
registerListRoute(paths.collection("taxonomy"), (c) => {
|
|
656
680
|
const data = Object.values(registry.taxonomy_types).map((tt) => ({
|
|
657
681
|
name: tt.name,
|
|
658
682
|
label: tt.label
|
|
@@ -671,7 +695,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
671
695
|
...contentType.system_fields.map((f) => f.name)
|
|
672
696
|
]);
|
|
673
697
|
if (contentType.only_one) {
|
|
674
|
-
app.get(
|
|
698
|
+
app.get(paths.collection(basePath), async (c) => {
|
|
675
699
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
676
700
|
if (result.data.length === 0) {
|
|
677
701
|
return c.json(
|
|
@@ -679,12 +703,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
679
703
|
404
|
|
680
704
|
);
|
|
681
705
|
}
|
|
682
|
-
let data = result.data[0];
|
|
706
|
+
let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
|
|
683
707
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
684
708
|
return c.json({ ok: true, data });
|
|
685
709
|
});
|
|
686
710
|
} else {
|
|
687
|
-
registerListRoute(
|
|
711
|
+
registerListRoute(paths.collection(basePath), async (c) => {
|
|
712
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
688
713
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
689
714
|
if (!pagination.ok) {
|
|
690
715
|
return c.json(
|
|
@@ -724,7 +749,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
724
749
|
400
|
|
725
750
|
);
|
|
726
751
|
}
|
|
727
|
-
const filtersResult = parseFilters(c.req.url, filterableFieldNames);
|
|
752
|
+
const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
|
|
728
753
|
if (!filtersResult.ok) {
|
|
729
754
|
return c.json(
|
|
730
755
|
{
|
|
@@ -761,13 +786,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
761
786
|
filters: filtersResult.filters,
|
|
762
787
|
include
|
|
763
788
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return c.json(result);
|
|
789
|
+
const labeled = result.data.map(
|
|
790
|
+
(row) => fieldKeys.toLabels(row)
|
|
791
|
+
);
|
|
792
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
793
|
+
return c.json({ ...result, data });
|
|
769
794
|
});
|
|
770
|
-
app.get(
|
|
795
|
+
app.get(paths.item(basePath), async (c) => {
|
|
771
796
|
const slug = c.req.param("slug");
|
|
772
797
|
const include = parseInclude(c.req.query("include"));
|
|
773
798
|
for (const field of include) {
|
|
@@ -794,7 +819,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
794
819
|
404
|
|
795
820
|
);
|
|
796
821
|
}
|
|
797
|
-
let data = item;
|
|
822
|
+
let data = fieldKeyMaps[typeName].toLabels(item);
|
|
798
823
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
799
824
|
return c.json({ ok: true, data });
|
|
800
825
|
});
|
|
@@ -803,7 +828,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
803
828
|
for (const [typeName] of Object.entries(registry.taxonomy_types)) {
|
|
804
829
|
const repo = repos[typeName];
|
|
805
830
|
if (!repo) continue;
|
|
806
|
-
registerListRoute(
|
|
831
|
+
registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
|
|
807
832
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
808
833
|
if (!pagination.ok) {
|
|
809
834
|
return c.json(
|
|
@@ -822,13 +847,14 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
822
847
|
page: pagination.page,
|
|
823
848
|
per_page: pagination.per_page
|
|
824
849
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
850
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
851
|
+
const labeled = result.data.map(
|
|
852
|
+
(row) => fieldKeys.toLabels(row)
|
|
853
|
+
);
|
|
854
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
855
|
+
return c.json({ ...result, data });
|
|
830
856
|
});
|
|
831
|
-
app.get(
|
|
857
|
+
app.get(paths.taxonomyItem(typeName), async (c) => {
|
|
832
858
|
const id = c.req.param("id");
|
|
833
859
|
const item = await repo.findOne(id);
|
|
834
860
|
if (!item || !isPublished(item)) {
|
|
@@ -840,7 +866,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
840
866
|
404
|
|
841
867
|
);
|
|
842
868
|
}
|
|
843
|
-
let data = item;
|
|
869
|
+
let data = fieldKeyMaps[typeName].toLabels(item);
|
|
844
870
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
845
871
|
return c.json({ ok: true, data });
|
|
846
872
|
});
|
|
@@ -856,7 +882,7 @@ function parsePagination2(pageStr, perPageStr) {
|
|
|
856
882
|
if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
|
|
857
883
|
return { ok: true, page, per_page };
|
|
858
884
|
}
|
|
859
|
-
function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
885
|
+
function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
860
886
|
const mediaListHandler = async (c) => {
|
|
861
887
|
const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
|
|
862
888
|
if (!pagination.ok) {
|
|
@@ -895,11 +921,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
895
921
|
return c.json(result);
|
|
896
922
|
};
|
|
897
923
|
if (listRateLimit) {
|
|
898
|
-
app.get(
|
|
924
|
+
app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
|
|
899
925
|
} else {
|
|
900
|
-
app.get(
|
|
926
|
+
app.get(paths.mediaCollection(), mediaListHandler);
|
|
901
927
|
}
|
|
902
|
-
app.get(
|
|
928
|
+
app.get(paths.mediaItem(), async (c) => {
|
|
903
929
|
const id = c.req.param("id");
|
|
904
930
|
const item = await mediaRepo.findOne(id);
|
|
905
931
|
if (!item) {
|
|
@@ -923,9 +949,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
|
|
|
923
949
|
const added = [];
|
|
924
950
|
const removed = [];
|
|
925
951
|
for (const f of mediaFields) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
952
|
+
const key = f.db_column?.column_name;
|
|
953
|
+
if (key === void 0 || key === "") continue;
|
|
954
|
+
if (after !== null && !(key in after)) continue;
|
|
955
|
+
const oldId = before ? extractMediaId(before[key]) : null;
|
|
956
|
+
const newId = after ? extractMediaId(after[key]) : null;
|
|
929
957
|
if (newId === oldId) continue;
|
|
930
958
|
if (oldId) removed.push(oldId);
|
|
931
959
|
if (newId) added.push(newId);
|
|
@@ -985,7 +1013,7 @@ function isEmpty(value) {
|
|
|
985
1013
|
function checkRequiredFields(fields, data) {
|
|
986
1014
|
return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
987
1015
|
}
|
|
988
|
-
function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
1016
|
+
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
989
1017
|
const searchParams = new URL(url).searchParams;
|
|
990
1018
|
const pagination = parsePagination(
|
|
991
1019
|
searchParams.get("page") ?? void 0,
|
|
@@ -1020,7 +1048,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
1020
1048
|
status: 400
|
|
1021
1049
|
};
|
|
1022
1050
|
}
|
|
1023
|
-
const filtersResult = parseFilters(url, schemaFieldNames);
|
|
1051
|
+
const filtersResult = parseFilters(url, schemaFieldNames, columnFor);
|
|
1024
1052
|
if (!filtersResult.ok) {
|
|
1025
1053
|
return {
|
|
1026
1054
|
ok: false,
|
|
@@ -1052,7 +1080,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
1052
1080
|
search
|
|
1053
1081
|
};
|
|
1054
1082
|
}
|
|
1055
|
-
function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db) {
|
|
1083
|
+
function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
|
|
1056
1084
|
for (const [typeName, contentType] of Object.entries(registry.content_types)) {
|
|
1057
1085
|
const basePath = `content/${typeName}`;
|
|
1058
1086
|
const repo = repos[typeName];
|
|
@@ -1064,6 +1092,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1064
1092
|
const relationFieldNames = new Set(
|
|
1065
1093
|
contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
1066
1094
|
);
|
|
1095
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
1067
1096
|
const requiredFields = contentType.fields.filter((f) => f.required);
|
|
1068
1097
|
const mediaFields = contentType.fields.filter(
|
|
1069
1098
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -1077,7 +1106,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1077
1106
|
`/admin/api/${basePath}`,
|
|
1078
1107
|
requirePermission("content:read"),
|
|
1079
1108
|
async (c) => {
|
|
1080
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
1109
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
1081
1110
|
if (!parsed.ok) {
|
|
1082
1111
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
1083
1112
|
}
|
|
@@ -1097,7 +1126,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1097
1126
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
1098
1127
|
}
|
|
1099
1128
|
const result = await repo.findMany(findOpts);
|
|
1100
|
-
|
|
1129
|
+
const data = result.data.map((row) => fieldKeys.toLabels(row));
|
|
1130
|
+
return c.json({ ...result, data });
|
|
1101
1131
|
}
|
|
1102
1132
|
);
|
|
1103
1133
|
app.get(
|
|
@@ -1132,7 +1162,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1132
1162
|
}
|
|
1133
1163
|
}
|
|
1134
1164
|
}
|
|
1135
|
-
|
|
1165
|
+
const data = fieldKeys.toLabels(item);
|
|
1166
|
+
return c.json({ ok: true, data });
|
|
1136
1167
|
}
|
|
1137
1168
|
);
|
|
1138
1169
|
app.post(
|
|
@@ -1199,6 +1230,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1199
1230
|
}
|
|
1200
1231
|
);
|
|
1201
1232
|
const writeNewItem = async (c, body) => {
|
|
1233
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
1202
1234
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1203
1235
|
if (fieldErrors.length > 0) {
|
|
1204
1236
|
return c.json(
|
|
@@ -1222,9 +1254,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1222
1254
|
const junctionFields = contentType.fields.filter(
|
|
1223
1255
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1224
1256
|
);
|
|
1225
|
-
const
|
|
1226
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1227
|
-
);
|
|
1257
|
+
const columnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
1228
1258
|
const columnData = {
|
|
1229
1259
|
published: body["published"] ?? false
|
|
1230
1260
|
};
|
|
@@ -1233,9 +1263,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1233
1263
|
} else {
|
|
1234
1264
|
columnData["slug"] = body["slug"];
|
|
1235
1265
|
}
|
|
1236
|
-
for (const f of
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1266
|
+
for (const f of columnBackedFields) {
|
|
1267
|
+
const key = f.db_column.column_name;
|
|
1268
|
+
if (!(key in storageBody)) continue;
|
|
1269
|
+
columnData[key] = storageBody[key];
|
|
1239
1270
|
}
|
|
1240
1271
|
if (db) {
|
|
1241
1272
|
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
@@ -1255,7 +1286,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1255
1286
|
}
|
|
1256
1287
|
const item = await repo.create(columnData);
|
|
1257
1288
|
const itemId = item["id"];
|
|
1258
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
1289
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
1259
1290
|
if (db) {
|
|
1260
1291
|
for (const f of paragraphFields) {
|
|
1261
1292
|
const comp = f.ui_component;
|
|
@@ -1272,7 +1303,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1272
1303
|
}
|
|
1273
1304
|
}
|
|
1274
1305
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1275
|
-
|
|
1306
|
+
const data = fieldKeys.toLabels(item);
|
|
1307
|
+
return c.json({ ok: true, data }, 201);
|
|
1276
1308
|
};
|
|
1277
1309
|
app.patch(
|
|
1278
1310
|
`/admin/api/${basePath}/:id`,
|
|
@@ -1332,11 +1364,12 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1332
1364
|
}
|
|
1333
1365
|
);
|
|
1334
1366
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
1367
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
1335
1368
|
if (body["published"] === true) {
|
|
1336
1369
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1337
1370
|
});
|
|
1338
1371
|
if (publishDeny) return publishDeny;
|
|
1339
|
-
const merged = { ...existing, ...body };
|
|
1372
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
1340
1373
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
1341
1374
|
if (fieldErrors.length > 0) {
|
|
1342
1375
|
return c.json(
|
|
@@ -1356,17 +1389,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1356
1389
|
const patchJunctionFields = contentType.fields.filter(
|
|
1357
1390
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
1358
1391
|
);
|
|
1359
|
-
const
|
|
1360
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1361
|
-
);
|
|
1392
|
+
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
1362
1393
|
const patchData = {};
|
|
1363
1394
|
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
1364
1395
|
if ("published" in body) patchData["published"] = body["published"];
|
|
1365
|
-
for (const f of
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
}
|
|
1396
|
+
for (const f of patchColumnBackedFields) {
|
|
1397
|
+
const key = f.db_column.column_name;
|
|
1398
|
+
if (!(key in storageBody)) continue;
|
|
1399
|
+
patchData[key] = storageBody[key];
|
|
1370
1400
|
}
|
|
1371
1401
|
const updated = await repo.update(id, patchData);
|
|
1372
1402
|
if (!updated) {
|
|
@@ -1376,7 +1406,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1376
1406
|
);
|
|
1377
1407
|
}
|
|
1378
1408
|
const mediaDeltas = [
|
|
1379
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
1409
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
1380
1410
|
];
|
|
1381
1411
|
if (db) {
|
|
1382
1412
|
for (const f of patchParagraphFields) {
|
|
@@ -1394,7 +1424,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1394
1424
|
}
|
|
1395
1425
|
}
|
|
1396
1426
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1397
|
-
|
|
1427
|
+
const data = fieldKeys.toLabels(updated);
|
|
1428
|
+
return c.json({ ok: true, data });
|
|
1398
1429
|
};
|
|
1399
1430
|
if (contentType.only_one) {
|
|
1400
1431
|
app.put(
|
|
@@ -1454,6 +1485,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1454
1485
|
const relationFieldNames = new Set(
|
|
1455
1486
|
taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
1456
1487
|
);
|
|
1488
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
1457
1489
|
const requiredFields = taxonomyType.fields.filter((f) => f.required);
|
|
1458
1490
|
const mediaFields = taxonomyType.fields.filter(
|
|
1459
1491
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -1467,7 +1499,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1467
1499
|
`/admin/api/taxonomy/${typeName}`,
|
|
1468
1500
|
requirePermission("content:read"),
|
|
1469
1501
|
async (c) => {
|
|
1470
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
1502
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
1471
1503
|
if (!parsed.ok) {
|
|
1472
1504
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
1473
1505
|
}
|
|
@@ -1487,7 +1519,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1487
1519
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
1488
1520
|
}
|
|
1489
1521
|
const result = await repo.findMany(findOpts);
|
|
1490
|
-
|
|
1522
|
+
const data = result.data.map((row) => fieldKeys.toLabels(row));
|
|
1523
|
+
return c.json({ ...result, data });
|
|
1491
1524
|
}
|
|
1492
1525
|
);
|
|
1493
1526
|
app.get(
|
|
@@ -1502,7 +1535,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1502
1535
|
404
|
|
1503
1536
|
);
|
|
1504
1537
|
}
|
|
1505
|
-
|
|
1538
|
+
const data = fieldKeys.toLabels(item);
|
|
1539
|
+
return c.json({ ok: true, data });
|
|
1506
1540
|
}
|
|
1507
1541
|
);
|
|
1508
1542
|
app.post(
|
|
@@ -1510,6 +1544,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1510
1544
|
requirePermission("content:create"),
|
|
1511
1545
|
async (c) => {
|
|
1512
1546
|
const body = await c.req.json();
|
|
1547
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
1513
1548
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
1514
1549
|
if (fieldErrors.length > 0) {
|
|
1515
1550
|
return c.json(
|
|
@@ -1529,20 +1564,19 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1529
1564
|
});
|
|
1530
1565
|
if (publishDeny) return publishDeny;
|
|
1531
1566
|
}
|
|
1532
|
-
const taxColumnFields = taxonomyType.fields.filter(
|
|
1533
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1534
|
-
);
|
|
1535
1567
|
const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
1568
|
+
const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
1536
1569
|
const taxColumnData = {
|
|
1537
1570
|
published: body["published"] ?? false
|
|
1538
1571
|
};
|
|
1539
|
-
for (const f of
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1572
|
+
for (const f of taxColumnBackedFields) {
|
|
1573
|
+
const key = f.db_column.column_name;
|
|
1574
|
+
if (!(key in storageBody)) continue;
|
|
1575
|
+
taxColumnData[key] = storageBody[key];
|
|
1542
1576
|
}
|
|
1543
1577
|
const item = await repo.create(taxColumnData);
|
|
1544
1578
|
const taxItemId = item["id"];
|
|
1545
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
1579
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
1546
1580
|
if (db) {
|
|
1547
1581
|
for (const f of taxParagraphFields) {
|
|
1548
1582
|
const comp = f.ui_component;
|
|
@@ -1554,7 +1588,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1554
1588
|
}
|
|
1555
1589
|
}
|
|
1556
1590
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1557
|
-
|
|
1591
|
+
const data = fieldKeys.toLabels(item);
|
|
1592
|
+
return c.json({ ok: true, data }, 201);
|
|
1558
1593
|
}
|
|
1559
1594
|
);
|
|
1560
1595
|
app.patch(
|
|
@@ -1563,6 +1598,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1563
1598
|
async (c) => {
|
|
1564
1599
|
const id = c.req.param("id");
|
|
1565
1600
|
const body = await c.req.json();
|
|
1601
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
1566
1602
|
const existing = await repo.findOne(id);
|
|
1567
1603
|
if (!existing) {
|
|
1568
1604
|
return c.json(
|
|
@@ -1574,7 +1610,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1574
1610
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
1575
1611
|
});
|
|
1576
1612
|
if (publishDeny) return publishDeny;
|
|
1577
|
-
const merged = { ...existing, ...body };
|
|
1613
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
1578
1614
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
1579
1615
|
if (fieldErrors.length > 0) {
|
|
1580
1616
|
return c.json(
|
|
@@ -1590,17 +1626,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1590
1626
|
);
|
|
1591
1627
|
}
|
|
1592
1628
|
}
|
|
1593
|
-
const taxPatchColumnFields = taxonomyType.fields.filter(
|
|
1594
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
1595
|
-
);
|
|
1596
1629
|
const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
1630
|
+
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
1597
1631
|
const taxPatchData = {};
|
|
1598
1632
|
if ("published" in body) taxPatchData["published"] = body["published"];
|
|
1599
|
-
for (const f of
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1633
|
+
for (const f of taxPatchColumnBackedFields) {
|
|
1634
|
+
const key = f.db_column.column_name;
|
|
1635
|
+
if (!(key in storageBody)) continue;
|
|
1636
|
+
taxPatchData[key] = storageBody[key];
|
|
1604
1637
|
}
|
|
1605
1638
|
const updated = await repo.update(id, taxPatchData);
|
|
1606
1639
|
if (!updated) {
|
|
@@ -1610,7 +1643,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1610
1643
|
);
|
|
1611
1644
|
}
|
|
1612
1645
|
const mediaDeltas = [
|
|
1613
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
1646
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
1614
1647
|
];
|
|
1615
1648
|
if (db) {
|
|
1616
1649
|
for (const f of taxPatchParagraphFields) {
|
|
@@ -1623,7 +1656,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1623
1656
|
}
|
|
1624
1657
|
}
|
|
1625
1658
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1626
|
-
|
|
1659
|
+
const data = fieldKeys.toLabels(updated);
|
|
1660
|
+
return c.json({ ok: true, data });
|
|
1627
1661
|
}
|
|
1628
1662
|
);
|
|
1629
1663
|
app.delete(
|
|
@@ -2615,7 +2649,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2615
2649
|
import { sql as sql5 } from "drizzle-orm";
|
|
2616
2650
|
var API_VERSION = (() => {
|
|
2617
2651
|
try {
|
|
2618
|
-
return "0.
|
|
2652
|
+
return "0.4.0";
|
|
2619
2653
|
} catch {
|
|
2620
2654
|
return "0.0.0";
|
|
2621
2655
|
}
|
|
@@ -3026,7 +3060,8 @@ function createCmsApp(options) {
|
|
|
3026
3060
|
if (!options.storage) {
|
|
3027
3061
|
throw new Error(MISSING_STORAGE_ERROR);
|
|
3028
3062
|
}
|
|
3029
|
-
const prefix = options.prefix
|
|
3063
|
+
const prefix = normalizePrefix(options.prefix);
|
|
3064
|
+
const publicPaths = createPublicPaths(prefix);
|
|
3030
3065
|
const { storage, registry, db, rateLimit, media, cors } = options;
|
|
3031
3066
|
const cmsName = options.name ?? "Manguito CMS";
|
|
3032
3067
|
const maxFileSize = media?.max_file_size;
|
|
@@ -3106,23 +3141,27 @@ function createCmsApp(options) {
|
|
|
3106
3141
|
registerAuthRoutes(app, db);
|
|
3107
3142
|
app.use("/admin/api/*", authMiddleware);
|
|
3108
3143
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
3109
|
-
app.get(
|
|
3144
|
+
app.get(publicPaths.openapi(), (c) => {
|
|
3110
3145
|
const paths = {};
|
|
3111
3146
|
for (const ct of Object.values(registry.content_types)) {
|
|
3112
3147
|
const base = ct.default_base_path;
|
|
3113
3148
|
if (ct.only_one) {
|
|
3114
|
-
paths[
|
|
3149
|
+
paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
|
|
3115
3150
|
} else {
|
|
3116
|
-
paths[
|
|
3117
|
-
paths[
|
|
3151
|
+
paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
|
|
3152
|
+
paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
|
|
3153
|
+
get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
|
|
3154
|
+
};
|
|
3118
3155
|
}
|
|
3119
3156
|
}
|
|
3120
3157
|
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
3121
|
-
paths[
|
|
3122
|
-
paths[
|
|
3158
|
+
paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
|
|
3159
|
+
paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
|
|
3160
|
+
get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
|
|
3161
|
+
};
|
|
3123
3162
|
}
|
|
3124
|
-
paths[
|
|
3125
|
-
paths["
|
|
3163
|
+
paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
|
|
3164
|
+
paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
|
|
3126
3165
|
return c.json({
|
|
3127
3166
|
openapi: "3.0.3",
|
|
3128
3167
|
info: { title: "Manguito CMS Public API", version: "1.0.0" },
|
|
@@ -3137,14 +3176,24 @@ function createCmsApp(options) {
|
|
|
3137
3176
|
paths: {}
|
|
3138
3177
|
})
|
|
3139
3178
|
);
|
|
3140
|
-
|
|
3141
|
-
|
|
3179
|
+
const fieldKeyMaps = Object.fromEntries([
|
|
3180
|
+
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
3181
|
+
typeName,
|
|
3182
|
+
createFieldKeyMap(ct.fields)
|
|
3183
|
+
]),
|
|
3184
|
+
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
3185
|
+
typeName,
|
|
3186
|
+
createFieldKeyMap(tt.fields)
|
|
3187
|
+
])
|
|
3188
|
+
]);
|
|
3189
|
+
registerPublicContentRoutes(app, registry, publicRepos, fieldKeyMaps, publicPaths, listRateLimit, programmaticResolver);
|
|
3190
|
+
registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
|
|
3142
3191
|
if (options.graphql?.enabled) {
|
|
3143
3192
|
const gqlOptions = options.graphql;
|
|
3144
3193
|
let gqlHandler = null;
|
|
3145
3194
|
let initError = null;
|
|
3146
|
-
const ready = import("./handler-
|
|
3147
|
-
gqlHandler = createGraphQLHandler(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
|
|
3195
|
+
const ready = import("./handler-TPEULQ5W.mjs").then(({ createGraphQLHandler }) => {
|
|
3196
|
+
gqlHandler = createGraphQLHandler(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
|
|
3148
3197
|
}).catch((err) => {
|
|
3149
3198
|
initError = err;
|
|
3150
3199
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3184,7 +3233,7 @@ function createCmsApp(options) {
|
|
|
3184
3233
|
);
|
|
3185
3234
|
registerSchemaRoute(app, registry, db);
|
|
3186
3235
|
registerUserRoutes(app, db, requirePermission, requireHierarchy);
|
|
3187
|
-
registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db);
|
|
3236
|
+
registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
|
|
3188
3237
|
registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
|
|
3189
3238
|
return { prefix, app };
|
|
3190
3239
|
}
|
|
@@ -3225,7 +3274,7 @@ function resolveGraphQL(cfg) {
|
|
|
3225
3274
|
};
|
|
3226
3275
|
}
|
|
3227
3276
|
function createAPIAdapter(options = {}) {
|
|
3228
|
-
const prefix = options.prefix
|
|
3277
|
+
const prefix = normalizePrefix(options.prefix);
|
|
3229
3278
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
3230
3279
|
return {
|
|
3231
3280
|
prefix,
|