@bobbykim/manguito-cms-api 0.1.1 → 0.2.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/index.d.mts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +170 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +166 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -425,6 +425,140 @@ function buildRolesRegistry(roles) {
|
|
|
425
425
|
return Object.fromEntries(roles.map((r) => [r.name, r]));
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
// src/programmatic/resolve.ts
|
|
429
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
430
|
+
var LIST_CONCURRENCY = 10;
|
|
431
|
+
function resolverKey(schema, field) {
|
|
432
|
+
return `${schema}::${field}`;
|
|
433
|
+
}
|
|
434
|
+
function validateResolverBindings(registry, resolvers) {
|
|
435
|
+
const declared = /* @__PURE__ */ new Set();
|
|
436
|
+
const schemas = [
|
|
437
|
+
...Object.values(registry.content_types),
|
|
438
|
+
...Object.values(registry.taxonomy_types)
|
|
439
|
+
];
|
|
440
|
+
for (const schema of schemas) {
|
|
441
|
+
for (const field of schema.fields) {
|
|
442
|
+
if (field.field_type === "programmatic") {
|
|
443
|
+
declared.add(resolverKey(schema.name, field.name));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const missing = [];
|
|
448
|
+
for (const key of declared) if (!resolvers.has(key)) missing.push(key);
|
|
449
|
+
const orphans = [];
|
|
450
|
+
for (const key of resolvers.keys()) if (!declared.has(key)) orphans.push(key);
|
|
451
|
+
if (missing.length === 0 && orphans.length === 0) return;
|
|
452
|
+
const lines = ["\u2717 Programmatic field resolver bindings are invalid."];
|
|
453
|
+
if (missing.length > 0) {
|
|
454
|
+
lines.push("", ' Declared as `type: "programmatic"` but no resolver found:');
|
|
455
|
+
for (const k of missing) lines.push(` - ${k} (add a programmaticField in src/programmatic)`);
|
|
456
|
+
}
|
|
457
|
+
if (orphans.length > 0) {
|
|
458
|
+
lines.push("", " Resolver has no matching programmatic field in the schema:");
|
|
459
|
+
for (const k of orphans) lines.push(` - ${k}`);
|
|
460
|
+
}
|
|
461
|
+
lines.push("", "Exiting.");
|
|
462
|
+
throw new Error(lines.join("\n"));
|
|
463
|
+
}
|
|
464
|
+
function buildContext(row) {
|
|
465
|
+
return {
|
|
466
|
+
get: (name) => row[name],
|
|
467
|
+
record: row
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async function withTimeout(p, ms) {
|
|
471
|
+
let timer;
|
|
472
|
+
const timeout = new Promise((_, reject) => {
|
|
473
|
+
timer = setTimeout(() => reject(new Error("resolver timeout")), ms);
|
|
474
|
+
});
|
|
475
|
+
try {
|
|
476
|
+
return await Promise.race([p, timeout]);
|
|
477
|
+
} finally {
|
|
478
|
+
clearTimeout(timer);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async function runOne(def, row, onError) {
|
|
482
|
+
const fallback = def.fallback ?? null;
|
|
483
|
+
try {
|
|
484
|
+
const result = await withTimeout(
|
|
485
|
+
Promise.resolve(def.resolve(buildContext(row))),
|
|
486
|
+
def.timeout ?? DEFAULT_TIMEOUT_MS
|
|
487
|
+
);
|
|
488
|
+
return { value: result === void 0 ? null : result, failed: false };
|
|
489
|
+
} catch (err) {
|
|
490
|
+
onError(
|
|
491
|
+
`\u26A0 programmatic field ${resolverKey(def.schema, def.field)} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
492
|
+
);
|
|
493
|
+
return { value: fallback, failed: true };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function runPool(items, limit, fn) {
|
|
497
|
+
let i = 0;
|
|
498
|
+
async function worker() {
|
|
499
|
+
while (i < items.length) {
|
|
500
|
+
const idx = i++;
|
|
501
|
+
await fn(items[idx]);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
|
|
505
|
+
await Promise.all(workers);
|
|
506
|
+
}
|
|
507
|
+
function createProgrammaticResolver(resolvers, options) {
|
|
508
|
+
const onError = options?.onError ?? ((message) => {
|
|
509
|
+
process.stderr.write(message + "\n");
|
|
510
|
+
});
|
|
511
|
+
const bySchema = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const def of resolvers.values()) {
|
|
513
|
+
const arr = bySchema.get(def.schema) ?? [];
|
|
514
|
+
arr.push(def);
|
|
515
|
+
bySchema.set(def.schema, arr);
|
|
516
|
+
}
|
|
517
|
+
const cache = /* @__PURE__ */ new Map();
|
|
518
|
+
async function resolveField(def, row) {
|
|
519
|
+
const itemId = row["id"] !== void 0 ? String(row["id"]) : "";
|
|
520
|
+
if (def.cache && itemId) {
|
|
521
|
+
const key = `${resolverKey(def.schema, def.field)}::${itemId}`;
|
|
522
|
+
const now = Date.now();
|
|
523
|
+
const hit = cache.get(key);
|
|
524
|
+
if (hit && hit.expires > now) return hit.value;
|
|
525
|
+
const { value: value2, failed } = await runOne(def, row, onError);
|
|
526
|
+
if (!failed) cache.set(key, { value: value2, expires: now + def.cache.ttl * 1e3 });
|
|
527
|
+
return value2;
|
|
528
|
+
}
|
|
529
|
+
const { value } = await runOne(def, row, onError);
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
async function resolveItem(schema, row) {
|
|
533
|
+
const defs = bySchema.get(schema);
|
|
534
|
+
if (!defs || defs.length === 0) return { ...row };
|
|
535
|
+
const out = { ...row };
|
|
536
|
+
await Promise.all(
|
|
537
|
+
defs.map(async (def) => {
|
|
538
|
+
out[def.field] = await resolveField(def, row);
|
|
539
|
+
})
|
|
540
|
+
);
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
async function resolveList(schema, rows) {
|
|
544
|
+
const defs = (bySchema.get(schema) ?? []).filter((d) => d.on_list === true);
|
|
545
|
+
if (defs.length === 0) return rows.map((r) => ({ ...r }));
|
|
546
|
+
const out = rows.map((r) => ({ ...r }));
|
|
547
|
+
const tasks = [];
|
|
548
|
+
for (let i = 0; i < rows.length; i++) {
|
|
549
|
+
for (const def of defs) tasks.push({ rowIndex: i, def });
|
|
550
|
+
}
|
|
551
|
+
await runPool(tasks, LIST_CONCURRENCY, async ({ rowIndex, def }) => {
|
|
552
|
+
out[rowIndex][def.field] = await resolveField(def, rows[rowIndex]);
|
|
553
|
+
});
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
function hasSchema(schema) {
|
|
557
|
+
return bySchema.has(schema);
|
|
558
|
+
}
|
|
559
|
+
return { resolveItem, resolveList, hasSchema };
|
|
560
|
+
}
|
|
561
|
+
|
|
428
562
|
// src/routes/query-params.ts
|
|
429
563
|
var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
430
564
|
var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -476,7 +610,7 @@ function parseFilters(url, validFields) {
|
|
|
476
610
|
function isPublished(item) {
|
|
477
611
|
return item["published"] === true;
|
|
478
612
|
}
|
|
479
|
-
function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
613
|
+
function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
|
|
480
614
|
function registerListRoute(path, handler) {
|
|
481
615
|
if (listRateLimit) {
|
|
482
616
|
app.get(path, listRateLimit, handler);
|
|
@@ -503,13 +637,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
503
637
|
const basePath = contentType.default_base_path;
|
|
504
638
|
const repo = repos[typeName];
|
|
505
639
|
if (!repo) continue;
|
|
506
|
-
const schemaFieldNames = /* @__PURE__ */ new Set([
|
|
507
|
-
...contentType.fields.map((f) => f.name),
|
|
508
|
-
...contentType.system_fields.map((f) => f.name)
|
|
509
|
-
]);
|
|
510
640
|
const relationFieldNames = new Set(
|
|
511
641
|
contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
512
642
|
);
|
|
643
|
+
const filterableFieldNames = /* @__PURE__ */ new Set([
|
|
644
|
+
...contentType.fields.filter((f) => f.field_type !== "programmatic").map((f) => f.name),
|
|
645
|
+
...contentType.system_fields.map((f) => f.name)
|
|
646
|
+
]);
|
|
513
647
|
if (contentType.only_one) {
|
|
514
648
|
app.get(`/api/${basePath}`, async (c) => {
|
|
515
649
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
@@ -519,7 +653,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
519
653
|
404
|
|
520
654
|
);
|
|
521
655
|
}
|
|
522
|
-
|
|
656
|
+
let data = result.data[0];
|
|
657
|
+
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
658
|
+
return c.json({ ok: true, data });
|
|
523
659
|
});
|
|
524
660
|
} else {
|
|
525
661
|
registerListRoute(`/api/${basePath}`, async (c) => {
|
|
@@ -562,7 +698,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
562
698
|
400
|
|
563
699
|
);
|
|
564
700
|
}
|
|
565
|
-
const filtersResult = parseFilters(c.req.url,
|
|
701
|
+
const filtersResult = parseFilters(c.req.url, filterableFieldNames);
|
|
566
702
|
if (!filtersResult.ok) {
|
|
567
703
|
return c.json(
|
|
568
704
|
{
|
|
@@ -599,6 +735,10 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
599
735
|
filters: filtersResult.filters,
|
|
600
736
|
include
|
|
601
737
|
});
|
|
738
|
+
if (resolver?.hasSchema(typeName)) {
|
|
739
|
+
const resolved = await resolver.resolveList(typeName, result.data);
|
|
740
|
+
return c.json({ ...result, data: resolved });
|
|
741
|
+
}
|
|
602
742
|
return c.json(result);
|
|
603
743
|
});
|
|
604
744
|
app.get(`/api/${basePath}/:slug`, async (c) => {
|
|
@@ -628,7 +768,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
628
768
|
404
|
|
629
769
|
);
|
|
630
770
|
}
|
|
631
|
-
|
|
771
|
+
let data = item;
|
|
772
|
+
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
773
|
+
return c.json({ ok: true, data });
|
|
632
774
|
});
|
|
633
775
|
}
|
|
634
776
|
}
|
|
@@ -654,6 +796,10 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
654
796
|
page: pagination.page,
|
|
655
797
|
per_page: pagination.per_page
|
|
656
798
|
});
|
|
799
|
+
if (resolver?.hasSchema(typeName)) {
|
|
800
|
+
const resolved = await resolver.resolveList(typeName, result.data);
|
|
801
|
+
return c.json({ ...result, data: resolved });
|
|
802
|
+
}
|
|
657
803
|
return c.json(result);
|
|
658
804
|
});
|
|
659
805
|
app.get(`/api/taxonomy/${typeName}/:id`, async (c) => {
|
|
@@ -668,7 +814,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
|
|
|
668
814
|
404
|
|
669
815
|
);
|
|
670
816
|
}
|
|
671
|
-
|
|
817
|
+
let data = item;
|
|
818
|
+
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
819
|
+
return c.json({ ok: true, data });
|
|
672
820
|
});
|
|
673
821
|
}
|
|
674
822
|
}
|
|
@@ -2624,7 +2772,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
2624
2772
|
import { sql as sql6 } from "drizzle-orm";
|
|
2625
2773
|
var API_VERSION = (() => {
|
|
2626
2774
|
try {
|
|
2627
|
-
return "0.
|
|
2775
|
+
return "0.2.0";
|
|
2628
2776
|
} catch {
|
|
2629
2777
|
return "0.0.0";
|
|
2630
2778
|
}
|
|
@@ -3040,6 +3188,9 @@ function createCmsApp(options) {
|
|
|
3040
3188
|
const cmsName = options.name ?? "Manguito CMS";
|
|
3041
3189
|
const maxFileSize = media?.max_file_size;
|
|
3042
3190
|
const rolesRegistry = buildRolesRegistry(registry.roles.roles);
|
|
3191
|
+
const resolverMap = options.resolvers ?? /* @__PURE__ */ new Map();
|
|
3192
|
+
validateResolverBindings(registry, resolverMap);
|
|
3193
|
+
const programmaticResolver = createProgrammaticResolver(resolverMap);
|
|
3043
3194
|
const app = new Hono();
|
|
3044
3195
|
const uploadOrigins = storage.getUploadOrigins?.() ?? [];
|
|
3045
3196
|
app.use("*", createSecurityHeadersMiddleware({ connectSrc: uploadOrigins }));
|
|
@@ -3124,7 +3275,7 @@ function createCmsApp(options) {
|
|
|
3124
3275
|
paths: {}
|
|
3125
3276
|
})
|
|
3126
3277
|
);
|
|
3127
|
-
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit);
|
|
3278
|
+
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
|
|
3128
3279
|
registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
|
|
3129
3280
|
registerConfigRoute(
|
|
3130
3281
|
app,
|
|
@@ -3184,6 +3335,9 @@ function createAPIAdapter(options = {}) {
|
|
|
3184
3335
|
export {
|
|
3185
3336
|
createAPIAdapter,
|
|
3186
3337
|
createCmsApp,
|
|
3187
|
-
|
|
3338
|
+
createProgrammaticResolver,
|
|
3339
|
+
createServer,
|
|
3340
|
+
resolverKey,
|
|
3341
|
+
validateResolverBindings
|
|
3188
3342
|
};
|
|
3189
3343
|
//# sourceMappingURL=index.mjs.map
|