@pramen/cms 0.0.51 → 0.0.53

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/README.md CHANGED
@@ -489,6 +489,39 @@ become multilingual. `listCmsCapabilities` is what the editor reads; `listLocale
489
489
  DATA query (which locales are actually authored), and the two differ while a locale is
490
490
  declared but not yet used.
491
491
 
492
+ ### Which store: D1 or the Durable Object
493
+
494
+ For a CMS behind a site, **D1 is usually the right store** — and it is the only one that
495
+ lets the CMS live inside an Astro site's own Worker, because D1 is a binding while a
496
+ Durable Object has to be exported from the worker entry.
497
+
498
+ What you give up, and it is worth knowing before you pick:
499
+
500
+ - **No atomic multi-statement mutations.** D1 has no interactive transactions, so
501
+ `transaction(fn)` runs `fn` as-is: a mutation that throws midway keeps what it already
502
+ wrote. Single-statement writes are still atomic. The Worker logs a specific error naming
503
+ the handler and how many statements had committed, so a partial write is diagnosable
504
+ rather than looking like an ordinary 500. Where the CMS does several writes in one
505
+ mutation — a revision plus the row it belongs to, a delete plus its revision purge — the
506
+ order is chosen so a failure leaves a benign state, but it is not a rollback.
507
+ - **No live queries.** Those need the Durable Object's socket host. The editor does not use
508
+ them, so this only matters if your own frontend subscribes.
509
+ - **Scheduled publish needs a Cron trigger.** There is no DO alarm to self-drain the outbox,
510
+ so a delayed task runs only when a Cron trigger calls `createPramen().scheduled`:
511
+
512
+ ```jsonc
513
+ "triggers": { "crons": ["* * * * *"] }
514
+ ```
515
+
516
+ Forget it and a scheduled publish simply never fires. The Worker now notices — when a
517
+ request-tail drain leaves a task queued for the future and no Cron drain has ever run, it
518
+ logs once naming the fix, and stops as soon as a Cron drain is seen.
519
+ - **One shared database, no tenant column.** A non-`main` tenant on D1 is refused unless you
520
+ set `PRAMEN_D1_ALLOW_MULTITENANT=true`, because the rows would commingle.
521
+
522
+ Pick the Durable Object store when a mutation must be atomic, when you want live queries, or
523
+ when tenants must be isolated by construction.
524
+
492
525
  ## Limitations
493
526
 
494
527
  - **Block `fields` are opaque JSON**, so pramen's row/cell-level ACL and relational queries
package/dist/index.d.ts CHANGED
@@ -908,6 +908,20 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
908
908
  ok: true;
909
909
  }>;
910
910
  listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
911
+ /** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
912
+ * viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
913
+ * runs in `astro:config:setup`, where there is no editor session and shipping a token to
914
+ * CI just to list type names would be the wrong trade.
915
+ *
916
+ * Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
917
+ * `cms_content_types` ("slugs/names are structural, not sensitive"), and
918
+ * `listPublishedPages` already returns the content-type slug of every published page.
919
+ * The projection is deliberately narrow — slug and name only, never the regions or
920
+ * field schema, which describe the editing surface rather than the published site. */
921
+ listPublicContentTypes: import("@pramen/server").Handler<unknown, {
922
+ slug: string;
923
+ name: string;
924
+ }[]>;
911
925
  getContentType: import("@pramen/server").Handler<{
912
926
  id: string;
913
927
  }, Record<string, unknown>>;
@@ -1262,6 +1276,20 @@ export declare const cmsHandlers: {
1262
1276
  ok: true;
1263
1277
  }>;
1264
1278
  listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
1279
+ /** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
1280
+ * viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
1281
+ * runs in `astro:config:setup`, where there is no editor session and shipping a token to
1282
+ * CI just to list type names would be the wrong trade.
1283
+ *
1284
+ * Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
1285
+ * `cms_content_types` ("slugs/names are structural, not sensitive"), and
1286
+ * `listPublishedPages` already returns the content-type slug of every published page.
1287
+ * The projection is deliberately narrow — slug and name only, never the regions or
1288
+ * field schema, which describe the editing surface rather than the published site. */
1289
+ listPublicContentTypes: import("@pramen/server").Handler<unknown, {
1290
+ slug: string;
1291
+ name: string;
1292
+ }[]>;
1265
1293
  getContentType: import("@pramen/server").Handler<{
1266
1294
  id: string;
1267
1295
  }, Record<string, unknown>>;
package/dist/index.js CHANGED
@@ -1273,6 +1273,20 @@ export function createCmsHandlers(opts = {}) {
1273
1273
  return { ok: true };
1274
1274
  }, { ...reviewer, ...mediaIdInput }),
1275
1275
  listContentTypes: query((ctx) => cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } }), viewer),
1276
+ /** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
1277
+ * viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
1278
+ * runs in `astro:config:setup`, where there is no editor session and shipping a token to
1279
+ * CI just to list type names would be the wrong trade.
1280
+ *
1281
+ * Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
1282
+ * `cms_content_types` ("slugs/names are structural, not sensitive"), and
1283
+ * `listPublishedPages` already returns the content-type slug of every published page.
1284
+ * The projection is deliberately narrow — slug and name only, never the regions or
1285
+ * field schema, which describe the editing surface rather than the published site. */
1286
+ listPublicContentTypes: query(async (ctx) => {
1287
+ const rows = await cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } });
1288
+ return rows.map((r) => ({ slug: String(r.slug), name: String(r.name ?? r.slug) }));
1289
+ }),
1276
1290
  getContentType: query(async (ctx, input) => {
1277
1291
  const rows = await cdb(ctx).find({ from: "cms_content_types", where: { id: input.id }, limit: 1 });
1278
1292
  return rows[0] ?? null;
@@ -1883,8 +1897,6 @@ export function createCmsHandlers(opts = {}) {
1883
1897
  // has no notion of `x-pramen-store`. Minting on the D1 store therefore produces a
1884
1898
  // link that 404s forever while the editor reports success — refuse instead of
1885
1899
  // handing out a token that cannot work.
1886
- if (ctx.store === "d1")
1887
- throw new PramenError("page preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
1888
1900
  const db = cdb(ctx);
1889
1901
  // Read the page through the ACL first: minting a link is granting access to it, so a
1890
1902
  // caller who cannot read the page must not be able to mint a link that can.
@@ -2959,12 +2971,6 @@ export function createCollectionHandlers(collections, opts = {}) {
2959
2971
  const secret = previewSecret(ctx.env);
2960
2972
  if (!secret)
2961
2973
  throw previewUnconfigured(); // fail closed — never mint a forgeable link
2962
- // Redemption always reaches a Durable Object (callPrivileged -> PRAMEN.get) and has no
2963
- // notion of `x-pramen-store`, so a link minted on D1 would 404 forever while the
2964
- // editor reported success.
2965
- if (ctx.store === "d1") {
2966
- throw new PramenError("collection preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
2967
- }
2968
2974
  const row = await loadRow(cdb(ctx), c, input.id);
2969
2975
  const ttl = Math.max(60, Math.min(input.expiresIn ?? previewTtl, 30 * 24 * 3600));
2970
2976
  const exp = Math.floor(Date.now() / 1000) + ttl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "description": "Optional block/page builder for pramen — Drupal-Paragraphs-style typed blocks in named regions, reusable blocks, scheduled publishing, built entirely from pramen primitives.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@pramen/server": "0.0.51"
44
+ "@pramen/server": "0.0.53"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "react": ">=18"
package/src/index.ts CHANGED
@@ -1724,6 +1724,21 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
1724
1724
 
1725
1725
  listContentTypes: query((ctx) => cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } }), viewer),
1726
1726
 
1727
+ /** Content-type slugs + names, PUBLIC. The editor-facing `listContentTypes` above is
1728
+ * viewer-gated, which a BUILD cannot satisfy: `@pramen/cms-astro`'s `collections: "auto"`
1729
+ * runs in `astro:config:setup`, where there is no editor session and shipping a token to
1730
+ * CI just to list type names would be the wrong trade.
1731
+ *
1732
+ * Nothing new is exposed. `cmsPolicies().public` already grants anonymous read of
1733
+ * `cms_content_types` ("slugs/names are structural, not sensitive"), and
1734
+ * `listPublishedPages` already returns the content-type slug of every published page.
1735
+ * The projection is deliberately narrow — slug and name only, never the regions or
1736
+ * field schema, which describe the editing surface rather than the published site. */
1737
+ listPublicContentTypes: query(async (ctx) => {
1738
+ const rows = await cdb(ctx).find({ from: "cms_content_types", orderBy: { column: "name" } });
1739
+ return rows.map((r) => ({ slug: String(r.slug), name: String(r.name ?? r.slug) }));
1740
+ }),
1741
+
1727
1742
  getContentType: query(async (ctx, input: { id: string }) => {
1728
1743
  const rows = await cdb(ctx).find({ from: "cms_content_types", where: { id: input.id }, limit: 1 });
1729
1744
  return rows[0] ?? null;
@@ -2314,7 +2329,7 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
2314
2329
  // has no notion of `x-pramen-store`. Minting on the D1 store therefore produces a
2315
2330
  // link that 404s forever while the editor reports success — refuse instead of
2316
2331
  // handing out a token that cannot work.
2317
- if (ctx.store === "d1") throw new PramenError("page preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
2332
+
2318
2333
  const db = cdb(ctx);
2319
2334
  // Read the page through the ACL first: minting a link is granting access to it, so a
2320
2335
  // caller who cannot read the page must not be able to mint a link that can.
@@ -3561,12 +3576,6 @@ export function createCollectionHandlers(collections: readonly CollectionDef[],
3561
3576
  needs(c, "preview");
3562
3577
  const secret = previewSecret(ctx.env);
3563
3578
  if (!secret) throw previewUnconfigured(); // fail closed — never mint a forgeable link
3564
- // Redemption always reaches a Durable Object (callPrivileged -> PRAMEN.get) and has no
3565
- // notion of `x-pramen-store`, so a link minted on D1 would 404 forever while the
3566
- // editor reported success.
3567
- if (ctx.store === "d1") {
3568
- throw new PramenError("collection preview is not available on the D1 store (redemption requires the Durable Object)", 503, "unavailable");
3569
- }
3570
3579
  const row = await loadRow(cdb(ctx), c, input.id);
3571
3580
  const ttl = Math.max(60, Math.min(input.expiresIn ?? previewTtl, 30 * 24 * 3600));
3572
3581
  const exp = Math.floor(Date.now() / 1000) + ttl;