@bobbykim/manguito-cms-api 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-GM55HGWA.mjs +607 -0
- package/dist/chunk-GM55HGWA.mjs.map +1 -0
- package/dist/chunk-TK4DNRFF.mjs +286 -0
- package/dist/chunk-TK4DNRFF.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 +747 -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-CGHZG3J5.mjs +8 -0
- package/dist/handler-CGHZG3J5.mjs.map +1 -0
- package/dist/index.d.mts +7 -14
- package/dist/index.d.ts +7 -14
- package/dist/index.js +1265 -439
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +357 -455
- 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.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +30,931 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/relations.ts
|
|
34
|
+
function quoteIdent(name) {
|
|
35
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
36
|
+
return `"${name}"`;
|
|
37
|
+
}
|
|
38
|
+
function paragraphMediaFieldNames(pType) {
|
|
39
|
+
return new Set(
|
|
40
|
+
pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
|
|
44
|
+
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);
|
|
45
|
+
if (mediaColumns.length === 0) return [];
|
|
46
|
+
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
47
|
+
const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
|
|
48
|
+
const result = await db.execute(
|
|
49
|
+
import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
50
|
+
);
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const row of result.rows) {
|
|
53
|
+
for (const col of mediaColumns) {
|
|
54
|
+
const v = row[col];
|
|
55
|
+
if (typeof v === "string" && v !== "") ids.push(v);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return ids;
|
|
59
|
+
}
|
|
60
|
+
function nestedParagraphFields(pType, registry) {
|
|
61
|
+
if (!registry) return [];
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const f of pType.fields) {
|
|
64
|
+
if (f.db_column !== null) continue;
|
|
65
|
+
const comp = f.ui_component;
|
|
66
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
67
|
+
const nType = registry.paragraph_types[comp.ref];
|
|
68
|
+
if (nType) out.push({ fieldName: f.name, nType });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
|
|
73
|
+
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
74
|
+
const result = await db.execute(
|
|
75
|
+
import_drizzle_orm2.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
76
|
+
);
|
|
77
|
+
return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
|
|
78
|
+
}
|
|
79
|
+
async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry) {
|
|
80
|
+
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
81
|
+
const nested = nestedParagraphFields(pType, registry);
|
|
82
|
+
if (nested.length > 0) {
|
|
83
|
+
const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
|
|
84
|
+
for (const rowId of staleIds) {
|
|
85
|
+
for (const n of nested) {
|
|
86
|
+
const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
|
|
87
|
+
removed.push(...d.removed);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
92
|
+
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
93
|
+
const mediaFieldNames = paragraphMediaFieldNames(pType);
|
|
94
|
+
const added = [];
|
|
95
|
+
for (let i = 0; i < items.length; i++) {
|
|
96
|
+
const pItem = items[i];
|
|
97
|
+
const rowId = import_node_crypto.default.randomUUID();
|
|
98
|
+
const data = {
|
|
99
|
+
id: rowId,
|
|
100
|
+
parent_id: itemId,
|
|
101
|
+
parent_type: parentType,
|
|
102
|
+
parent_field: fieldName,
|
|
103
|
+
order: i,
|
|
104
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
105
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
106
|
+
};
|
|
107
|
+
for (const pf of pType.fields) {
|
|
108
|
+
if (!pf.db_column || pf.db_column.junction) continue;
|
|
109
|
+
data[pf.db_column.column_name] = pItem[pf.name] ?? null;
|
|
110
|
+
}
|
|
111
|
+
const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
|
|
112
|
+
const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
|
|
113
|
+
await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
|
|
114
|
+
for (const name of mediaFieldNames) {
|
|
115
|
+
const val = pItem[name];
|
|
116
|
+
if (typeof val === "string" && val !== "") added.push(val);
|
|
117
|
+
}
|
|
118
|
+
for (const n of nested) {
|
|
119
|
+
const nestedItems = Array.isArray(pItem[n.fieldName]) ? pItem[n.fieldName] : [];
|
|
120
|
+
const d = await persistParagraphField(
|
|
121
|
+
db,
|
|
122
|
+
rowId,
|
|
123
|
+
pType.db.table_name,
|
|
124
|
+
n.fieldName,
|
|
125
|
+
n.nType,
|
|
126
|
+
nestedItems,
|
|
127
|
+
registry
|
|
128
|
+
);
|
|
129
|
+
added.push(...d.added);
|
|
130
|
+
removed.push(...d.removed);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { added, removed };
|
|
134
|
+
}
|
|
135
|
+
async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
|
|
136
|
+
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
137
|
+
const nested = nestedParagraphFields(pType, registry);
|
|
138
|
+
if (nested.length > 0) {
|
|
139
|
+
const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
|
|
140
|
+
for (const rowId of rowIds) {
|
|
141
|
+
for (const n of nested) {
|
|
142
|
+
const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
|
|
143
|
+
removed.push(...d.removed);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
148
|
+
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
149
|
+
return { added: [], removed };
|
|
150
|
+
}
|
|
151
|
+
async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
152
|
+
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
|
|
153
|
+
const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
|
|
154
|
+
const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
|
|
155
|
+
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
|
|
156
|
+
for (const rid of relatedIds) {
|
|
157
|
+
await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function buildRelationsMap(fields, registry) {
|
|
161
|
+
const relations = {};
|
|
162
|
+
for (const field of fields) {
|
|
163
|
+
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
164
|
+
if (!field.db_column) continue;
|
|
165
|
+
relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
|
|
166
|
+
} else if (field.field_type === "paragraph") {
|
|
167
|
+
const ref = field.ui_component.ref;
|
|
168
|
+
const pType = ref ? registry.paragraph_types[ref] : void 0;
|
|
169
|
+
if (!pType) continue;
|
|
170
|
+
relations[field.name] = { type: "paragraph", table: pType.db.table_name };
|
|
171
|
+
} else if (field.field_type === "reference") {
|
|
172
|
+
if (!field.db_column) continue;
|
|
173
|
+
if (field.db_column.junction) {
|
|
174
|
+
const j = field.db_column.junction;
|
|
175
|
+
relations[field.name] = {
|
|
176
|
+
type: "junction",
|
|
177
|
+
table: j.right_table,
|
|
178
|
+
junction_table: j.table_name,
|
|
179
|
+
left_column: j.left_column,
|
|
180
|
+
right_column: j.right_column,
|
|
181
|
+
order_column: j.order_column
|
|
182
|
+
};
|
|
183
|
+
} else if (field.db_column.foreign_key) {
|
|
184
|
+
relations[field.name] = {
|
|
185
|
+
type: "reference",
|
|
186
|
+
table: field.db_column.foreign_key.table,
|
|
187
|
+
fk_column: field.db_column.column_name
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return relations;
|
|
193
|
+
}
|
|
194
|
+
function groupBy(items, key) {
|
|
195
|
+
const result = {};
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
const k = String(item[key]);
|
|
198
|
+
if (!result[k]) result[k] = [];
|
|
199
|
+
result[k].push(item);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
|
|
204
|
+
if (rows.length === 0) return;
|
|
205
|
+
if (rel.type === "paragraph") {
|
|
206
|
+
const parentIds = rows.map((r) => r["id"]);
|
|
207
|
+
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
208
|
+
const result = await db.execute(
|
|
209
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
210
|
+
);
|
|
211
|
+
const byParent = groupBy(result.rows, "parent_id");
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
row[fieldName] = byParent[row["id"]] ?? [];
|
|
214
|
+
}
|
|
215
|
+
} else if (rel.type === "reference") {
|
|
216
|
+
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
217
|
+
if (fkValues.length === 0) {
|
|
218
|
+
for (const row of rows) row[fieldName] = null;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const unique = [...new Set(fkValues)];
|
|
222
|
+
const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
223
|
+
if (uncached.length > 0) {
|
|
224
|
+
const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
225
|
+
const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
|
|
226
|
+
const result = await db.execute(
|
|
227
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`
|
|
228
|
+
);
|
|
229
|
+
for (const item of result.rows) {
|
|
230
|
+
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
const fkVal = row[rel.fk_column];
|
|
235
|
+
row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
|
|
236
|
+
}
|
|
237
|
+
} else if (rel.type === "junction") {
|
|
238
|
+
const parentIds = rows.map((r) => r["id"]);
|
|
239
|
+
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
240
|
+
const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
|
|
241
|
+
const junctionResult = await db.execute(
|
|
242
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
|
|
243
|
+
);
|
|
244
|
+
const jRows = junctionResult.rows;
|
|
245
|
+
const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
|
|
246
|
+
const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
247
|
+
if (uncached.length > 0) {
|
|
248
|
+
const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
249
|
+
const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
|
|
250
|
+
const entitiesResult = await db.execute(
|
|
251
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
|
|
252
|
+
);
|
|
253
|
+
for (const item of entitiesResult.rows) {
|
|
254
|
+
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const byLeft = groupBy(jRows, rel.left_column);
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
const jEntries = byLeft[row["id"]] ?? [];
|
|
260
|
+
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
261
|
+
}
|
|
262
|
+
} else if (rel.type === "media") {
|
|
263
|
+
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
264
|
+
if (fkValues.length === 0) {
|
|
265
|
+
for (const row of rows) row[fieldName] = null;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const unique = [...new Set(fkValues)];
|
|
269
|
+
const uncached = unique.filter((id) => !cache.has(`media:${id}`));
|
|
270
|
+
if (uncached.length > 0) {
|
|
271
|
+
const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
272
|
+
const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
|
|
273
|
+
for (const item of result.rows) {
|
|
274
|
+
cache.set(`media:${item["id"]}`, item);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const row of rows) {
|
|
278
|
+
const fkVal = row[rel.fk_column];
|
|
279
|
+
row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function resolveRelationBareIds(db, rows, fieldName, rel) {
|
|
284
|
+
if (rows.length === 0) return;
|
|
285
|
+
if (rel.type === "paragraph") {
|
|
286
|
+
const parentIds = rows.map((r) => r["id"]);
|
|
287
|
+
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
288
|
+
const result = await db.execute(
|
|
289
|
+
import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
290
|
+
);
|
|
291
|
+
const byParent = groupBy(result.rows, "parent_id");
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
|
|
294
|
+
}
|
|
295
|
+
} else if (rel.type === "junction") {
|
|
296
|
+
const parentIds = rows.map((r) => r["id"]);
|
|
297
|
+
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
298
|
+
const result = await db.execute(
|
|
299
|
+
import_drizzle_orm2.sql`SELECT ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} AS left_id, ${import_drizzle_orm2.sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
|
|
300
|
+
);
|
|
301
|
+
const byLeft = groupBy(result.rows, "left_id");
|
|
302
|
+
for (const row of rows) {
|
|
303
|
+
row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
var import_node_crypto, import_drizzle_orm2;
|
|
308
|
+
var init_relations = __esm({
|
|
309
|
+
"src/relations.ts"() {
|
|
310
|
+
"use strict";
|
|
311
|
+
import_node_crypto = __toESM(require("crypto"));
|
|
312
|
+
import_drizzle_orm2 = require("drizzle-orm");
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// src/graphql/scalars.ts
|
|
317
|
+
var import_graphql, DateTimeScalar, JSONScalar;
|
|
318
|
+
var init_scalars = __esm({
|
|
319
|
+
"src/graphql/scalars.ts"() {
|
|
320
|
+
"use strict";
|
|
321
|
+
import_graphql = require("graphql");
|
|
322
|
+
DateTimeScalar = new import_graphql.GraphQLScalarType({
|
|
323
|
+
name: "DateTime",
|
|
324
|
+
description: "ISO-8601 date-time string",
|
|
325
|
+
serialize(value) {
|
|
326
|
+
if (value instanceof Date) return value.toISOString();
|
|
327
|
+
if (typeof value === "string") return value;
|
|
328
|
+
if (typeof value === "number") return new Date(value).toISOString();
|
|
329
|
+
throw new TypeError(`DateTime cannot serialize value: ${String(value)}`);
|
|
330
|
+
},
|
|
331
|
+
parseValue(value) {
|
|
332
|
+
if (typeof value !== "string") throw new TypeError("DateTime must be a string");
|
|
333
|
+
return value;
|
|
334
|
+
},
|
|
335
|
+
parseLiteral(ast) {
|
|
336
|
+
if (ast.kind !== import_graphql.Kind.STRING) throw new TypeError("DateTime must be a string");
|
|
337
|
+
return ast.value;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
JSONScalar = new import_graphql.GraphQLScalarType({
|
|
341
|
+
name: "JSON",
|
|
342
|
+
description: "Arbitrary JSON value",
|
|
343
|
+
serialize: (value) => value,
|
|
344
|
+
parseValue: (value) => value,
|
|
345
|
+
parseLiteral: (ast, variables) => (0, import_graphql.valueFromASTUntyped)(ast, variables ?? void 0)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// src/graphql/type-mapping.ts
|
|
351
|
+
function scalarOutputType(fieldType) {
|
|
352
|
+
switch (fieldType) {
|
|
353
|
+
case "text/plain":
|
|
354
|
+
case "text/rich":
|
|
355
|
+
return import_graphql2.GraphQLString;
|
|
356
|
+
case "integer":
|
|
357
|
+
return import_graphql2.GraphQLInt;
|
|
358
|
+
case "float":
|
|
359
|
+
return import_graphql2.GraphQLFloat;
|
|
360
|
+
case "boolean":
|
|
361
|
+
return import_graphql2.GraphQLBoolean;
|
|
362
|
+
case "date":
|
|
363
|
+
return DateTimeScalar;
|
|
364
|
+
case "programmatic":
|
|
365
|
+
return JSONScalar;
|
|
366
|
+
default:
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
var import_graphql2;
|
|
371
|
+
var init_type_mapping = __esm({
|
|
372
|
+
"src/graphql/type-mapping.ts"() {
|
|
373
|
+
"use strict";
|
|
374
|
+
import_graphql2 = require("graphql");
|
|
375
|
+
init_scalars();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// src/graphql/naming.ts
|
|
380
|
+
function schemaSegment(machineName) {
|
|
381
|
+
const idx = machineName.indexOf("--");
|
|
382
|
+
return idx === -1 ? machineName : machineName.slice(idx + 2);
|
|
383
|
+
}
|
|
384
|
+
function words(input) {
|
|
385
|
+
return input.split(/[_\s-]+/).filter(Boolean);
|
|
386
|
+
}
|
|
387
|
+
function toPascalCase(input) {
|
|
388
|
+
return words(input).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
389
|
+
}
|
|
390
|
+
function toCamelCase(input) {
|
|
391
|
+
const pascal = toPascalCase(input);
|
|
392
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
393
|
+
}
|
|
394
|
+
function pluralize(word) {
|
|
395
|
+
if (/[^aeiou]y$/.test(word)) return word.slice(0, -1) + "ies";
|
|
396
|
+
if (/(s|x|z|ch|sh)$/.test(word)) return word + "es";
|
|
397
|
+
return word + "s";
|
|
398
|
+
}
|
|
399
|
+
function isValidGraphQLName(name) {
|
|
400
|
+
return GRAPHQL_NAME.test(name);
|
|
401
|
+
}
|
|
402
|
+
function graphqlTypeName(machineName) {
|
|
403
|
+
return toPascalCase(schemaSegment(machineName));
|
|
404
|
+
}
|
|
405
|
+
function singleQueryName(machineName) {
|
|
406
|
+
return toCamelCase(schemaSegment(machineName));
|
|
407
|
+
}
|
|
408
|
+
function collectionQueryName(machineName) {
|
|
409
|
+
return pluralize(singleQueryName(machineName));
|
|
410
|
+
}
|
|
411
|
+
function buildFieldNameMap(schemaNames) {
|
|
412
|
+
const toG = /* @__PURE__ */ new Map();
|
|
413
|
+
const toS = /* @__PURE__ */ new Map();
|
|
414
|
+
for (const name of schemaNames) {
|
|
415
|
+
const g = toCamelCase(name);
|
|
416
|
+
toG.set(name, g);
|
|
417
|
+
toS.set(g, name);
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
toGraphql: (schemaName) => toG.get(schemaName) ?? toCamelCase(schemaName),
|
|
421
|
+
toSchema: (graphqlName) => toS.get(graphqlName) ?? graphqlName
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
var GRAPHQL_NAME;
|
|
425
|
+
var init_naming = __esm({
|
|
426
|
+
"src/graphql/naming.ts"() {
|
|
427
|
+
"use strict";
|
|
428
|
+
GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/graphql/filters.ts
|
|
433
|
+
function buildSortFieldEnum(typeName) {
|
|
434
|
+
return new import_graphql3.GraphQLEnumType({
|
|
435
|
+
name: `${typeName}SortField`,
|
|
436
|
+
values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.column }]))
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function comparableFilterInput(name, scalar) {
|
|
440
|
+
return new import_graphql3.GraphQLInputObjectType({
|
|
441
|
+
name,
|
|
442
|
+
fields: {
|
|
443
|
+
eq: { type: scalar },
|
|
444
|
+
in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) },
|
|
445
|
+
gt: { type: scalar },
|
|
446
|
+
gte: { type: scalar },
|
|
447
|
+
lt: { type: scalar },
|
|
448
|
+
lte: { type: scalar }
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function equalityFilterInput(name, scalar) {
|
|
453
|
+
return new import_graphql3.GraphQLInputObjectType({
|
|
454
|
+
name,
|
|
455
|
+
fields: {
|
|
456
|
+
eq: { type: scalar },
|
|
457
|
+
in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) }
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function filterInputForField(fieldType) {
|
|
462
|
+
const scalar = scalarOutputType(fieldType);
|
|
463
|
+
if (fieldType === "boolean") return BooleanFilter;
|
|
464
|
+
if (fieldType === "date") return DateTimeFilter;
|
|
465
|
+
if (fieldType === "integer") return IntFilter;
|
|
466
|
+
if (fieldType === "float") return FloatFilter;
|
|
467
|
+
if (fieldType === "reference") return IDFilter;
|
|
468
|
+
if (scalar === null) return null;
|
|
469
|
+
return StringFilter;
|
|
470
|
+
}
|
|
471
|
+
function buildFilterInputType(type) {
|
|
472
|
+
const fields = {};
|
|
473
|
+
fields["id"] = { type: IDFilter };
|
|
474
|
+
fields["published"] = { type: BooleanFilter };
|
|
475
|
+
fields["createdAt"] = { type: DateTimeFilter };
|
|
476
|
+
fields["updatedAt"] = { type: DateTimeFilter };
|
|
477
|
+
if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
|
|
478
|
+
for (const f of type.fields) {
|
|
479
|
+
if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
|
|
480
|
+
if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
|
|
481
|
+
const input = filterInputForField(f.field_type);
|
|
482
|
+
if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
|
|
483
|
+
}
|
|
484
|
+
if (Object.keys(fields).length === 0) return null;
|
|
485
|
+
return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
|
|
486
|
+
}
|
|
487
|
+
function translateFilters(input, nameMap) {
|
|
488
|
+
const out = {};
|
|
489
|
+
if (!input) return out;
|
|
490
|
+
for (const [gqlField, raw] of Object.entries(input)) {
|
|
491
|
+
if (raw === null || typeof raw !== "object") continue;
|
|
492
|
+
const column = nameMap.toSchema(gqlField);
|
|
493
|
+
const spec = raw;
|
|
494
|
+
if (spec["eq"] !== void 0) {
|
|
495
|
+
out[column] = spec["eq"];
|
|
496
|
+
} else if (Array.isArray(spec["in"])) {
|
|
497
|
+
out[column] = spec["in"];
|
|
498
|
+
} else {
|
|
499
|
+
const op = {};
|
|
500
|
+
for (const k of ["gt", "gte", "lt", "lte"]) {
|
|
501
|
+
if (spec[k] !== void 0) op[k] = spec[k];
|
|
502
|
+
}
|
|
503
|
+
if (Object.keys(op).length > 0) out[column] = op;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
var import_graphql3, SortOrderEnum, SORTABLE, StringFilter, BooleanFilter, DateTimeFilter, IDFilter, IntFilter, FloatFilter;
|
|
509
|
+
var init_filters = __esm({
|
|
510
|
+
"src/graphql/filters.ts"() {
|
|
511
|
+
"use strict";
|
|
512
|
+
import_graphql3 = require("graphql");
|
|
513
|
+
init_naming();
|
|
514
|
+
init_type_mapping();
|
|
515
|
+
init_scalars();
|
|
516
|
+
SortOrderEnum = new import_graphql3.GraphQLEnumType({
|
|
517
|
+
name: "SortOrder",
|
|
518
|
+
values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
|
|
519
|
+
});
|
|
520
|
+
SORTABLE = [
|
|
521
|
+
{ gql: "title", column: "title" },
|
|
522
|
+
{ gql: "createdAt", column: "created_at" },
|
|
523
|
+
{ gql: "updatedAt", column: "updated_at" }
|
|
524
|
+
];
|
|
525
|
+
StringFilter = equalityFilterInput("StringFilter", import_graphql3.GraphQLString);
|
|
526
|
+
BooleanFilter = new import_graphql3.GraphQLInputObjectType({
|
|
527
|
+
name: "BooleanFilter",
|
|
528
|
+
fields: { eq: { type: import_graphql3.GraphQLBoolean } }
|
|
529
|
+
});
|
|
530
|
+
DateTimeFilter = comparableFilterInput("DateTimeFilter", DateTimeScalar);
|
|
531
|
+
IDFilter = equalityFilterInput("IDFilter", import_graphql3.GraphQLString);
|
|
532
|
+
IntFilter = comparableFilterInput("IntFilter", import_graphql3.GraphQLInt);
|
|
533
|
+
FloatFilter = comparableFilterInput("FloatFilter", import_graphql3.GraphQLFloat);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// src/graphql/resolvers.ts
|
|
538
|
+
function codeError2(code, message) {
|
|
539
|
+
return new import_graphql4.GraphQLError(message, { extensions: { code } });
|
|
540
|
+
}
|
|
541
|
+
function isPublished2(item) {
|
|
542
|
+
return !!item && item["published"] === true;
|
|
543
|
+
}
|
|
544
|
+
function scalarFieldResolver(schemaName) {
|
|
545
|
+
return (parent) => parent[schemaName];
|
|
546
|
+
}
|
|
547
|
+
function relationFieldResolver(typeName, schemaFieldName) {
|
|
548
|
+
return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
|
|
549
|
+
}
|
|
550
|
+
function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
|
|
551
|
+
return async (parent, _args, ctx) => {
|
|
552
|
+
let p = ctx.programmaticMemo.get(parent);
|
|
553
|
+
if (!p) {
|
|
554
|
+
p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
|
|
555
|
+
ctx.programmaticMemo.set(parent, p);
|
|
556
|
+
}
|
|
557
|
+
return (await p)[schemaFieldName];
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
|
|
561
|
+
if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, parent);
|
|
562
|
+
const enriched = { ...parent };
|
|
563
|
+
await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
|
|
564
|
+
return ctx.resolver.resolveItem(typeName, enriched);
|
|
565
|
+
}
|
|
566
|
+
function collectionResolver(typeName, nameMap) {
|
|
567
|
+
return async (_root, args, ctx) => {
|
|
568
|
+
const page = args.page ?? 1;
|
|
569
|
+
const perPage = args.perPage ?? 10;
|
|
570
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
571
|
+
throw codeError2("INVALID_PAGINATION", "page must be \u2265 1");
|
|
572
|
+
}
|
|
573
|
+
if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {
|
|
574
|
+
throw codeError2("INVALID_PAGINATION", "perPage must be between 1 and 100");
|
|
575
|
+
}
|
|
576
|
+
const repo = ctx.repos[typeName];
|
|
577
|
+
const result = await repo.findMany({
|
|
578
|
+
published_only: true,
|
|
579
|
+
page,
|
|
580
|
+
per_page: perPage,
|
|
581
|
+
sort_by: args.sortBy ?? "created_at",
|
|
582
|
+
sort_order: args.sortOrder ?? "asc",
|
|
583
|
+
filters: translateFilters(args.filter, nameMap)
|
|
584
|
+
});
|
|
585
|
+
return { data: result.data, meta: result.meta };
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function singleBySlugResolver(typeName) {
|
|
589
|
+
return async (_root, args, ctx) => {
|
|
590
|
+
const repo = ctx.repos[typeName];
|
|
591
|
+
const item = await repo.findBySlug(args.slug);
|
|
592
|
+
return isPublished2(item) ? item : null;
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function singletonResolver(typeName) {
|
|
596
|
+
return async (_root, _args, ctx) => {
|
|
597
|
+
const repo = ctx.repos[typeName];
|
|
598
|
+
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
599
|
+
return result.data[0] ?? null;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function taxonomySingleResolver(typeName) {
|
|
603
|
+
return async (_root, args, ctx) => {
|
|
604
|
+
const repo = ctx.repos[typeName];
|
|
605
|
+
const item = await repo.findOne(args.id);
|
|
606
|
+
return isPublished2(item) ? item : null;
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var import_graphql4;
|
|
610
|
+
var init_resolvers = __esm({
|
|
611
|
+
"src/graphql/resolvers.ts"() {
|
|
612
|
+
"use strict";
|
|
613
|
+
import_graphql4 = require("graphql");
|
|
614
|
+
init_filters();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// src/graphql/schema.ts
|
|
619
|
+
function buildGraphQLSchema(registry) {
|
|
620
|
+
const objectTypes = /* @__PURE__ */ new Map();
|
|
621
|
+
const enumTypes = /* @__PURE__ */ new Map();
|
|
622
|
+
for (const [name, enumType] of Object.entries(registry.enum_types)) {
|
|
623
|
+
if (enumType.values.every(isValidGraphQLName)) {
|
|
624
|
+
enumTypes.set(
|
|
625
|
+
name,
|
|
626
|
+
new import_graphql5.GraphQLEnumType({
|
|
627
|
+
name: graphqlTypeName(name),
|
|
628
|
+
values: Object.fromEntries(enumType.values.map((v) => [v, { value: v }]))
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
} else {
|
|
632
|
+
process.stderr.write(
|
|
633
|
+
`\u26A0 enum '${name}' has values that aren't valid GraphQL identifiers; exposing as String
|
|
634
|
+
`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function outputTypeForField(field) {
|
|
639
|
+
const scalar = scalarOutputType(field.field_type);
|
|
640
|
+
if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
|
|
641
|
+
if (field.field_type === "enum") {
|
|
642
|
+
const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
|
|
643
|
+
const et = ref ? enumTypes.get(ref) : void 0;
|
|
644
|
+
const t = et ?? import_graphql5.GraphQLString;
|
|
645
|
+
return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
|
|
646
|
+
}
|
|
647
|
+
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
648
|
+
return MEDIA;
|
|
649
|
+
}
|
|
650
|
+
if (field.field_type === "paragraph") {
|
|
651
|
+
const ref = field.ui_component.component === "paragraph-embed" ? field.ui_component.ref : void 0;
|
|
652
|
+
const target = ref ? objectTypes.get(ref) : void 0;
|
|
653
|
+
if (ref && !target) {
|
|
654
|
+
process.stderr.write(
|
|
655
|
+
`\u26A0 paragraph field '${field.name}' targets unknown paragraph type '${ref}'; exposing as Media
|
|
656
|
+
`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(target ?? MEDIA));
|
|
660
|
+
}
|
|
661
|
+
if (field.field_type === "reference") {
|
|
662
|
+
const ref = field.ui_component.component === "typeahead-select" ? field.ui_component.ref : void 0;
|
|
663
|
+
const target = ref ? objectTypes.get(ref) : void 0;
|
|
664
|
+
if (ref && !target) {
|
|
665
|
+
process.stderr.write(`\u26A0 reference field '${field.name}' targets unknown type '${ref}'; exposing as Media
|
|
666
|
+
`);
|
|
667
|
+
}
|
|
668
|
+
const rel = field.ui_component.component === "typeahead-select" ? field.ui_component.rel : void 0;
|
|
669
|
+
const isMany = rel === "many-to-many" || rel === "one-to-many";
|
|
670
|
+
const t = target ?? MEDIA;
|
|
671
|
+
return isMany ? new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t)) : t;
|
|
672
|
+
}
|
|
673
|
+
return import_graphql5.GraphQLString;
|
|
674
|
+
}
|
|
675
|
+
function buildObjectType(machineName, type) {
|
|
676
|
+
const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
|
|
677
|
+
return new import_graphql5.GraphQLObjectType({
|
|
678
|
+
name: graphqlTypeName(machineName),
|
|
679
|
+
fields: () => {
|
|
680
|
+
const fields = {};
|
|
681
|
+
fields["id"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (p) => p["id"] };
|
|
682
|
+
if (type.schema_type !== "paragraph-type") {
|
|
683
|
+
fields["published"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (p) => p["published"] };
|
|
684
|
+
}
|
|
685
|
+
if (type.schema_type === "content-type") {
|
|
686
|
+
fields["slug"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (p) => p["slug"] };
|
|
687
|
+
}
|
|
688
|
+
fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
|
|
689
|
+
fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
|
|
690
|
+
for (const field of type.fields) {
|
|
691
|
+
const gqlName = toCamelCase(field.name);
|
|
692
|
+
const outType = outputTypeForField(field);
|
|
693
|
+
let resolve;
|
|
694
|
+
if (field.field_type === "programmatic") {
|
|
695
|
+
resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames);
|
|
696
|
+
} else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
697
|
+
resolve = relationFieldResolver(machineName, field.name);
|
|
698
|
+
} else {
|
|
699
|
+
resolve = scalarFieldResolver(field.name);
|
|
700
|
+
}
|
|
701
|
+
fields[gqlName] = { type: outType, resolve };
|
|
702
|
+
}
|
|
703
|
+
return fields;
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
const typeNameOwners = /* @__PURE__ */ new Map();
|
|
708
|
+
function registerTypeName(machineName) {
|
|
709
|
+
const gqlName = graphqlTypeName(machineName);
|
|
710
|
+
const owner = typeNameOwners.get(gqlName);
|
|
711
|
+
if (owner !== void 0 && owner !== machineName) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`GraphQL type name collision: '${gqlName}' is produced by both '${owner}' and '${machineName}'. Rename one schema's machine name segment so their GraphQL type names differ.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
typeNameOwners.set(gqlName, machineName);
|
|
717
|
+
}
|
|
718
|
+
for (const [name, ct] of Object.entries(registry.content_types)) {
|
|
719
|
+
registerTypeName(name);
|
|
720
|
+
objectTypes.set(name, buildObjectType(name, ct));
|
|
721
|
+
}
|
|
722
|
+
for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
|
|
723
|
+
registerTypeName(name);
|
|
724
|
+
objectTypes.set(name, buildObjectType(name, tt));
|
|
725
|
+
}
|
|
726
|
+
for (const [name, pt] of Object.entries(registry.paragraph_types)) {
|
|
727
|
+
registerTypeName(name);
|
|
728
|
+
objectTypes.set(name, buildObjectType(name, pt));
|
|
729
|
+
}
|
|
730
|
+
const queryFields = {};
|
|
731
|
+
for (const [name, ct] of Object.entries(registry.content_types)) {
|
|
732
|
+
const objType = objectTypes.get(name);
|
|
733
|
+
const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
|
|
734
|
+
if (ct.only_one) {
|
|
735
|
+
queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
const listType = new import_graphql5.GraphQLObjectType({
|
|
739
|
+
name: `${graphqlTypeName(name)}List`,
|
|
740
|
+
fields: {
|
|
741
|
+
data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
|
|
742
|
+
meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
const filterType = buildFilterInputType(ct);
|
|
746
|
+
queryFields[collectionQueryName(name)] = {
|
|
747
|
+
type: new import_graphql5.GraphQLNonNull(listType),
|
|
748
|
+
args: {
|
|
749
|
+
page: { type: import_graphql5.GraphQLInt },
|
|
750
|
+
perPage: { type: import_graphql5.GraphQLInt },
|
|
751
|
+
sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
|
|
752
|
+
sortOrder: { type: SortOrderEnum },
|
|
753
|
+
...filterType ? { filter: { type: filterType } } : {}
|
|
754
|
+
},
|
|
755
|
+
resolve: collectionResolver(name, nameMap)
|
|
756
|
+
};
|
|
757
|
+
queryFields[singleQueryName(name)] = {
|
|
758
|
+
type: objType,
|
|
759
|
+
args: { slug: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString) } },
|
|
760
|
+
resolve: singleBySlugResolver(name)
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
|
|
764
|
+
const objType = objectTypes.get(name);
|
|
765
|
+
const listType = new import_graphql5.GraphQLObjectType({
|
|
766
|
+
name: `${graphqlTypeName(name)}List`,
|
|
767
|
+
fields: {
|
|
768
|
+
data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
|
|
769
|
+
meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
queryFields[collectionQueryName(name)] = {
|
|
773
|
+
type: new import_graphql5.GraphQLNonNull(listType),
|
|
774
|
+
args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
|
|
775
|
+
resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
|
|
776
|
+
};
|
|
777
|
+
queryFields[singleQueryName(name)] = {
|
|
778
|
+
type: objType,
|
|
779
|
+
args: { id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID) } },
|
|
780
|
+
resolve: taxonomySingleResolver(name)
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return new import_graphql5.GraphQLSchema({
|
|
784
|
+
query: new import_graphql5.GraphQLObjectType({ name: "Query", fields: queryFields })
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
var import_graphql5, PAGE_META, MEDIA;
|
|
788
|
+
var init_schema = __esm({
|
|
789
|
+
"src/graphql/schema.ts"() {
|
|
790
|
+
"use strict";
|
|
791
|
+
import_graphql5 = require("graphql");
|
|
792
|
+
init_scalars();
|
|
793
|
+
init_type_mapping();
|
|
794
|
+
init_naming();
|
|
795
|
+
init_filters();
|
|
796
|
+
init_resolvers();
|
|
797
|
+
PAGE_META = new import_graphql5.GraphQLObjectType({
|
|
798
|
+
name: "PageMeta",
|
|
799
|
+
fields: {
|
|
800
|
+
total: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total },
|
|
801
|
+
page: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.page },
|
|
802
|
+
perPage: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.per_page },
|
|
803
|
+
totalPages: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total_pages },
|
|
804
|
+
hasNext: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_next },
|
|
805
|
+
hasPrev: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_prev }
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
MEDIA = new import_graphql5.GraphQLObjectType({
|
|
809
|
+
name: "Media",
|
|
810
|
+
fields: {
|
|
811
|
+
id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (m) => m.id },
|
|
812
|
+
type: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.type },
|
|
813
|
+
url: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.url },
|
|
814
|
+
mimeType: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.mime_type },
|
|
815
|
+
alt: { type: import_graphql5.GraphQLString, resolve: (m) => m.alt },
|
|
816
|
+
fileSize: { type: import_graphql5.GraphQLInt, resolve: (m) => m.file_size },
|
|
817
|
+
width: { type: import_graphql5.GraphQLInt, resolve: (m) => m.width },
|
|
818
|
+
height: { type: import_graphql5.GraphQLInt, resolve: (m) => m.height },
|
|
819
|
+
duration: { type: import_graphql5.GraphQLInt, resolve: (m) => m.duration }
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// src/graphql/dataloaders.ts
|
|
826
|
+
function createRelationLoaders(db, registry) {
|
|
827
|
+
const relMaps = /* @__PURE__ */ new Map();
|
|
828
|
+
const loaders = /* @__PURE__ */ new Map();
|
|
829
|
+
const cache = /* @__PURE__ */ new Map();
|
|
830
|
+
function relationsFor(typeName) {
|
|
831
|
+
let m = relMaps.get(typeName);
|
|
832
|
+
if (!m) {
|
|
833
|
+
const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName] ?? registry.paragraph_types[typeName];
|
|
834
|
+
m = type ? buildRelationsMap(type.fields, registry) : {};
|
|
835
|
+
relMaps.set(typeName, m);
|
|
836
|
+
}
|
|
837
|
+
return m;
|
|
838
|
+
}
|
|
839
|
+
function loaderFor(typeName, fieldName, rel) {
|
|
840
|
+
const key = `${typeName}:${fieldName}`;
|
|
841
|
+
let dl = loaders.get(key);
|
|
842
|
+
if (!dl) {
|
|
843
|
+
dl = new import_dataloader.default(
|
|
844
|
+
async (parents) => {
|
|
845
|
+
const rows = parents;
|
|
846
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, true);
|
|
847
|
+
return rows.map((r) => r[fieldName]);
|
|
848
|
+
},
|
|
849
|
+
// Batch within a tick; do not memoize by parent identity (rows are mutated
|
|
850
|
+
// and may recur across nesting levels).
|
|
851
|
+
{ cache: false }
|
|
852
|
+
);
|
|
853
|
+
loaders.set(key, dl);
|
|
854
|
+
}
|
|
855
|
+
return dl;
|
|
856
|
+
}
|
|
857
|
+
return {
|
|
858
|
+
async load(typeName, fieldName, parent) {
|
|
859
|
+
const rel = relationsFor(typeName)[fieldName];
|
|
860
|
+
if (!rel) return null;
|
|
861
|
+
return loaderFor(typeName, fieldName, rel).load(parent);
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
var import_dataloader;
|
|
866
|
+
var init_dataloaders = __esm({
|
|
867
|
+
"src/graphql/dataloaders.ts"() {
|
|
868
|
+
"use strict";
|
|
869
|
+
import_dataloader = __toESM(require("dataloader"));
|
|
870
|
+
init_relations();
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// src/graphql/security.ts
|
|
875
|
+
function buildArmorPlugin(options) {
|
|
876
|
+
const armor = new import_graphql_armor.EnvelopArmor({
|
|
877
|
+
maxDepth: { n: options.maxDepth },
|
|
878
|
+
costLimit: { maxCost: options.maxComplexity },
|
|
879
|
+
maxAliases: { n: 15 },
|
|
880
|
+
maxDirectives: { n: 50 },
|
|
881
|
+
blockFieldSuggestion: { enabled: true }
|
|
882
|
+
// maxTokens is intentionally left at Armor's secure default (n=1000) to bound
|
|
883
|
+
// GraphQL document token count at parse time, a layer the depth/cost/alias/directive
|
|
884
|
+
// limits do not cover.
|
|
885
|
+
});
|
|
886
|
+
return armor.protect();
|
|
887
|
+
}
|
|
888
|
+
function introspectionPlugin(enabled) {
|
|
889
|
+
return {
|
|
890
|
+
onValidate({ addValidationRule }) {
|
|
891
|
+
if (!enabled) addValidationRule(import_graphql6.NoSchemaIntrospectionCustomRule);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
var import_graphql_armor, import_graphql6;
|
|
896
|
+
var init_security = __esm({
|
|
897
|
+
"src/graphql/security.ts"() {
|
|
898
|
+
"use strict";
|
|
899
|
+
import_graphql_armor = require("@escape.tech/graphql-armor");
|
|
900
|
+
import_graphql6 = require("graphql");
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// src/graphql/handler.ts
|
|
905
|
+
var handler_exports = {};
|
|
906
|
+
__export(handler_exports, {
|
|
907
|
+
createGraphQLHandler: () => createGraphQLHandler
|
|
908
|
+
});
|
|
909
|
+
function isClientSafeGraphQLError(error) {
|
|
910
|
+
let current = error;
|
|
911
|
+
while (current != null && typeof current === "object" && current.name === "GraphQLError") {
|
|
912
|
+
if (current.originalError == null) return true;
|
|
913
|
+
current = current.originalError;
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
function createGraphQLHandler(registry, repos, resolver, db, options) {
|
|
918
|
+
const schema = buildGraphQLSchema(registry);
|
|
919
|
+
const { plugins } = buildArmorPlugin({
|
|
920
|
+
maxDepth: options.maxDepth,
|
|
921
|
+
maxComplexity: options.maxComplexity
|
|
922
|
+
});
|
|
923
|
+
const yoga = (0, import_graphql_yoga.createYoga)({
|
|
924
|
+
schema,
|
|
925
|
+
graphqlEndpoint: "/graphql",
|
|
926
|
+
graphiql: options.graphiql,
|
|
927
|
+
landingPage: false,
|
|
928
|
+
maskedErrors: {
|
|
929
|
+
maskError: (error, message, isDev) => isClientSafeGraphQLError(error) ? error : (0, import_graphql_yoga.maskError)(error, message, isDev)
|
|
930
|
+
},
|
|
931
|
+
// buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's
|
|
932
|
+
// plugin shapes aren't part of its public typings; Yoga's `plugins` option only
|
|
933
|
+
// accepts its own `Plugin` type, so the cast bridges the two.
|
|
934
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
935
|
+
plugins: [...plugins, introspectionPlugin(options.introspection)],
|
|
936
|
+
context: () => ({
|
|
937
|
+
db,
|
|
938
|
+
registry,
|
|
939
|
+
repos,
|
|
940
|
+
resolver,
|
|
941
|
+
loaders: createRelationLoaders(db, registry),
|
|
942
|
+
programmaticMemo: /* @__PURE__ */ new WeakMap()
|
|
943
|
+
})
|
|
944
|
+
});
|
|
945
|
+
return (c) => yoga.fetch(c.req.raw, {});
|
|
946
|
+
}
|
|
947
|
+
var import_graphql_yoga;
|
|
948
|
+
var init_handler = __esm({
|
|
949
|
+
"src/graphql/handler.ts"() {
|
|
950
|
+
"use strict";
|
|
951
|
+
import_graphql_yoga = require("graphql-yoga");
|
|
952
|
+
init_schema();
|
|
953
|
+
init_dataloaders();
|
|
954
|
+
init_security();
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
30
958
|
// src/index.ts
|
|
31
959
|
var index_exports = {};
|
|
32
960
|
__export(index_exports, {
|
|
@@ -72,26 +1000,43 @@ function createCorsMiddleware(corsConfig) {
|
|
|
72
1000
|
}
|
|
73
1001
|
|
|
74
1002
|
// src/middleware/security-headers.ts
|
|
1003
|
+
var GRAPHIQL_CDN = "https://unpkg.com";
|
|
75
1004
|
function createSecurityHeadersMiddleware(options = {}) {
|
|
76
1005
|
const connectSrc = ["'self'", ...options.connectSrc ?? []].join(" ");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1006
|
+
function buildCsp(parts) {
|
|
1007
|
+
return [
|
|
1008
|
+
"default-src 'self'",
|
|
1009
|
+
"img-src 'self' data: https:",
|
|
1010
|
+
"media-src 'self' https:",
|
|
1011
|
+
`style-src ${parts.styleSrc}`,
|
|
1012
|
+
`script-src ${parts.scriptSrc}`,
|
|
1013
|
+
"font-src 'self' data:",
|
|
1014
|
+
`connect-src ${parts.connectSrc}`,
|
|
1015
|
+
...parts.workerSrc ? [`worker-src ${parts.workerSrc}`] : [],
|
|
1016
|
+
"frame-ancestors 'none'",
|
|
1017
|
+
"base-uri 'self'",
|
|
1018
|
+
"form-action 'self'"
|
|
1019
|
+
].join("; ");
|
|
1020
|
+
}
|
|
1021
|
+
const csp = buildCsp({
|
|
1022
|
+
scriptSrc: "'self'",
|
|
1023
|
+
styleSrc: "'self' 'unsafe-inline'",
|
|
1024
|
+
connectSrc
|
|
1025
|
+
});
|
|
1026
|
+
const graphiqlCsp = buildCsp({
|
|
1027
|
+
scriptSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
1028
|
+
styleSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
|
|
1029
|
+
connectSrc: `${connectSrc} ${GRAPHIQL_CDN}`,
|
|
1030
|
+
workerSrc: "'self' blob:"
|
|
1031
|
+
});
|
|
1032
|
+
const graphiqlPath = options.graphiqlPath;
|
|
89
1033
|
return async function securityHeaders(c, next) {
|
|
90
1034
|
await next();
|
|
91
1035
|
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
92
1036
|
c.res.headers.set("X-Frame-Options", "DENY");
|
|
93
1037
|
c.res.headers.set("Referrer-Policy", "no-referrer");
|
|
94
|
-
c.
|
|
1038
|
+
const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
|
|
1039
|
+
c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
|
|
95
1040
|
};
|
|
96
1041
|
}
|
|
97
1042
|
|
|
@@ -890,304 +1835,101 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
890
1835
|
if (typeParam !== void 0 && !VALID_MEDIA_TYPES.has(typeParam)) {
|
|
891
1836
|
return c.json(
|
|
892
1837
|
{
|
|
893
|
-
ok: false,
|
|
894
|
-
error: {
|
|
895
|
-
code: "VALIDATION_ERROR",
|
|
896
|
-
message: `type must be one of: image, video, file`
|
|
897
|
-
}
|
|
898
|
-
},
|
|
899
|
-
422
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
const opts = {
|
|
903
|
-
page: pagination.page,
|
|
904
|
-
per_page: pagination.per_page
|
|
905
|
-
};
|
|
906
|
-
if (typeParam !== void 0) {
|
|
907
|
-
opts.type = typeParam;
|
|
908
|
-
}
|
|
909
|
-
const result = await mediaRepo.findMany(opts);
|
|
910
|
-
return c.json(result);
|
|
911
|
-
};
|
|
912
|
-
if (listRateLimit) {
|
|
913
|
-
app.get("/api/media", listRateLimit, mediaListHandler);
|
|
914
|
-
} else {
|
|
915
|
-
app.get("/api/media", mediaListHandler);
|
|
916
|
-
}
|
|
917
|
-
app.get("/api/media/:id", async (c) => {
|
|
918
|
-
const id = c.req.param("id");
|
|
919
|
-
const item = await mediaRepo.findOne(id);
|
|
920
|
-
if (!item) {
|
|
921
|
-
return c.json(
|
|
922
|
-
{ ok: false, error: { code: "NOT_FOUND", message: "Media item not found" } },
|
|
923
|
-
404
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
return c.json({ ok: true, data: item });
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// src/routes/admin/content.ts
|
|
931
|
-
var import_drizzle_orm3 = require("drizzle-orm");
|
|
932
|
-
|
|
933
|
-
// src/media-references.ts
|
|
934
|
-
function extractMediaId(value) {
|
|
935
|
-
return typeof value === "string" && value !== "" ? value : null;
|
|
936
|
-
}
|
|
937
|
-
function topLevelMediaDelta(mediaFields, before, after) {
|
|
938
|
-
const added = [];
|
|
939
|
-
const removed = [];
|
|
940
|
-
for (const f of mediaFields) {
|
|
941
|
-
if (after !== null && !(f.name in after)) continue;
|
|
942
|
-
const oldId = before ? extractMediaId(before[f.name]) : null;
|
|
943
|
-
const newId = after ? extractMediaId(after[f.name]) : null;
|
|
944
|
-
if (newId === oldId) continue;
|
|
945
|
-
if (oldId) removed.push(oldId);
|
|
946
|
-
if (newId) added.push(newId);
|
|
947
|
-
}
|
|
948
|
-
return { added, removed };
|
|
949
|
-
}
|
|
950
|
-
function mergeMediaDeltas(...deltas) {
|
|
951
|
-
return {
|
|
952
|
-
added: deltas.flatMap((d) => d.added),
|
|
953
|
-
removed: deltas.flatMap((d) => d.removed)
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
957
|
-
const added = new Set(delta.added);
|
|
958
|
-
const removed = new Set(delta.removed);
|
|
959
|
-
const netAdd = [...added].filter((id) => !removed.has(id));
|
|
960
|
-
const netRemove = [...removed].filter((id) => !added.has(id));
|
|
961
|
-
if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
|
|
962
|
-
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// src/relations.ts
|
|
966
|
-
var import_node_crypto = __toESM(require("crypto"));
|
|
967
|
-
var import_drizzle_orm2 = require("drizzle-orm");
|
|
968
|
-
function quoteIdent(name) {
|
|
969
|
-
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
970
|
-
return `"${name}"`;
|
|
971
|
-
}
|
|
972
|
-
function paragraphMediaFieldNames(pType) {
|
|
973
|
-
return new Set(
|
|
974
|
-
pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
|
|
978
|
-
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);
|
|
979
|
-
if (mediaColumns.length === 0) return [];
|
|
980
|
-
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
981
|
-
const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
|
|
982
|
-
const result = await db.execute(
|
|
983
|
-
import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
|
|
984
|
-
);
|
|
985
|
-
const ids = [];
|
|
986
|
-
for (const row of result.rows) {
|
|
987
|
-
for (const col of mediaColumns) {
|
|
988
|
-
const v = row[col];
|
|
989
|
-
if (typeof v === "string" && v !== "") ids.push(v);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return ids;
|
|
993
|
-
}
|
|
994
|
-
async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
|
|
995
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
996
|
-
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
997
|
-
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
998
|
-
const mediaFieldNames = paragraphMediaFieldNames(pType);
|
|
999
|
-
const added = [];
|
|
1000
|
-
for (let i = 0; i < items.length; i++) {
|
|
1001
|
-
const pItem = items[i];
|
|
1002
|
-
const data = {
|
|
1003
|
-
id: import_node_crypto.default.randomUUID(),
|
|
1004
|
-
parent_id: itemId,
|
|
1005
|
-
parent_type: parentType,
|
|
1006
|
-
parent_field: fieldName,
|
|
1007
|
-
order: i,
|
|
1008
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
1009
|
-
updated_at: /* @__PURE__ */ new Date()
|
|
1010
|
-
};
|
|
1011
|
-
for (const pf of pType.fields) {
|
|
1012
|
-
if (!pf.db_column || pf.db_column.junction) continue;
|
|
1013
|
-
data[pf.db_column.column_name] = pItem[pf.name] ?? null;
|
|
1014
|
-
}
|
|
1015
|
-
const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
|
|
1016
|
-
const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
|
|
1017
|
-
await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
|
|
1018
|
-
for (const name of mediaFieldNames) {
|
|
1019
|
-
const val = pItem[name];
|
|
1020
|
-
if (typeof val === "string" && val !== "") added.push(val);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return { added, removed };
|
|
1024
|
-
}
|
|
1025
|
-
async function deleteParagraphField(db, itemId, fieldName, pType) {
|
|
1026
|
-
const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
|
|
1027
|
-
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
|
|
1028
|
-
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
|
|
1029
|
-
return { added: [], removed };
|
|
1030
|
-
}
|
|
1031
|
-
async function persistJunctionField(db, itemId, junction, relatedIds) {
|
|
1032
|
-
const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
|
|
1033
|
-
const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
|
|
1034
|
-
const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
|
|
1035
|
-
await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
|
|
1036
|
-
for (const rid of relatedIds) {
|
|
1037
|
-
await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
function buildRelationsMap(fields, registry) {
|
|
1041
|
-
const relations = {};
|
|
1042
|
-
for (const field of fields) {
|
|
1043
|
-
if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
1044
|
-
if (!field.db_column) continue;
|
|
1045
|
-
relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
|
|
1046
|
-
} else if (field.field_type === "paragraph") {
|
|
1047
|
-
const ref = field.ui_component.ref;
|
|
1048
|
-
const pType = ref ? registry.paragraph_types[ref] : void 0;
|
|
1049
|
-
if (!pType) continue;
|
|
1050
|
-
relations[field.name] = { type: "paragraph", table: pType.db.table_name };
|
|
1051
|
-
} else if (field.field_type === "reference") {
|
|
1052
|
-
if (!field.db_column) continue;
|
|
1053
|
-
if (field.db_column.junction) {
|
|
1054
|
-
const j = field.db_column.junction;
|
|
1055
|
-
relations[field.name] = {
|
|
1056
|
-
type: "junction",
|
|
1057
|
-
table: j.right_table,
|
|
1058
|
-
junction_table: j.table_name,
|
|
1059
|
-
left_column: j.left_column,
|
|
1060
|
-
right_column: j.right_column,
|
|
1061
|
-
order_column: j.order_column
|
|
1062
|
-
};
|
|
1063
|
-
} else if (field.db_column.foreign_key) {
|
|
1064
|
-
relations[field.name] = {
|
|
1065
|
-
type: "reference",
|
|
1066
|
-
table: field.db_column.foreign_key.table,
|
|
1067
|
-
fk_column: field.db_column.column_name
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
return relations;
|
|
1073
|
-
}
|
|
1074
|
-
function groupBy(items, key) {
|
|
1075
|
-
const result = {};
|
|
1076
|
-
for (const item of items) {
|
|
1077
|
-
const k = String(item[key]);
|
|
1078
|
-
if (!result[k]) result[k] = [];
|
|
1079
|
-
result[k].push(item);
|
|
1080
|
-
}
|
|
1081
|
-
return result;
|
|
1082
|
-
}
|
|
1083
|
-
async function resolveRelationField(db, rows, fieldName, rel, cache) {
|
|
1084
|
-
if (rows.length === 0) return;
|
|
1085
|
-
if (rel.type === "paragraph") {
|
|
1086
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1087
|
-
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1088
|
-
const result = await db.execute(
|
|
1089
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
|
|
1090
|
-
);
|
|
1091
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1092
|
-
for (const row of rows) {
|
|
1093
|
-
row[fieldName] = byParent[row["id"]] ?? [];
|
|
1094
|
-
}
|
|
1095
|
-
} else if (rel.type === "reference") {
|
|
1096
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1097
|
-
if (fkValues.length === 0) {
|
|
1098
|
-
for (const row of rows) row[fieldName] = null;
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
const unique = [...new Set(fkValues)];
|
|
1102
|
-
const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1103
|
-
if (uncached.length > 0) {
|
|
1104
|
-
const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1105
|
-
const result = await db.execute(
|
|
1106
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})`
|
|
1107
|
-
);
|
|
1108
|
-
for (const item of result.rows) {
|
|
1109
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
for (const row of rows) {
|
|
1113
|
-
const fkVal = row[rel.fk_column];
|
|
1114
|
-
row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
|
|
1115
|
-
}
|
|
1116
|
-
} else if (rel.type === "junction") {
|
|
1117
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1118
|
-
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1119
|
-
const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
|
|
1120
|
-
const junctionResult = await db.execute(
|
|
1121
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
|
|
1122
|
-
);
|
|
1123
|
-
const jRows = junctionResult.rows;
|
|
1124
|
-
const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
|
|
1125
|
-
const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
|
|
1126
|
-
if (uncached.length > 0) {
|
|
1127
|
-
const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1128
|
-
const entitiesResult = await db.execute(
|
|
1129
|
-
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})`
|
|
1130
|
-
);
|
|
1131
|
-
for (const item of entitiesResult.rows) {
|
|
1132
|
-
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
const byLeft = groupBy(jRows, rel.left_column);
|
|
1136
|
-
for (const row of rows) {
|
|
1137
|
-
const jEntries = byLeft[row["id"]] ?? [];
|
|
1138
|
-
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
1139
|
-
}
|
|
1140
|
-
} else if (rel.type === "media") {
|
|
1141
|
-
const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
1142
|
-
if (fkValues.length === 0) {
|
|
1143
|
-
for (const row of rows) row[fieldName] = null;
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
const unique = [...new Set(fkValues)];
|
|
1147
|
-
const uncached = unique.filter((id) => !cache.has(`media:${id}`));
|
|
1148
|
-
if (uncached.length > 0) {
|
|
1149
|
-
const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1150
|
-
const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
|
|
1151
|
-
for (const item of result.rows) {
|
|
1152
|
-
cache.set(`media:${item["id"]}`, item);
|
|
1153
|
-
}
|
|
1838
|
+
ok: false,
|
|
1839
|
+
error: {
|
|
1840
|
+
code: "VALIDATION_ERROR",
|
|
1841
|
+
message: `type must be one of: image, video, file`
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
422
|
|
1845
|
+
);
|
|
1154
1846
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1847
|
+
const opts = {
|
|
1848
|
+
page: pagination.page,
|
|
1849
|
+
per_page: pagination.per_page
|
|
1850
|
+
};
|
|
1851
|
+
if (typeParam !== void 0) {
|
|
1852
|
+
opts.type = typeParam;
|
|
1158
1853
|
}
|
|
1854
|
+
const result = await mediaRepo.findMany(opts);
|
|
1855
|
+
return c.json(result);
|
|
1856
|
+
};
|
|
1857
|
+
if (listRateLimit) {
|
|
1858
|
+
app.get("/api/media", listRateLimit, mediaListHandler);
|
|
1859
|
+
} else {
|
|
1860
|
+
app.get("/api/media", mediaListHandler);
|
|
1159
1861
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
);
|
|
1169
|
-
const byParent = groupBy(result.rows, "parent_id");
|
|
1170
|
-
for (const row of rows) {
|
|
1171
|
-
row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
|
|
1172
|
-
}
|
|
1173
|
-
} else if (rel.type === "junction") {
|
|
1174
|
-
const parentIds = rows.map((r) => r["id"]);
|
|
1175
|
-
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
1176
|
-
const result = await db.execute(
|
|
1177
|
-
import_drizzle_orm2.sql`SELECT ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} AS left_id, ${import_drizzle_orm2.sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
|
|
1178
|
-
);
|
|
1179
|
-
const byLeft = groupBy(result.rows, "left_id");
|
|
1180
|
-
for (const row of rows) {
|
|
1181
|
-
row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
|
|
1862
|
+
app.get("/api/media/:id", async (c) => {
|
|
1863
|
+
const id = c.req.param("id");
|
|
1864
|
+
const item = await mediaRepo.findOne(id);
|
|
1865
|
+
if (!item) {
|
|
1866
|
+
return c.json(
|
|
1867
|
+
{ ok: false, error: { code: "NOT_FOUND", message: "Media item not found" } },
|
|
1868
|
+
404
|
|
1869
|
+
);
|
|
1182
1870
|
}
|
|
1871
|
+
return c.json({ ok: true, data: item });
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// src/routes/admin/content.ts
|
|
1876
|
+
var import_drizzle_orm3 = require("drizzle-orm");
|
|
1877
|
+
|
|
1878
|
+
// src/media-references.ts
|
|
1879
|
+
function extractMediaId(value) {
|
|
1880
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
1881
|
+
}
|
|
1882
|
+
function topLevelMediaDelta(mediaFields, before, after) {
|
|
1883
|
+
const added = [];
|
|
1884
|
+
const removed = [];
|
|
1885
|
+
for (const f of mediaFields) {
|
|
1886
|
+
if (after !== null && !(f.name in after)) continue;
|
|
1887
|
+
const oldId = before ? extractMediaId(before[f.name]) : null;
|
|
1888
|
+
const newId = after ? extractMediaId(after[f.name]) : null;
|
|
1889
|
+
if (newId === oldId) continue;
|
|
1890
|
+
if (oldId) removed.push(oldId);
|
|
1891
|
+
if (newId) added.push(newId);
|
|
1183
1892
|
}
|
|
1893
|
+
return { added, removed };
|
|
1894
|
+
}
|
|
1895
|
+
function mergeMediaDeltas(...deltas) {
|
|
1896
|
+
return {
|
|
1897
|
+
added: deltas.flatMap((d) => d.added),
|
|
1898
|
+
removed: deltas.flatMap((d) => d.removed)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
1902
|
+
const added = new Set(delta.added);
|
|
1903
|
+
const removed = new Set(delta.removed);
|
|
1904
|
+
const netAdd = [...added].filter((id) => !removed.has(id));
|
|
1905
|
+
const netRemove = [...removed].filter((id) => !added.has(id));
|
|
1906
|
+
if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
|
|
1907
|
+
if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
|
|
1184
1908
|
}
|
|
1185
1909
|
|
|
1186
1910
|
// src/routes/admin/content.ts
|
|
1911
|
+
init_relations();
|
|
1187
1912
|
function quoteIdent2(name) {
|
|
1188
1913
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1189
1914
|
return `"${name}"`;
|
|
1190
1915
|
}
|
|
1916
|
+
async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
|
|
1917
|
+
const result = await db.execute(
|
|
1918
|
+
import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
1919
|
+
);
|
|
1920
|
+
const rows = result.rows;
|
|
1921
|
+
for (const nf of pType.fields) {
|
|
1922
|
+
if (nf.db_column !== null) continue;
|
|
1923
|
+
const comp = nf.ui_component;
|
|
1924
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1925
|
+
const nType = registry.paragraph_types[comp.ref];
|
|
1926
|
+
if (!nType) continue;
|
|
1927
|
+
for (const row of rows) {
|
|
1928
|
+
row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return rows;
|
|
1932
|
+
}
|
|
1191
1933
|
async function lookupBasePathId(db, pathOrName) {
|
|
1192
1934
|
const r = await db.execute(
|
|
1193
1935
|
import_drizzle_orm3.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
|
|
@@ -1339,10 +2081,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1339
2081
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1340
2082
|
const pType = registry.paragraph_types[comp.ref];
|
|
1341
2083
|
if (!pType) continue;
|
|
1342
|
-
|
|
1343
|
-
import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
|
|
1344
|
-
);
|
|
1345
|
-
row[f.name] = r.rows;
|
|
2084
|
+
row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
|
|
1346
2085
|
} else if (f.db_column.junction) {
|
|
1347
2086
|
const j = f.db_column.junction;
|
|
1348
2087
|
const r = await db.execute(
|
|
@@ -1417,82 +2156,85 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1417
2156
|
);
|
|
1418
2157
|
}
|
|
1419
2158
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
2159
|
+
return writeNewItem(c, body);
|
|
2160
|
+
}
|
|
2161
|
+
);
|
|
2162
|
+
const writeNewItem = async (c, body) => {
|
|
2163
|
+
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2164
|
+
if (fieldErrors.length > 0) {
|
|
2165
|
+
return c.json(
|
|
2166
|
+
{
|
|
2167
|
+
ok: false,
|
|
2168
|
+
error: {
|
|
2169
|
+
code: "VALIDATION_ERROR",
|
|
2170
|
+
message: "Required fields are missing",
|
|
2171
|
+
details: fieldErrors
|
|
2172
|
+
}
|
|
2173
|
+
},
|
|
2174
|
+
422
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
if (body["published"] === true) {
|
|
2178
|
+
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2179
|
+
});
|
|
2180
|
+
if (publishDeny) return publishDeny;
|
|
2181
|
+
}
|
|
2182
|
+
const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
2183
|
+
const junctionFields = contentType.fields.filter(
|
|
2184
|
+
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2185
|
+
);
|
|
2186
|
+
const columnFields = contentType.fields.filter(
|
|
2187
|
+
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2188
|
+
);
|
|
2189
|
+
const columnData = {
|
|
2190
|
+
published: body["published"] ?? false
|
|
2191
|
+
};
|
|
2192
|
+
if (contentType.only_one) {
|
|
2193
|
+
columnData["slug"] = typeName;
|
|
2194
|
+
} else {
|
|
2195
|
+
columnData["slug"] = body["slug"];
|
|
2196
|
+
}
|
|
2197
|
+
for (const f of columnFields) {
|
|
2198
|
+
const val = body[f.name];
|
|
2199
|
+
columnData[f.db_column.column_name] = val !== void 0 ? val : null;
|
|
2200
|
+
}
|
|
2201
|
+
if (db) {
|
|
2202
|
+
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
2203
|
+
if (!basePathId) {
|
|
1422
2204
|
return c.json(
|
|
1423
2205
|
{
|
|
1424
2206
|
ok: false,
|
|
1425
2207
|
error: {
|
|
1426
|
-
code: "
|
|
1427
|
-
message:
|
|
1428
|
-
details: fieldErrors
|
|
2208
|
+
code: "BASE_PATH_NOT_FOUND",
|
|
2209
|
+
message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
|
|
1429
2210
|
}
|
|
1430
2211
|
},
|
|
1431
|
-
|
|
2212
|
+
500
|
|
1432
2213
|
);
|
|
1433
2214
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
(
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
published: body["published"] ?? false
|
|
1448
|
-
};
|
|
1449
|
-
if (contentType.only_one) {
|
|
1450
|
-
columnData["slug"] = typeName;
|
|
1451
|
-
} else {
|
|
1452
|
-
columnData["slug"] = body["slug"];
|
|
1453
|
-
}
|
|
1454
|
-
for (const f of columnFields) {
|
|
1455
|
-
const val = body[f.name];
|
|
1456
|
-
columnData[f.db_column.column_name] = val !== void 0 ? val : null;
|
|
1457
|
-
}
|
|
1458
|
-
if (db) {
|
|
1459
|
-
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
1460
|
-
if (!basePathId) {
|
|
1461
|
-
return c.json(
|
|
1462
|
-
{
|
|
1463
|
-
ok: false,
|
|
1464
|
-
error: {
|
|
1465
|
-
code: "BASE_PATH_NOT_FOUND",
|
|
1466
|
-
message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
|
|
1467
|
-
}
|
|
1468
|
-
},
|
|
1469
|
-
500
|
|
1470
|
-
);
|
|
1471
|
-
}
|
|
1472
|
-
columnData["base_path_id"] = basePathId;
|
|
2215
|
+
columnData["base_path_id"] = basePathId;
|
|
2216
|
+
}
|
|
2217
|
+
const item = await repo.create(columnData);
|
|
2218
|
+
const itemId = item["id"];
|
|
2219
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
|
|
2220
|
+
if (db) {
|
|
2221
|
+
for (const f of paragraphFields) {
|
|
2222
|
+
const comp = f.ui_component;
|
|
2223
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
2224
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
2225
|
+
if (!pType) continue;
|
|
2226
|
+
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
2227
|
+
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
|
|
1473
2228
|
}
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
for (const f of paragraphFields) {
|
|
1479
|
-
const comp = f.ui_component;
|
|
1480
|
-
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1481
|
-
const pType = registry.paragraph_types[comp.ref];
|
|
1482
|
-
if (!pType) continue;
|
|
1483
|
-
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1484
|
-
mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items));
|
|
1485
|
-
}
|
|
1486
|
-
for (const f of junctionFields) {
|
|
1487
|
-
const junction = f.db_column.junction;
|
|
1488
|
-
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
1489
|
-
await persistJunctionField(db, itemId, junction, relatedIds);
|
|
1490
|
-
}
|
|
2229
|
+
for (const f of junctionFields) {
|
|
2230
|
+
const junction = f.db_column.junction;
|
|
2231
|
+
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
2232
|
+
await persistJunctionField(db, itemId, junction, relatedIds);
|
|
1491
2233
|
}
|
|
1492
|
-
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
1493
|
-
return c.json({ ok: true, data: item }, 201);
|
|
1494
2234
|
}
|
|
1495
|
-
|
|
2235
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2236
|
+
return c.json({ ok: true, data: item }, 201);
|
|
2237
|
+
};
|
|
1496
2238
|
app.patch(
|
|
1497
2239
|
`/admin/api/${basePath}/:id`,
|
|
1498
2240
|
requirePermission("content:edit"),
|
|
@@ -1547,71 +2289,92 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1547
2289
|
);
|
|
1548
2290
|
}
|
|
1549
2291
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2292
|
+
return writeExistingItem(c, id, existing, body);
|
|
2293
|
+
}
|
|
2294
|
+
);
|
|
2295
|
+
const writeExistingItem = async (c, id, existing, body) => {
|
|
2296
|
+
if (body["published"] === true) {
|
|
2297
|
+
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2298
|
+
});
|
|
2299
|
+
if (publishDeny) return publishDeny;
|
|
2300
|
+
const merged = { ...existing, ...body };
|
|
2301
|
+
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
2302
|
+
if (fieldErrors.length > 0) {
|
|
2303
|
+
return c.json(
|
|
2304
|
+
{
|
|
2305
|
+
ok: false,
|
|
2306
|
+
error: {
|
|
2307
|
+
code: "PUBLISH_VALIDATION_ERROR",
|
|
2308
|
+
message: "Cannot publish \u2014 required fields are missing",
|
|
2309
|
+
details: fieldErrors
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
422
|
|
2313
|
+
);
|
|
1569
2314
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
)
|
|
1574
|
-
|
|
1575
|
-
|
|
2315
|
+
}
|
|
2316
|
+
const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
|
|
2317
|
+
const patchJunctionFields = contentType.fields.filter(
|
|
2318
|
+
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2319
|
+
);
|
|
2320
|
+
const patchColumnFields = contentType.fields.filter(
|
|
2321
|
+
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2322
|
+
);
|
|
2323
|
+
const patchData = {};
|
|
2324
|
+
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
2325
|
+
if ("published" in body) patchData["published"] = body["published"];
|
|
2326
|
+
for (const f of patchColumnFields) {
|
|
2327
|
+
if (f.name in body) {
|
|
2328
|
+
const val = body[f.name];
|
|
2329
|
+
patchData[f.db_column.column_name] = val !== void 0 ? val : null;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const updated = await repo.update(id, patchData);
|
|
2333
|
+
if (!updated) {
|
|
2334
|
+
return c.json(
|
|
2335
|
+
{ ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
|
|
2336
|
+
404
|
|
1576
2337
|
);
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
2338
|
+
}
|
|
2339
|
+
const mediaDeltas = [
|
|
2340
|
+
topLevelMediaDelta(mediaFields, existing, body)
|
|
2341
|
+
];
|
|
2342
|
+
if (db) {
|
|
2343
|
+
for (const f of patchParagraphFields) {
|
|
2344
|
+
const comp = f.ui_component;
|
|
2345
|
+
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
2346
|
+
const pType = registry.paragraph_types[comp.ref];
|
|
2347
|
+
if (!pType) continue;
|
|
2348
|
+
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
2349
|
+
mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
|
|
1585
2350
|
}
|
|
1586
|
-
const
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
404
|
|
1591
|
-
);
|
|
2351
|
+
for (const f of patchJunctionFields) {
|
|
2352
|
+
const junction = f.db_column.junction;
|
|
2353
|
+
const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
|
|
2354
|
+
await persistJunctionField(db, id, junction, relatedIds);
|
|
1592
2355
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
2356
|
+
}
|
|
2357
|
+
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2358
|
+
return c.json({ ok: true, data: updated });
|
|
2359
|
+
};
|
|
2360
|
+
if (contentType.only_one) {
|
|
2361
|
+
app.put(
|
|
2362
|
+
`/admin/api/${basePath}`,
|
|
2363
|
+
requirePermission("content:edit"),
|
|
2364
|
+
async (c) => {
|
|
2365
|
+
const body = await c.req.json();
|
|
2366
|
+
const existingResult = await repo.findMany({ page: 1, per_page: 1 });
|
|
2367
|
+
const existing = existingResult.data[0];
|
|
2368
|
+
if (!existing) {
|
|
2369
|
+
const createDeny = await requirePermission("content:create")(c, async () => {
|
|
2370
|
+
});
|
|
2371
|
+
if (createDeny) return createDeny;
|
|
2372
|
+
return writeNewItem(c, body);
|
|
1609
2373
|
}
|
|
2374
|
+
return writeExistingItem(c, existing["id"], existing, body);
|
|
1610
2375
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
}
|
|
1614
|
-
);
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
1615
2378
|
app.delete(
|
|
1616
2379
|
`/admin/api/${basePath}/:id`,
|
|
1617
2380
|
requirePermission("content:delete"),
|
|
@@ -1633,7 +2396,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1633
2396
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1634
2397
|
const pType = registry.paragraph_types[comp.ref];
|
|
1635
2398
|
if (!pType) continue;
|
|
1636
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
|
|
2399
|
+
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
1637
2400
|
}
|
|
1638
2401
|
}
|
|
1639
2402
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1748,7 +2511,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1748
2511
|
const pType = registry.paragraph_types[comp.ref];
|
|
1749
2512
|
if (!pType) continue;
|
|
1750
2513
|
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1751
|
-
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items));
|
|
2514
|
+
mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1752
2515
|
}
|
|
1753
2516
|
}
|
|
1754
2517
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1817,7 +2580,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1817
2580
|
const pType = registry.paragraph_types[comp.ref];
|
|
1818
2581
|
if (!pType) continue;
|
|
1819
2582
|
const items = Array.isArray(body[f.name]) ? body[f.name] : [];
|
|
1820
|
-
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items));
|
|
2583
|
+
mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
|
|
1821
2584
|
}
|
|
1822
2585
|
}
|
|
1823
2586
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -1845,7 +2608,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
1845
2608
|
if (comp.component !== "paragraph-embed" || !comp.ref) continue;
|
|
1846
2609
|
const pType = registry.paragraph_types[comp.ref];
|
|
1847
2610
|
if (!pType) continue;
|
|
1848
|
-
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
|
|
2611
|
+
mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
|
|
1849
2612
|
}
|
|
1850
2613
|
}
|
|
1851
2614
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
@@ -2813,7 +3576,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2813
3576
|
var import_drizzle_orm6 = require("drizzle-orm");
|
|
2814
3577
|
var API_VERSION = (() => {
|
|
2815
3578
|
try {
|
|
2816
|
-
return "0.2
|
|
3579
|
+
return "0.3.2";
|
|
2817
3580
|
} catch {
|
|
2818
3581
|
return "0.0.0";
|
|
2819
3582
|
}
|
|
@@ -2920,6 +3683,7 @@ function registerSchemaRoute(app, registry, db) {
|
|
|
2920
3683
|
// src/repositories/content.ts
|
|
2921
3684
|
var import_node_crypto3 = __toESM(require("crypto"));
|
|
2922
3685
|
var import_drizzle_orm8 = require("drizzle-orm");
|
|
3686
|
+
init_relations();
|
|
2923
3687
|
var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
2924
3688
|
function codeError(code, message) {
|
|
2925
3689
|
const err = new Error(message);
|
|
@@ -2975,7 +3739,7 @@ function whereFragment(conds) {
|
|
|
2975
3739
|
return conds.length > 0 ? import_drizzle_orm8.sql` WHERE ${import_drizzle_orm8.sql.join(conds, import_drizzle_orm8.sql` AND `)}` : import_drizzle_orm8.sql``;
|
|
2976
3740
|
}
|
|
2977
3741
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
2978
|
-
const { relations = {} } = options;
|
|
3742
|
+
const { relations = {}, publishedRelations = false } = options;
|
|
2979
3743
|
function tableRaw() {
|
|
2980
3744
|
return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
|
|
2981
3745
|
}
|
|
@@ -2988,7 +3752,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2988
3752
|
}
|
|
2989
3753
|
for (const [fieldName, rel] of Object.entries(relations)) {
|
|
2990
3754
|
if (rel.type === "media") {
|
|
2991
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
3755
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
2992
3756
|
} else if (!include.includes(fieldName)) {
|
|
2993
3757
|
await resolveRelationBareIds(db, rows, fieldName, rel);
|
|
2994
3758
|
}
|
|
@@ -2996,7 +3760,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
2996
3760
|
for (const fieldName of include) {
|
|
2997
3761
|
const rel = relations[fieldName];
|
|
2998
3762
|
if (rel.type !== "media") {
|
|
2999
|
-
await resolveRelationField(db, rows, fieldName, rel, cache);
|
|
3763
|
+
await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
|
|
3000
3764
|
}
|
|
3001
3765
|
}
|
|
3002
3766
|
return rows;
|
|
@@ -3117,6 +3881,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3117
3881
|
};
|
|
3118
3882
|
}
|
|
3119
3883
|
|
|
3884
|
+
// src/app.ts
|
|
3885
|
+
init_relations();
|
|
3886
|
+
|
|
3120
3887
|
// src/repositories/media.ts
|
|
3121
3888
|
var import_node_crypto4 = __toESM(require("crypto"));
|
|
3122
3889
|
var import_drizzle_orm9 = require("drizzle-orm");
|
|
@@ -3234,7 +4001,14 @@ function createCmsApp(options) {
|
|
|
3234
4001
|
const programmaticResolver = createProgrammaticResolver(resolverMap);
|
|
3235
4002
|
const app = new import_hono.Hono();
|
|
3236
4003
|
const uploadOrigins = storage.getUploadOrigins?.() ?? [];
|
|
3237
|
-
|
|
4004
|
+
const graphiqlEnabled = options.graphql?.enabled === true && options.graphql.graphiql === true;
|
|
4005
|
+
app.use(
|
|
4006
|
+
"*",
|
|
4007
|
+
createSecurityHeadersMiddleware({
|
|
4008
|
+
connectSrc: uploadOrigins,
|
|
4009
|
+
...graphiqlEnabled && { graphiqlPath: "/graphql" }
|
|
4010
|
+
})
|
|
4011
|
+
);
|
|
3238
4012
|
app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
|
|
3239
4013
|
app.onError(errorHandler);
|
|
3240
4014
|
const listRateLimit = resolveListRateLimit(rateLimit);
|
|
@@ -3269,7 +4043,8 @@ function createCmsApp(options) {
|
|
|
3269
4043
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
3270
4044
|
typeName,
|
|
3271
4045
|
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
3272
|
-
relations: buildRelationsMap(ct.fields, registry)
|
|
4046
|
+
relations: buildRelationsMap(ct.fields, registry),
|
|
4047
|
+
publishedRelations: true
|
|
3273
4048
|
})
|
|
3274
4049
|
])
|
|
3275
4050
|
);
|
|
@@ -3277,11 +4052,22 @@ function createCmsApp(options) {
|
|
|
3277
4052
|
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
3278
4053
|
typeName,
|
|
3279
4054
|
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
3280
|
-
relations: buildRelationsMap(tt.fields, registry)
|
|
4055
|
+
relations: buildRelationsMap(tt.fields, registry),
|
|
4056
|
+
publishedRelations: true
|
|
3281
4057
|
})
|
|
3282
4058
|
])
|
|
3283
4059
|
);
|
|
3284
4060
|
const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
|
|
4061
|
+
const graphqlRepos = Object.fromEntries([
|
|
4062
|
+
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4063
|
+
typeName,
|
|
4064
|
+
createDrizzleContentRepository(db, ct.db.table_name)
|
|
4065
|
+
]),
|
|
4066
|
+
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4067
|
+
typeName,
|
|
4068
|
+
createDrizzleContentRepository(db, tt.db.table_name)
|
|
4069
|
+
])
|
|
4070
|
+
]);
|
|
3285
4071
|
registerAuthRoutes(app, db);
|
|
3286
4072
|
app.use("/admin/api/*", authMiddleware);
|
|
3287
4073
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
@@ -3318,6 +4104,35 @@ function createCmsApp(options) {
|
|
|
3318
4104
|
);
|
|
3319
4105
|
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
|
|
3320
4106
|
registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
|
|
4107
|
+
if (options.graphql?.enabled) {
|
|
4108
|
+
const gqlOptions = options.graphql;
|
|
4109
|
+
let gqlHandler = null;
|
|
4110
|
+
let initError = null;
|
|
4111
|
+
const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
|
|
4112
|
+
gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
|
|
4113
|
+
}).catch((err) => {
|
|
4114
|
+
initError = err;
|
|
4115
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4116
|
+
process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
|
|
4117
|
+
`);
|
|
4118
|
+
});
|
|
4119
|
+
const invokeGraphQL = async (c) => {
|
|
4120
|
+
if (!gqlHandler && !initError) await ready;
|
|
4121
|
+
if (!gqlHandler) {
|
|
4122
|
+
return c.json(
|
|
4123
|
+
{ ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
|
|
4124
|
+
500
|
|
4125
|
+
);
|
|
4126
|
+
}
|
|
4127
|
+
return gqlHandler(c, async () => {
|
|
4128
|
+
});
|
|
4129
|
+
};
|
|
4130
|
+
if (listRateLimit) {
|
|
4131
|
+
app.all("/graphql", listRateLimit, invokeGraphQL);
|
|
4132
|
+
} else {
|
|
4133
|
+
app.all("/graphql", invokeGraphQL);
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
3321
4136
|
registerConfigRoute(
|
|
3322
4137
|
app,
|
|
3323
4138
|
{
|
|
@@ -3364,13 +4179,24 @@ function createServer(options = {}) {
|
|
|
3364
4179
|
}
|
|
3365
4180
|
|
|
3366
4181
|
// src/index.ts
|
|
4182
|
+
function resolveGraphQL(cfg) {
|
|
4183
|
+
const devDefault = process.env["NODE_ENV"] !== "production";
|
|
4184
|
+
return {
|
|
4185
|
+
enabled: cfg.enabled ?? false,
|
|
4186
|
+
maxDepth: cfg.maxDepth ?? 8,
|
|
4187
|
+
maxComplexity: cfg.maxComplexity ?? 1e3,
|
|
4188
|
+
graphiql: cfg.graphiql ?? devDefault,
|
|
4189
|
+
introspection: cfg.introspection ?? cfg.graphiql ?? devDefault
|
|
4190
|
+
};
|
|
4191
|
+
}
|
|
3367
4192
|
function createAPIAdapter(options = {}) {
|
|
3368
4193
|
const prefix = options.prefix ?? "/api";
|
|
3369
4194
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
3370
4195
|
return {
|
|
3371
4196
|
prefix,
|
|
3372
4197
|
...media !== void 0 && { media },
|
|
3373
|
-
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
|
|
4198
|
+
...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
|
|
4199
|
+
...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
|
|
3374
4200
|
};
|
|
3375
4201
|
}
|
|
3376
4202
|
// Annotate the CommonJS export names for ESM import in node:
|