@bobbykim/manguito-cms-api 0.4.3 → 0.5.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-HEYT5CH5.mjs → chunk-ARZXLNZS.mjs} +2 -2
- package/dist/{chunk-4Y4G564P.mjs → chunk-D5AO4BTL.mjs} +87 -58
- package/dist/chunk-D5AO4BTL.mjs.map +1 -0
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-X7JDHJAL.mjs +8 -0
- package/dist/index.d.mts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +303 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +213 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-4Y4G564P.mjs.map +0 -1
- package/dist/handler-45O5OX2C.mjs +0 -8
- /package/dist/{chunk-HEYT5CH5.mjs.map → chunk-ARZXLNZS.mjs.map} +0 -0
- /package/dist/{handler-45O5OX2C.mjs.map → handler-X7JDHJAL.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -30,6 +30,93 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/field-keys.ts
|
|
34
|
+
function isColumnBacked(field) {
|
|
35
|
+
const col = field.db_column;
|
|
36
|
+
if (col === null) return false;
|
|
37
|
+
if (col.column_name === "") return false;
|
|
38
|
+
if (col.junction) return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
|
|
42
|
+
const labelToColumn = /* @__PURE__ */ new Map();
|
|
43
|
+
const columnToLabel = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const { label, column } of pairs) {
|
|
45
|
+
labelToColumn.set(label, column);
|
|
46
|
+
columnToLabel.set(column, label);
|
|
47
|
+
}
|
|
48
|
+
for (const label of allLabels) {
|
|
49
|
+
const columnOwner = columnToLabel.get(label);
|
|
50
|
+
if (columnOwner !== void 0 && columnOwner !== label) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Fatal: field key map failed to build \u2014 field label "${label}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const label of labelToColumn.keys()) {
|
|
57
|
+
if (droppedKeys.has(label)) labelToColumn.delete(label);
|
|
58
|
+
}
|
|
59
|
+
for (const column of columnToLabel.keys()) {
|
|
60
|
+
if (droppedKeys.has(column)) columnToLabel.delete(column);
|
|
61
|
+
}
|
|
62
|
+
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
63
|
+
function remap(input, lookup) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const key of Object.keys(input)) {
|
|
66
|
+
if (droppedKeys.has(key)) continue;
|
|
67
|
+
out[lookup.get(key) ?? key] = input[key];
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
toStorage: (input) => remap(input, labelToColumn),
|
|
73
|
+
toLabels: (row) => remap(row, columnToLabel),
|
|
74
|
+
columnFor: (label) => labelToColumn.get(label),
|
|
75
|
+
labelFor: (column) => columnToLabel.get(column),
|
|
76
|
+
labels: [...labelToColumn.keys()],
|
|
77
|
+
diverges
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createFieldKeyMap(fields) {
|
|
81
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
82
|
+
const pairs = [];
|
|
83
|
+
for (const f of fields) {
|
|
84
|
+
if (!isColumnBacked(f)) continue;
|
|
85
|
+
pairs.push({ label: f.name, column: f.db_column.column_name });
|
|
86
|
+
if (f.removed === true) {
|
|
87
|
+
droppedKeys.add(f.name);
|
|
88
|
+
droppedKeys.add(f.db_column.column_name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return buildFieldKeyMap(
|
|
92
|
+
pairs,
|
|
93
|
+
fields.map((f) => f.name),
|
|
94
|
+
droppedKeys
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function createFieldKeyMapFromProjection(projectionType, allFields) {
|
|
98
|
+
const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
|
|
99
|
+
const projected = new Set(pairs.map((p) => p.label));
|
|
100
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const f of allFields) {
|
|
102
|
+
if (!isColumnBacked(f)) continue;
|
|
103
|
+
dropped.add(f.name);
|
|
104
|
+
dropped.add(f.db_column.column_name);
|
|
105
|
+
}
|
|
106
|
+
for (const p of pairs) dropped.delete(p.column);
|
|
107
|
+
for (const label of projected) dropped.delete(label);
|
|
108
|
+
return buildFieldKeyMap(
|
|
109
|
+
pairs,
|
|
110
|
+
allFields.map((f) => f.name),
|
|
111
|
+
dropped
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
var init_field_keys = __esm({
|
|
115
|
+
"src/field-keys.ts"() {
|
|
116
|
+
"use strict";
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
33
120
|
// src/relations.ts
|
|
34
121
|
function quoteIdent(name) {
|
|
35
122
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
@@ -333,65 +420,6 @@ var init_relations = __esm({
|
|
|
333
420
|
}
|
|
334
421
|
});
|
|
335
422
|
|
|
336
|
-
// src/field-keys.ts
|
|
337
|
-
function isColumnBacked(field) {
|
|
338
|
-
const col = field.db_column;
|
|
339
|
-
if (col === null) return false;
|
|
340
|
-
if (col.column_name === "") return false;
|
|
341
|
-
if (col.junction) return false;
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
function createFieldKeyMap(fields) {
|
|
345
|
-
const labelToColumn = /* @__PURE__ */ new Map();
|
|
346
|
-
const columnToLabel = /* @__PURE__ */ new Map();
|
|
347
|
-
const droppedKeys = /* @__PURE__ */ new Set();
|
|
348
|
-
for (const f of fields) {
|
|
349
|
-
if (!isColumnBacked(f)) continue;
|
|
350
|
-
columnToLabel.set(f.db_column.column_name, f.name);
|
|
351
|
-
if (f.removed === true) {
|
|
352
|
-
droppedKeys.add(f.name);
|
|
353
|
-
droppedKeys.add(f.db_column.column_name);
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
labelToColumn.set(f.name, f.db_column.column_name);
|
|
357
|
-
}
|
|
358
|
-
for (const f of fields) {
|
|
359
|
-
const columnOwner = columnToLabel.get(f.name);
|
|
360
|
-
if (columnOwner !== void 0 && columnOwner !== f.name) {
|
|
361
|
-
throw new Error(
|
|
362
|
-
`Fatal: field key map failed to build \u2014 field label "${f.name}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
for (const f of fields) {
|
|
367
|
-
if (isColumnBacked(f) && f.removed === true) {
|
|
368
|
-
columnToLabel.delete(f.db_column.column_name);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
372
|
-
function remap(input, lookup) {
|
|
373
|
-
const out = {};
|
|
374
|
-
for (const key of Object.keys(input)) {
|
|
375
|
-
if (droppedKeys.has(key)) continue;
|
|
376
|
-
out[lookup.get(key) ?? key] = input[key];
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
return {
|
|
381
|
-
toStorage: (input) => remap(input, labelToColumn),
|
|
382
|
-
toLabels: (row) => remap(row, columnToLabel),
|
|
383
|
-
columnFor: (label) => labelToColumn.get(label),
|
|
384
|
-
labelFor: (column) => columnToLabel.get(column),
|
|
385
|
-
labels: [...labelToColumn.keys()],
|
|
386
|
-
diverges
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
var init_field_keys = __esm({
|
|
390
|
-
"src/field-keys.ts"() {
|
|
391
|
-
"use strict";
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
|
|
395
423
|
// src/graphql/scalars.ts
|
|
396
424
|
var import_graphql, DateTimeScalar, JSONScalar;
|
|
397
425
|
var init_scalars = __esm({
|
|
@@ -1069,15 +1097,20 @@ function normalizePrefix(prefix) {
|
|
|
1069
1097
|
}
|
|
1070
1098
|
function createPublicPaths(prefix) {
|
|
1071
1099
|
return {
|
|
1072
|
-
collection: (basePath) => `${prefix}/${basePath}`,
|
|
1073
|
-
item: (basePath) => `${prefix}/${basePath}/:slug`,
|
|
1074
|
-
taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
|
|
1075
|
-
taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
|
|
1076
1100
|
mediaCollection: () => `${prefix}/media`,
|
|
1077
1101
|
mediaItem: () => `${prefix}/media/:id`,
|
|
1078
1102
|
openapi: () => `${prefix}/openapi.json`
|
|
1079
1103
|
};
|
|
1080
1104
|
}
|
|
1105
|
+
function createVersionedPaths(prefix, version) {
|
|
1106
|
+
const root = version === null ? prefix : `${prefix}/${version}`;
|
|
1107
|
+
return {
|
|
1108
|
+
collection: (basePath) => `${root}/${basePath}`,
|
|
1109
|
+
item: (basePath) => `${root}/${basePath}/:slug`,
|
|
1110
|
+
taxonomyCollection: (typeName) => `${root}/taxonomy/${typeName}`,
|
|
1111
|
+
taxonomyItem: (typeName) => `${root}/taxonomy/${typeName}/:id`
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1081
1114
|
|
|
1082
1115
|
// src/app.ts
|
|
1083
1116
|
var import_hono = require("hono");
|
|
@@ -1673,7 +1706,7 @@ function nestedTargets(fields) {
|
|
|
1673
1706
|
}
|
|
1674
1707
|
return out;
|
|
1675
1708
|
}
|
|
1676
|
-
function buildProjectors(registry, fieldKeyMaps) {
|
|
1709
|
+
function buildProjectors(registry, fieldKeyMaps, fallbacks) {
|
|
1677
1710
|
const projectors = {};
|
|
1678
1711
|
const sources = [
|
|
1679
1712
|
registry.content_types,
|
|
@@ -1684,7 +1717,14 @@ function buildProjectors(registry, fieldKeyMaps) {
|
|
|
1684
1717
|
for (const [typeName, type] of Object.entries(source)) {
|
|
1685
1718
|
const map = fieldKeyMaps[typeName];
|
|
1686
1719
|
if (!map) continue;
|
|
1687
|
-
|
|
1720
|
+
const typeFallbacks = fallbacks?.[typeName];
|
|
1721
|
+
projectors[typeName] = {
|
|
1722
|
+
map,
|
|
1723
|
+
nested: nestedTargets(type.fields),
|
|
1724
|
+
// Spread rather than assign: with exactOptionalPropertyTypes, an optional
|
|
1725
|
+
// property may be ABSENT but not present-and-undefined.
|
|
1726
|
+
...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
|
|
1727
|
+
};
|
|
1688
1728
|
}
|
|
1689
1729
|
}
|
|
1690
1730
|
return projectors;
|
|
@@ -1693,6 +1733,11 @@ function projectRow(row, typeName, projectors) {
|
|
|
1693
1733
|
const p = projectors[typeName];
|
|
1694
1734
|
if (!p) return row;
|
|
1695
1735
|
const out = p.map.toLabels(row);
|
|
1736
|
+
if (p.fallbacks !== void 0) {
|
|
1737
|
+
for (const [label, value] of Object.entries(p.fallbacks)) {
|
|
1738
|
+
if (out[label] === null || out[label] === void 0) out[label] = value;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1696
1741
|
for (const { label, target } of p.nested) {
|
|
1697
1742
|
const v = out[label];
|
|
1698
1743
|
if (v === null || v === void 0) continue;
|
|
@@ -1705,6 +1750,9 @@ function projectRow(row, typeName, projectors) {
|
|
|
1705
1750
|
return out;
|
|
1706
1751
|
}
|
|
1707
1752
|
|
|
1753
|
+
// src/routes/content.ts
|
|
1754
|
+
init_field_keys();
|
|
1755
|
+
|
|
1708
1756
|
// src/routes/query-params.ts
|
|
1709
1757
|
var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
1710
1758
|
var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -1786,13 +1834,24 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1786
1834
|
const basePath = contentType.default_base_path;
|
|
1787
1835
|
const repo = repos[typeName];
|
|
1788
1836
|
if (!repo) continue;
|
|
1789
|
-
const
|
|
1790
|
-
|
|
1791
|
-
)
|
|
1837
|
+
const fieldKeys = projectors[typeName].map;
|
|
1838
|
+
const relationFieldNames = /* @__PURE__ */ new Set();
|
|
1839
|
+
for (const f of contentType.fields) {
|
|
1840
|
+
if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
|
|
1841
|
+
if (isColumnBacked(f)) {
|
|
1842
|
+
const label = fieldKeys.labelFor(f.db_column.column_name);
|
|
1843
|
+
if (label !== void 0) relationFieldNames.add(label);
|
|
1844
|
+
} else {
|
|
1845
|
+
relationFieldNames.add(f.name);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1792
1848
|
const filterableFieldNames = /* @__PURE__ */ new Set([
|
|
1793
|
-
...
|
|
1849
|
+
...fieldKeys.labels,
|
|
1794
1850
|
...contentType.system_fields.map((f) => f.name)
|
|
1795
1851
|
]);
|
|
1852
|
+
const sortableFieldNames = new Set(
|
|
1853
|
+
[...SORTABLE_FIELDS].filter((f) => filterableFieldNames.has(f))
|
|
1854
|
+
);
|
|
1796
1855
|
if (contentType.only_one) {
|
|
1797
1856
|
app.get(paths.collection(basePath), async (c) => {
|
|
1798
1857
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
@@ -1808,8 +1867,6 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1808
1867
|
});
|
|
1809
1868
|
} else {
|
|
1810
1869
|
registerListRoute(paths.collection(basePath), async (c) => {
|
|
1811
|
-
const projector = projectors[typeName];
|
|
1812
|
-
const fieldKeys = projector.map;
|
|
1813
1870
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1814
1871
|
if (!pagination.ok) {
|
|
1815
1872
|
return c.json(
|
|
@@ -1824,13 +1881,13 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1824
1881
|
);
|
|
1825
1882
|
}
|
|
1826
1883
|
const sortBy = c.req.query("sort_by") ?? "created_at";
|
|
1827
|
-
if (!
|
|
1884
|
+
if (!sortableFieldNames.has(sortBy)) {
|
|
1828
1885
|
return c.json(
|
|
1829
1886
|
{
|
|
1830
1887
|
ok: false,
|
|
1831
1888
|
error: {
|
|
1832
1889
|
code: "INVALID_SORT_FIELD",
|
|
1833
|
-
message: `'${sortBy}' is not sortable. Allowed:
|
|
1890
|
+
message: `'${sortBy}' is not sortable. Allowed: ${[...sortableFieldNames].join(", ")}`
|
|
1834
1891
|
}
|
|
1835
1892
|
},
|
|
1836
1893
|
400
|
|
@@ -1877,7 +1934,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
|
|
|
1877
1934
|
);
|
|
1878
1935
|
}
|
|
1879
1936
|
}
|
|
1880
|
-
const sortColumn = fieldKeys.columnFor(sortBy)
|
|
1937
|
+
const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
|
|
1881
1938
|
const result = await repo.findMany({
|
|
1882
1939
|
published_only: true,
|
|
1883
1940
|
page: pagination.page,
|
|
@@ -3759,7 +3816,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3759
3816
|
var import_drizzle_orm6 = require("drizzle-orm");
|
|
3760
3817
|
var API_VERSION = (() => {
|
|
3761
3818
|
try {
|
|
3762
|
-
return "0.
|
|
3819
|
+
return "0.5.0";
|
|
3763
3820
|
} catch {
|
|
3764
3821
|
return "0.0.0";
|
|
3765
3822
|
}
|
|
@@ -4166,6 +4223,95 @@ function createMediaRepository(db) {
|
|
|
4166
4223
|
|
|
4167
4224
|
// src/app.ts
|
|
4168
4225
|
init_field_keys();
|
|
4226
|
+
|
|
4227
|
+
// src/versions.ts
|
|
4228
|
+
init_field_keys();
|
|
4229
|
+
var VERSION_SEGMENT = /^v\d+$/;
|
|
4230
|
+
function versionNumber(version) {
|
|
4231
|
+
return Number.parseInt(version.slice(1), 10);
|
|
4232
|
+
}
|
|
4233
|
+
function classifyVersion(segment, model) {
|
|
4234
|
+
if (!VERSION_SEGMENT.test(segment)) return "not-a-version";
|
|
4235
|
+
if (model.live.includes(segment)) return "live";
|
|
4236
|
+
return versionNumber(segment) < versionNumber(model.current) ? "retired" : "unknown";
|
|
4237
|
+
}
|
|
4238
|
+
function fallbacksFor(projection) {
|
|
4239
|
+
const out = {};
|
|
4240
|
+
for (const [typeName, type] of Object.entries(projection.types)) {
|
|
4241
|
+
const perLabel = {};
|
|
4242
|
+
for (const f of type.fields) {
|
|
4243
|
+
if (f.fallback !== void 0) perLabel[f.exposed_as] = f.fallback;
|
|
4244
|
+
}
|
|
4245
|
+
if (Object.keys(perLabel).length > 0) out[typeName] = perLabel;
|
|
4246
|
+
}
|
|
4247
|
+
return out;
|
|
4248
|
+
}
|
|
4249
|
+
function buildVersionSurface(input) {
|
|
4250
|
+
const { version, projectionVersion, prefix, registry, model, makeRepo } = input;
|
|
4251
|
+
const paths = createVersionedPaths(prefix, version);
|
|
4252
|
+
const projection = model.projections[projectionVersion];
|
|
4253
|
+
const fieldKeyMaps = {};
|
|
4254
|
+
const sources = [
|
|
4255
|
+
registry.content_types,
|
|
4256
|
+
registry.taxonomy_types,
|
|
4257
|
+
registry.paragraph_types
|
|
4258
|
+
];
|
|
4259
|
+
for (const source of sources) {
|
|
4260
|
+
for (const [typeName, type] of Object.entries(source)) {
|
|
4261
|
+
const projectionType = projection?.types[typeName];
|
|
4262
|
+
fieldKeyMaps[typeName] = projectionType ? createFieldKeyMapFromProjection(projectionType, type.fields) : createFieldKeyMap(type.fields);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
const fallbacks = projection ? fallbacksFor(projection) : void 0;
|
|
4266
|
+
const projectors = buildProjectors(registry, fieldKeyMaps, fallbacks);
|
|
4267
|
+
const sortableColumnsFor = (typeName) => {
|
|
4268
|
+
const map = fieldKeyMaps[typeName];
|
|
4269
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4270
|
+
const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
|
|
4271
|
+
const out = /* @__PURE__ */ new Set();
|
|
4272
|
+
for (const label of SORTABLE_FIELDS) {
|
|
4273
|
+
const column = map.columnFor(label);
|
|
4274
|
+
if (column !== void 0) out.add(column);
|
|
4275
|
+
else if (systemFieldNames.has(label)) out.add(label);
|
|
4276
|
+
}
|
|
4277
|
+
return out;
|
|
4278
|
+
};
|
|
4279
|
+
const repos = {};
|
|
4280
|
+
for (const [typeName, ct] of Object.entries(registry.content_types)) {
|
|
4281
|
+
repos[typeName] = makeRepo(typeName, ct.db.table_name, sortableColumnsFor(typeName));
|
|
4282
|
+
}
|
|
4283
|
+
for (const [typeName, tt] of Object.entries(registry.taxonomy_types)) {
|
|
4284
|
+
repos[typeName] = makeRepo(typeName, tt.db.table_name, sortableColumnsFor(typeName));
|
|
4285
|
+
}
|
|
4286
|
+
return { paths, fieldKeyMaps, projectors, repos };
|
|
4287
|
+
}
|
|
4288
|
+
function deprecationHeaders(input) {
|
|
4289
|
+
const { requested, model, successor } = input;
|
|
4290
|
+
const link = `<${successor}>; rel="successor-version"`;
|
|
4291
|
+
if (requested === null) {
|
|
4292
|
+
if (model.live.length <= 1) return null;
|
|
4293
|
+
return {
|
|
4294
|
+
Deprecation: "true",
|
|
4295
|
+
Link: link,
|
|
4296
|
+
Warning: `299 - "Unversioned path resolves to the latest version (${model.current}) and will change when a new version is cut. Pin a version."`
|
|
4297
|
+
};
|
|
4298
|
+
}
|
|
4299
|
+
if (requested === model.current) return null;
|
|
4300
|
+
return { Deprecation: "true", Link: link };
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
// src/app.ts
|
|
4304
|
+
function versionRoutePaths(registry, paths) {
|
|
4305
|
+
const out = [paths.collection("content"), paths.collection("taxonomy")];
|
|
4306
|
+
for (const ct of Object.values(registry.content_types)) {
|
|
4307
|
+
out.push(paths.collection(ct.default_base_path));
|
|
4308
|
+
if (!ct.only_one) out.push(paths.item(ct.default_base_path));
|
|
4309
|
+
}
|
|
4310
|
+
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4311
|
+
out.push(paths.taxonomyCollection(tt.name), paths.taxonomyItem(tt.name));
|
|
4312
|
+
}
|
|
4313
|
+
return out;
|
|
4314
|
+
}
|
|
4169
4315
|
var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
|
|
4170
4316
|
Add a storage adapter to your manguito.config.ts:
|
|
4171
4317
|
|
|
@@ -4181,6 +4327,7 @@ function createCmsApp(options) {
|
|
|
4181
4327
|
}
|
|
4182
4328
|
const prefix = normalizePrefix(options.prefix);
|
|
4183
4329
|
const publicPaths = createPublicPaths(prefix);
|
|
4330
|
+
const versionedPaths = createVersionedPaths(prefix, null);
|
|
4184
4331
|
const { storage, registry, db, rateLimit, media, cors } = options;
|
|
4185
4332
|
const cmsName = options.name ?? "Manguito CMS";
|
|
4186
4333
|
const maxFileSize = media?.max_file_size;
|
|
@@ -4229,9 +4376,18 @@ function createCmsApp(options) {
|
|
|
4229
4376
|
])
|
|
4230
4377
|
]);
|
|
4231
4378
|
const projectors = buildProjectors(registry, fieldKeyMaps);
|
|
4232
|
-
const sortableColumnsFor = (typeName) =>
|
|
4233
|
-
|
|
4234
|
-
|
|
4379
|
+
const sortableColumnsFor = (typeName) => {
|
|
4380
|
+
const map = fieldKeyMaps[typeName];
|
|
4381
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4382
|
+
const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
|
|
4383
|
+
const out = /* @__PURE__ */ new Set();
|
|
4384
|
+
for (const label of SORTABLE_FIELDS) {
|
|
4385
|
+
const column = map.columnFor(label);
|
|
4386
|
+
if (column !== void 0) out.add(column);
|
|
4387
|
+
else if (systemFieldNames.has(label)) out.add(label);
|
|
4388
|
+
}
|
|
4389
|
+
return out;
|
|
4390
|
+
};
|
|
4235
4391
|
const contentRepos = Object.fromEntries(
|
|
4236
4392
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4237
4393
|
typeName,
|
|
@@ -4250,27 +4406,14 @@ function createCmsApp(options) {
|
|
|
4250
4406
|
);
|
|
4251
4407
|
const repos = { ...contentRepos, ...taxonomyRepos };
|
|
4252
4408
|
const mediaRepo = createMediaRepository(db);
|
|
4253
|
-
const
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
])
|
|
4262
|
-
);
|
|
4263
|
-
const publicTaxonomyRepos = Object.fromEntries(
|
|
4264
|
-
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4265
|
-
typeName,
|
|
4266
|
-
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4267
|
-
relations: buildRelationsMap(tt.fields, registry),
|
|
4268
|
-
publishedRelations: true,
|
|
4269
|
-
sortableColumns: sortableColumnsFor(typeName)
|
|
4270
|
-
})
|
|
4271
|
-
])
|
|
4272
|
-
);
|
|
4273
|
-
const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
|
|
4409
|
+
const makeRepo = (typeName, tableName, sortableColumns) => {
|
|
4410
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName];
|
|
4411
|
+
return createDrizzleContentRepository(db, tableName, {
|
|
4412
|
+
relations: buildRelationsMap(type.fields, registry),
|
|
4413
|
+
publishedRelations: true,
|
|
4414
|
+
sortableColumns
|
|
4415
|
+
});
|
|
4416
|
+
};
|
|
4274
4417
|
const graphqlRepos = Object.fromEntries([
|
|
4275
4418
|
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4276
4419
|
typeName,
|
|
@@ -4293,17 +4436,17 @@ function createCmsApp(options) {
|
|
|
4293
4436
|
for (const ct of Object.values(registry.content_types)) {
|
|
4294
4437
|
const base = ct.default_base_path;
|
|
4295
4438
|
if (ct.only_one) {
|
|
4296
|
-
paths[
|
|
4439
|
+
paths[versionedPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
|
|
4297
4440
|
} else {
|
|
4298
|
-
paths[
|
|
4299
|
-
paths[
|
|
4441
|
+
paths[versionedPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
|
|
4442
|
+
paths[versionedPaths.item(base).replace(":slug", "{slug}")] = {
|
|
4300
4443
|
get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
|
|
4301
4444
|
};
|
|
4302
4445
|
}
|
|
4303
4446
|
}
|
|
4304
4447
|
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4305
|
-
paths[
|
|
4306
|
-
paths[
|
|
4448
|
+
paths[versionedPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
|
|
4449
|
+
paths[versionedPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
|
|
4307
4450
|
get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
|
|
4308
4451
|
};
|
|
4309
4452
|
}
|
|
@@ -4323,7 +4466,62 @@ function createCmsApp(options) {
|
|
|
4323
4466
|
paths: {}
|
|
4324
4467
|
})
|
|
4325
4468
|
);
|
|
4326
|
-
|
|
4469
|
+
const model = options.versions ?? {
|
|
4470
|
+
current: "v1",
|
|
4471
|
+
live: ["v1"],
|
|
4472
|
+
projections: {}
|
|
4473
|
+
};
|
|
4474
|
+
const passes = [
|
|
4475
|
+
...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
|
|
4476
|
+
{ version: null, projectionVersion: model.current }
|
|
4477
|
+
];
|
|
4478
|
+
for (const pass of passes) {
|
|
4479
|
+
const surface = buildVersionSurface({ ...pass, prefix, registry, model, makeRepo });
|
|
4480
|
+
const headers = deprecationHeaders({
|
|
4481
|
+
requested: pass.version,
|
|
4482
|
+
model,
|
|
4483
|
+
// The version root, with no trailing slash — never
|
|
4484
|
+
// createVersionedPaths(prefix, model.current).collection(''), which
|
|
4485
|
+
// would yield `${prefix}/${model.current}/` in every Link header.
|
|
4486
|
+
successor: `${prefix}/${model.current}`
|
|
4487
|
+
});
|
|
4488
|
+
if (headers !== null) {
|
|
4489
|
+
const attachDeprecationHeaders = async (c, next) => {
|
|
4490
|
+
await next();
|
|
4491
|
+
for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
|
|
4492
|
+
};
|
|
4493
|
+
for (const path of versionRoutePaths(registry, surface.paths)) {
|
|
4494
|
+
app.use(path, attachDeprecationHeaders);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
|
|
4498
|
+
}
|
|
4499
|
+
if (options.versions !== void 0) {
|
|
4500
|
+
app.all(`${prefix}/:version/*`, async (c, next) => {
|
|
4501
|
+
const kind = classifyVersion(c.req.param("version"), model);
|
|
4502
|
+
if (kind === "not-a-version" || kind === "live") return next();
|
|
4503
|
+
const live = model.live.join(", ");
|
|
4504
|
+
return kind === "retired" ? c.json(
|
|
4505
|
+
{
|
|
4506
|
+
ok: false,
|
|
4507
|
+
error: {
|
|
4508
|
+
code: "VERSION_RETIRED",
|
|
4509
|
+
message: `Version ${c.req.param("version")} is no longer served. Live versions: ${live}.`
|
|
4510
|
+
}
|
|
4511
|
+
},
|
|
4512
|
+
410
|
|
4513
|
+
) : c.json(
|
|
4514
|
+
{
|
|
4515
|
+
ok: false,
|
|
4516
|
+
error: {
|
|
4517
|
+
code: "VERSION_NOT_FOUND",
|
|
4518
|
+
message: `No version ${c.req.param("version")} is served. Live versions: ${live}.`
|
|
4519
|
+
}
|
|
4520
|
+
},
|
|
4521
|
+
404
|
|
4522
|
+
);
|
|
4523
|
+
});
|
|
4524
|
+
}
|
|
4327
4525
|
registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
|
|
4328
4526
|
if (options.graphql?.enabled) {
|
|
4329
4527
|
const gqlOptions = options.graphql;
|