@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.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
- return c.json({ ok: true, data: result.data[0] });
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, schemaFieldNames);
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
- return c.json({ ok: true, data: item });
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
- return c.json({ ok: true, data: item });
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.1.1";
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
- createServer
3338
+ createProgrammaticResolver,
3339
+ createServer,
3340
+ resolverKey,
3341
+ validateResolverBindings
3188
3342
  };
3189
3343
  //# sourceMappingURL=index.mjs.map