@voltro/cms 0.51.0 → 0.52.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.ts CHANGED
@@ -168,6 +168,9 @@ export declare interface ContentType {
168
168
  readonly fields: Readonly<Record<string, FieldSchema>>;
169
169
  readonly list?: ListConfig;
170
170
  readonly idPrefix?: string;
171
+ /** Declared ISR revalidations fired on publish/unpublish — see
172
+ * `ContentTypeSpec.revalidate`. */
173
+ readonly revalidate?: ContentTypeSpec['revalidate'];
171
174
  /** Field names whose value is computed-on-save (derivedFrom). */
172
175
  readonly derivedFields: ReadonlyArray<string>;
173
176
  /** Field names marked unique-per-tenant (composite `(tenantId, field)` UNIQUE). */
@@ -183,6 +186,24 @@ export declare interface ContentTypeSpec {
183
186
  readonly list?: ListConfig;
184
187
  /** Override the TypeID prefix for awkward plurals / long names. */
185
188
  readonly idPrefix?: string;
189
+ /**
190
+ * ISR routes to drop when content of this type is published or
191
+ * unpublished — `publish()`/`unpublish()` call `revalidatePath` /
192
+ * `revalidateTag` (from `@voltro/runtime`) for each entry after the
193
+ * write commits, reaching every `voltro start` replica.
194
+ *
195
+ * You DON'T need this on postgres for the plain publish case: a route
196
+ * declaring `cacheInvalidatesOn: ['<type>_published']` is already dropped
197
+ * by CDC when the published table changes. Declare routes here for the
198
+ * cases CDC can't cover — non-postgres dialects, pattern purges of routes
199
+ * whose loader reads this content indirectly, or tag fanout.
200
+ */
201
+ readonly revalidate?: {
202
+ /** Route patterns (`/blog/[slug]`) or concrete paths (`/pricing`). */
203
+ readonly paths?: ReadonlyArray<string>;
204
+ /** Tags matched against routes' `cacheInvalidatesOn` entries. */
205
+ readonly tags?: ReadonlyArray<string>;
206
+ };
186
207
  }
187
208
 
188
209
  /**
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { and as f, boolean as p, date as m, eq as h, gt as ne, gte as re, id as
3
3
  import { tenant as se } from "@voltro/plugin-multitenancy/mixin";
4
4
  import { createHmac as S, timingSafeEqual as ce } from "node:crypto";
5
5
  import { Effect as C } from "effect";
6
+ import { revalidatePath as le, revalidateTag as ue } from "@voltro/runtime";
6
7
  //#region src/contentType.ts
7
8
  var w = [
8
9
  "draft",
@@ -17,7 +18,7 @@ var w = [
17
18
  "updatedAt",
18
19
  "createdBy",
19
20
  "updatedBy"
20
- ], le = (e) => {
21
+ ], de = (e) => {
21
22
  let t = {}, n = [], r = [];
22
23
  for (let [i, a] of Object.entries(e.fields)) {
23
24
  if (T.includes(i)) throw Error(`defineContentType('${e.name}'): field '${i}' collides with a reserved lifecycle column (${T.join(", ")}). Rename it.`);
@@ -38,10 +39,11 @@ var w = [
38
39
  fields: t,
39
40
  ...e.list ? { list: e.list } : {},
40
41
  ...e.idPrefix ? { idPrefix: e.idPrefix } : {},
42
+ ...e.revalidate ? { revalidate: e.revalidate } : {},
41
43
  derivedFields: n,
42
44
  uniqueFields: r
43
45
  };
44
- }, E = (e) => {
46
+ }, fe = (e) => {
45
47
  switch (e.kind) {
46
48
  case "string": return e.isOptional ? b().nullable() : b();
47
49
  case "number": return e.isOptional ? _().nullable() : _();
@@ -61,40 +63,40 @@ var w = [
61
63
  case "media":
62
64
  case "reference": return e.isOptional ? b().nullable() : b();
63
65
  }
64
- }, ue = (e) => {
66
+ }, pe = (e) => {
65
67
  let t = {};
66
- for (let [n, r] of Object.entries(e.fields)) t[n] = E(r);
68
+ for (let [n, r] of Object.entries(e.fields)) t[n] = fe(r);
67
69
  return t;
68
- }, de = (e) => e.idPrefix, D = (e, t) => {
69
- let n = de(e), r = n ? g({ prefix: n }) : g(), i = oe(`${e.name}_${t}`, {
70
+ }, me = (e) => e.idPrefix, E = (e, t) => {
71
+ let n = me(e), r = n ? g({ prefix: n }) : g(), i = oe(`${e.name}_${t}`, {
70
72
  id: r,
71
73
  status: b().oneOf(w),
72
74
  ...t === "drafts" ? { publishAt: x().nullable() } : {},
73
- ...ue(e)
75
+ ...pe(e)
74
76
  }).with(se());
75
77
  return e.uniqueFields.reduce((e, t) => e.unique(`uq_${t}`, ["tenantId", t]), i);
76
- }, fe = (e) => ({
77
- draft: D(e, "drafts"),
78
- published: D(e, "published")
79
- }), pe = 3600, O = (e) => (Buffer.isBuffer(e) ? e : Buffer.from(e, "utf8")).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), k = (e) => Buffer.from(e.replace(/-/g, "+").replace(/_/g, "/"), "base64"), A = (e) => e ?? process.env.VOLTRO_SESSION_SECRET ?? null, me = (e, t, n = {}) => {
80
- let r = A(n.secret);
78
+ }, he = (e) => ({
79
+ draft: E(e, "drafts"),
80
+ published: E(e, "published")
81
+ }), ge = 3600, D = (e) => (Buffer.isBuffer(e) ? e : Buffer.from(e, "utf8")).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), O = (e) => Buffer.from(e.replace(/-/g, "+").replace(/_/g, "/"), "base64"), k = (e) => e ?? process.env.VOLTRO_SESSION_SECRET ?? null, _e = (e, t, n = {}) => {
82
+ let r = k(n.secret);
81
83
  if (r === null) throw Error("cms previewToken requires a signing secret — set VOLTRO_SESSION_SECRET or pass { secret }. Refusing to sign with a hardcoded default (it would ship in the bundle and be forgeable).");
82
84
  let i = {
83
85
  t: e,
84
86
  i: t,
85
- exp: Math.floor(Date.now() / 1e3) + (n.ttlSeconds ?? pe)
86
- }, a = O(JSON.stringify(i));
87
- return `${a}.${O(S("sha256", r).update(a).digest())}`;
88
- }, j = (e, t = {}) => {
87
+ exp: Math.floor(Date.now() / 1e3) + (n.ttlSeconds ?? ge)
88
+ }, a = D(JSON.stringify(i));
89
+ return `${a}.${D(S("sha256", r).update(a).digest())}`;
90
+ }, A = (e, t = {}) => {
89
91
  if (!e) return null;
90
- let n = A(t.secret);
92
+ let n = k(t.secret);
91
93
  if (n === null) return null;
92
94
  let r = e.indexOf(".");
93
95
  if (r <= 0 || r === e.length - 1) return null;
94
- let i = e.slice(0, r), a = e.slice(r + 1), o = S("sha256", n).update(i).digest(), s = k(a);
96
+ let i = e.slice(0, r), a = e.slice(r + 1), o = S("sha256", n).update(i).digest(), s = O(a);
95
97
  if (s.length !== o.length || !ce(s, o)) return null;
96
98
  try {
97
- let e = JSON.parse(k(i).toString("utf8"));
99
+ let e = JSON.parse(O(i).toString("utf8"));
98
100
  return typeof e.t != "string" || typeof e.i != "string" || typeof e.exp != "number" || e.exp < Math.floor(Date.now() / 1e3) ? null : {
99
101
  type: e.t,
100
102
  id: e.i
@@ -102,7 +104,7 @@ var w = [
102
104
  } catch {
103
105
  return null;
104
106
  }
105
- }, he = (e, t, n) => {
107
+ }, ve = (e, t, n) => {
106
108
  switch (t) {
107
109
  case "=": return h(e, n);
108
110
  case "!=": return y(e, n);
@@ -111,7 +113,7 @@ var w = [
111
113
  case ">": return ne(e, n);
112
114
  case ">=": return re(e, n);
113
115
  }
114
- }, M = (e) => {
116
+ }, j = (e) => {
115
117
  let t = e.excludeArchived ? [...e.predicates, y("status", "archived")] : e.predicates, n = t.length === 0 ? void 0 : t.length === 1 ? t[0] : f(...t);
116
118
  return {
117
119
  table: e.table,
@@ -121,15 +123,15 @@ var w = [
121
123
  skip: e.skip,
122
124
  projection: void 0
123
125
  };
124
- }, N = (e, t) => ({
126
+ }, M = (e, t) => ({
125
127
  where(n, r, i) {
126
- return N(e, {
128
+ return M(e, {
127
129
  ...t,
128
- predicates: [...t.predicates, he(n, r, i)]
130
+ predicates: [...t.predicates, ve(n, r, i)]
129
131
  });
130
132
  },
131
133
  orderBy(n, r = "asc") {
132
- return N(e, {
134
+ return M(e, {
133
135
  ...t,
134
136
  order: [...t.order, {
135
137
  column: n,
@@ -138,27 +140,27 @@ var w = [
138
140
  });
139
141
  },
140
142
  limit(n) {
141
- return N(e, {
143
+ return M(e, {
142
144
  ...t,
143
145
  take: n
144
146
  });
145
147
  },
146
148
  offset(n) {
147
- return N(e, {
149
+ return M(e, {
148
150
  ...t,
149
151
  skip: n
150
152
  });
151
153
  },
152
154
  async all() {
153
- return e.query(M(t));
155
+ return e.query(j(t));
154
156
  },
155
157
  async one() {
156
- return (await e.query(M({
158
+ return (await e.query(j({
157
159
  ...t,
158
160
  take: 1
159
161
  })))[0] ?? null;
160
162
  }
161
- }), P = (e, t, n) => {
163
+ }), N = (e, t, n) => {
162
164
  let r = new Map(t.map((e) => [e.name, e])), i = (e) => {
163
165
  let t = r.get(e);
164
166
  if (!t) throw Error(`cms.contentType('${e}'): unknown content type. Register it via defineContentType and pass it to cmsContext / createCmsClient.`);
@@ -166,7 +168,7 @@ var w = [
166
168
  };
167
169
  return {
168
170
  contentType(t) {
169
- return i(t), N(e, {
171
+ return i(t), M(e, {
170
172
  table: `${t}_published`,
171
173
  predicates: [],
172
174
  order: [],
@@ -177,7 +179,7 @@ var w = [
177
179
  },
178
180
  async preview(t, r, a) {
179
181
  i(t);
180
- let o = a ? j(a, n ? { secret: n } : {}) : null, s = o !== null && o.type === t && o.id === r ? `${t}_drafts` : `${t}_published`;
182
+ let o = a ? A(a, n ? { secret: n } : {}) : null, s = o !== null && o.type === t && o.id === r ? `${t}_drafts` : `${t}_published`;
181
183
  return (await e.query({
182
184
  table: s,
183
185
  predicate: h("id", r),
@@ -188,10 +190,10 @@ var w = [
188
190
  }))[0] ?? null;
189
191
  }
190
192
  };
191
- }, ge = (e, t, n = {}) => P(e, t, n.secret), F = (e) => P(e.store, e.types, e.secret), I = (e) => {
193
+ }, P = (e, t, n = {}) => N(e, t, n.secret), F = (e) => N(e.store, e.types, e.secret), ye = (e) => {
192
194
  if (e.cms === void 0) throw Error("useCms: `ctx.cms` is not set. Build it with cmsContext(store, types) and attach it to your AppContext (or use createCmsClient at build time).");
193
195
  return e.cms;
194
- }, L = (e) => `${e.name}_drafts`, R = (e) => `${e.name}_published`, z = (e, t, n) => ({
196
+ }, I = (e) => `${e.name}_drafts`, L = (e) => `${e.name}_published`, R = (e, t, n) => ({
195
197
  table: e,
196
198
  predicate: h("id", t),
197
199
  order: [],
@@ -199,21 +201,21 @@ var w = [
199
201
  skip: void 0,
200
202
  projection: void 0,
201
203
  ...n === !0 ? { crossTenant: !0 } : {}
202
- }), B = async (e, t, n) => (await e.query(z(t, n)))[0], V = (e, t) => {
204
+ }), z = async (e, t, n) => (await e.query(R(t, n)))[0], B = (e, t) => {
203
205
  let n = {};
204
206
  for (let r of Object.keys(e.fields)) r in t && (n[r] = t[r]);
205
207
  return n;
206
- }, H = async (t, n, r) => {
208
+ }, V = async (t, n, r) => {
207
209
  let i = u(n, r);
208
210
  e(n, i);
209
- let a = L(n), o = V(n, i), s = i.id;
211
+ let a = I(n), o = B(n, i), s = i.id;
210
212
  if (s !== void 0) {
211
- let e = await B(t, a, s);
213
+ let e = await z(t, a, s);
212
214
  if (e !== void 0) return await t.update(a, s, {
213
215
  ...o,
214
216
  status: "draft"
215
217
  }) ?? e;
216
- if ((await t.query(z(a, s, !0))).length > 0) throw new c({
218
+ if ((await t.query(R(a, s, !0))).length > 0) throw new c({
217
219
  contentType: n.name,
218
220
  id: s
219
221
  });
@@ -227,47 +229,62 @@ var w = [
227
229
  ...o,
228
230
  status: "draft"
229
231
  });
230
- }, U = async (e, t, n) => e.transactional(async (e) => {
231
- let r = await B(e, L(t), n);
232
- if (r === void 0) throw new d({
233
- contentType: t.name,
234
- id: n,
235
- stage: "draft"
236
- });
237
- let i = R(t), a = V(t, r), o = await B(e, i, n), s;
238
- if (o !== void 0) s = await e.update(i, n, {
239
- ...a,
240
- status: "published"
241
- }) ?? o;
242
- else {
243
- let t = r.tenantId;
244
- s = await e.insert(i, {
245
- ...a,
246
- id: n,
247
- status: "published",
248
- ...t == null ? {} : { tenantId: t }
232
+ }, H = (e) => {
233
+ let t = e.revalidate;
234
+ if (t !== void 0) {
235
+ for (let n of t.paths ?? []) le(n).catch((t) => {
236
+ console.warn(`[voltro cms] revalidatePath('${n}') for '${e.name}' failed:`, t);
237
+ });
238
+ for (let n of t.tags ?? []) ue(n).catch((t) => {
239
+ console.warn(`[voltro cms] revalidateTag('${n}') for '${e.name}' failed:`, t);
249
240
  });
250
241
  }
251
- return await e.update(L(t), n, { status: "published" }), s;
252
- }), W = async (e, t, n) => e.transactional(async (e) => {
253
- if (await B(e, R(t), n) === void 0) throw new d({
254
- contentType: t.name,
255
- id: n,
256
- stage: "published"
242
+ }, U = async (e, t, n) => {
243
+ let r = await e.transactional(async (e) => {
244
+ let r = await z(e, I(t), n);
245
+ if (r === void 0) throw new d({
246
+ contentType: t.name,
247
+ id: n,
248
+ stage: "draft"
249
+ });
250
+ let i = L(t), a = B(t, r), o = await z(e, i, n), s;
251
+ if (o !== void 0) s = await e.update(i, n, {
252
+ ...a,
253
+ status: "published"
254
+ }) ?? o;
255
+ else {
256
+ let t = r.tenantId;
257
+ s = await e.insert(i, {
258
+ ...a,
259
+ id: n,
260
+ status: "published",
261
+ ...t == null ? {} : { tenantId: t }
262
+ });
263
+ }
264
+ return await e.update(I(t), n, { status: "published" }), s;
257
265
  });
258
- await e.delete(R(t), n), await B(e, L(t), n) !== void 0 && await e.update(L(t), n, { status: "draft" });
259
- }), G = async (e, t, n) => e.transactional(async (e) => {
260
- let r = await B(e, L(t), n), i = await B(e, R(t), n);
266
+ return H(t), r;
267
+ }, W = async (e, t, n) => {
268
+ await e.transactional(async (e) => {
269
+ if (await z(e, L(t), n) === void 0) throw new d({
270
+ contentType: t.name,
271
+ id: n,
272
+ stage: "published"
273
+ });
274
+ await e.delete(L(t), n), await z(e, I(t), n) !== void 0 && await e.update(I(t), n, { status: "draft" });
275
+ }), H(t);
276
+ }, G = async (e, t, n) => e.transactional(async (e) => {
277
+ let r = await z(e, I(t), n), i = await z(e, L(t), n);
261
278
  if (r === void 0 && i === void 0) throw new d({
262
279
  contentType: t.name,
263
280
  id: n,
264
281
  stage: "draft"
265
282
  });
266
- r !== void 0 && await e.update(L(t), n, { status: "archived" }), i !== void 0 && await e.update(R(t), n, { status: "archived" });
283
+ r !== void 0 && await e.update(I(t), n, { status: "archived" }), i !== void 0 && await e.update(L(t), n, { status: "archived" });
267
284
  }), K = (e, t) => C.tryPromise({
268
285
  try: e,
269
286
  catch: (e) => e
270
- }).pipe(C.catchAll((e) => t(e) ? C.fail(e) : C.die(e))), _e = (e) => e instanceof t || e instanceof c, q = (e) => e instanceof d, ve = (e, t, n) => K(() => H(e, t, n), _e), ye = (e, t, n) => K(() => U(e, t, n), q), be = (e, t, n) => K(() => W(e, t, n), q), xe = (e, t, n) => K(() => G(e, t, n), q), J = (e) => `${e.name}_drafts`, Y = async (e, t, n, r) => {
287
+ }).pipe(C.catchAll((e) => t(e) ? C.fail(e) : C.die(e))), be = (e) => e instanceof t || e instanceof c, q = (e) => e instanceof d, xe = (e, t, n) => K(() => V(e, t, n), be), Se = (e, t, n) => K(() => U(e, t, n), q), Ce = (e, t, n) => K(() => W(e, t, n), q), we = (e, t, n) => K(() => G(e, t, n), q), J = (e) => `${e.name}_drafts`, Y = async (e, t, n, r) => {
271
288
  let i = J(t), a = await e.query({
272
289
  table: i,
273
290
  predicate: h("id", n),
@@ -282,7 +299,7 @@ var w = [
282
299
  stage: "draft"
283
300
  });
284
301
  return await e.update(i, n, { publishAt: r }) ?? a[0];
285
- }, Se = (e, t) => ({
302
+ }, Te = (e, t) => ({
286
303
  table: J(e),
287
304
  predicate: f(v("publishAt", t), h("status", "draft")),
288
305
  order: [],
@@ -290,52 +307,52 @@ var w = [
290
307
  skip: void 0,
291
308
  projection: void 0
292
309
  }), X = async (e, t, n = /* @__PURE__ */ new Date()) => {
293
- let r = await e.query(Se(t, n)), i = [];
310
+ let r = await e.query(Te(t, n)), i = [];
294
311
  for (let n of r) {
295
312
  let r = n.id;
296
313
  typeof r == "string" && (await U(e, t, r), await e.update(J(t), r, { publishAt: null }), i.push(r));
297
314
  }
298
315
  return i;
299
- }, Ce = (e, t, n, r) => C.tryPromise({
316
+ }, Z = (e, t, n, r) => C.tryPromise({
300
317
  try: () => Y(e, t, n, r),
301
318
  catch: (e) => e
302
- }).pipe(C.catchAll((e) => e instanceof d ? C.fail(e) : C.die(e))), we = (e, t, n = /* @__PURE__ */ new Date()) => C.promise(() => X(e, t, n)), Te = (e) => Object.entries(e.fields).filter(([, e]) => e.kind === "media").map(([e]) => e), Z = async (e, t, n) => {
319
+ }).pipe(C.catchAll((e) => e instanceof d ? C.fail(e) : C.die(e))), Ee = (e, t, n = /* @__PURE__ */ new Date()) => C.promise(() => X(e, t, n)), De = (e) => Object.entries(e.fields).filter(([, e]) => e.kind === "media").map(([e]) => e), Q = async (e, t, n) => {
303
320
  if (t == null) return t;
304
321
  switch (e.kind) {
305
322
  case "media": return typeof t == "string" ? await n(t) ?? t : t;
306
323
  case "array": {
307
324
  let r = e.element;
308
- return r === void 0 || !Array.isArray(t) ? t : Promise.all(t.map((e) => Z(r, e, n)));
325
+ return r === void 0 || !Array.isArray(t) ? t : Promise.all(t.map((e) => Q(r, e, n)));
309
326
  }
310
327
  case "struct": {
311
328
  let r = e.struct;
312
329
  if (r === void 0 || typeof t != "object" || Array.isArray(t)) return t;
313
330
  let i = t, a = { ...i };
314
- for (let [e, t] of Object.entries(r)) e in i && (a[e] = await Z(t, i[e], n));
331
+ for (let [e, t] of Object.entries(r)) e in i && (a[e] = await Q(t, i[e], n));
315
332
  return a;
316
333
  }
317
334
  default: return t;
318
335
  }
319
- }, Q = async (e, t, n) => {
336
+ }, $ = async (e, t, n) => {
320
337
  let r = { ...t };
321
338
  for (let [i, a] of Object.entries(e.fields)) {
322
339
  let e = a;
323
- (e.kind === "media" || e.kind === "array" || e.kind === "struct") && i in t && (r[i] = await Z(e, t[i], n));
340
+ (e.kind === "media" || e.kind === "array" || e.kind === "struct") && i in t && (r[i] = await Q(e, t[i], n));
324
341
  }
325
342
  return r;
326
- }, $ = async (e, t, n) => Promise.all(t.map((t) => Q(e, t, n))), Ee = (e) => Te(e), De = "/v1/cms/", Oe = (e) => {
343
+ }, Oe = async (e, t, n) => Promise.all(t.map((t) => $(e, t, n))), ke = (e) => De(e), Ae = "/v1/cms/", je = (e) => {
327
344
  let t = e.authorization;
328
345
  return t && t.toLowerCase().startsWith("bearer ") ? t.slice(7).trim() : e["x-api-key"];
329
- }, ke = async (e, t) => {
346
+ }, Me = async (e, t) => {
330
347
  if (e.method.toUpperCase() !== "GET") return {
331
348
  status: 405,
332
349
  body: { error: "method_not_allowed" }
333
350
  };
334
- if (!e.path.startsWith(De)) return {
351
+ if (!e.path.startsWith(Ae)) return {
335
352
  status: 404,
336
353
  body: { error: "not_found" }
337
354
  };
338
- let n = Oe(e.headers), r = n ? t.apiKeys.find((e) => e.key === n) : void 0;
355
+ let n = je(e.headers), r = n ? t.apiKeys.find((e) => e.key === n) : void 0;
339
356
  if (!r) return {
340
357
  status: 401,
341
358
  body: { error: "unauthorized" }
@@ -373,4 +390,4 @@ var w = [
373
390
  };
374
391
  };
375
392
  //#endregion
376
- export { w as CONTENT_STATUSES, ee as ContentForm, c as ContentIdConflict, d as ContentNotFound, t as ContentValidationFailed, a as Schema, u as applyDerivations, G as archive, xe as archiveEffect, ge as cmsContext, fe as contentTypeToEntities, i as contentViolations, F as createCmsClient, le as defineContentType, te as derivedFrom, ke as handleCmsRest, n as maxLength, Ee as mediaFields, r as pattern, me as previewToken, U as publish, X as publishDue, we as publishDueEffect, ye as publishEffect, Q as resolveMedia, $ as resolveMediaAll, H as saveDraft, ve as saveDraftEffect, Y as schedulePublish, Ce as schedulePublishEffect, l as slugify, o as unique, W as unpublish, be as unpublishEffect, I as useCms, e as validateContent, j as verifyPreviewToken, s as widgetFor };
393
+ export { w as CONTENT_STATUSES, ee as ContentForm, c as ContentIdConflict, d as ContentNotFound, t as ContentValidationFailed, a as Schema, u as applyDerivations, G as archive, we as archiveEffect, P as cmsContext, he as contentTypeToEntities, i as contentViolations, F as createCmsClient, de as defineContentType, te as derivedFrom, Me as handleCmsRest, n as maxLength, ke as mediaFields, r as pattern, _e as previewToken, U as publish, X as publishDue, Ee as publishDueEffect, Se as publishEffect, $ as resolveMedia, Oe as resolveMediaAll, V as saveDraft, xe as saveDraftEffect, Y as schedulePublish, Z as schedulePublishEffect, l as slugify, o as unique, W as unpublish, Ce as unpublishEffect, ye as useCms, e as validateContent, A as verifyPreviewToken, s as widgetFor };
package/dist/web.d.ts CHANGED
@@ -76,12 +76,44 @@ declare interface ContentType {
76
76
  readonly fields: Readonly<Record<string, FieldSchema>>;
77
77
  readonly list?: ListConfig;
78
78
  readonly idPrefix?: string;
79
+ /** Declared ISR revalidations fired on publish/unpublish — see
80
+ * `ContentTypeSpec.revalidate`. */
81
+ readonly revalidate?: ContentTypeSpec['revalidate'];
79
82
  /** Field names whose value is computed-on-save (derivedFrom). */
80
83
  readonly derivedFields: ReadonlyArray<string>;
81
84
  /** Field names marked unique-per-tenant (composite `(tenantId, field)` UNIQUE). */
82
85
  readonly uniqueFields: ReadonlyArray<string>;
83
86
  }
84
87
 
88
+ /** The user-authored spec passed to `defineContentType`. */
89
+ declare interface ContentTypeSpec {
90
+ readonly name: string;
91
+ readonly displayName: string;
92
+ readonly pluralName: string;
93
+ readonly fields: Readonly<Record<string, FieldSchema | PipeableField>>;
94
+ readonly list?: ListConfig;
95
+ /** Override the TypeID prefix for awkward plurals / long names. */
96
+ readonly idPrefix?: string;
97
+ /**
98
+ * ISR routes to drop when content of this type is published or
99
+ * unpublished — `publish()`/`unpublish()` call `revalidatePath` /
100
+ * `revalidateTag` (from `@voltro/runtime`) for each entry after the
101
+ * write commits, reaching every `voltro start` replica.
102
+ *
103
+ * You DON'T need this on postgres for the plain publish case: a route
104
+ * declaring `cacheInvalidatesOn: ['<type>_published']` is already dropped
105
+ * by CDC when the published table changes. Declare routes here for the
106
+ * cases CDC can't cover — non-postgres dialects, pattern purges of routes
107
+ * whose loader reads this content indirectly, or tag fanout.
108
+ */
109
+ readonly revalidate?: {
110
+ /** Route patterns (`/blog/[slug]`) or concrete paths (`/pricing`). */
111
+ readonly paths?: ReadonlyArray<string>;
112
+ /** Tags matched against routes' `cacheInvalidatesOn` entries. */
113
+ readonly tags?: ReadonlyArray<string>;
114
+ };
115
+ }
116
+
85
117
  /**
86
118
  * Raised by `validateContent` / `saveDraft` when a row violates the
87
119
  * content type's field rules (pattern / maxLength / typed kinds /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cms",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "Headless CMS — typed content-type schemas in code (defineContentType + the Schema field namespace), draft/published table derivation (contentTypeToEntities), the ctx.cms typed read surface, the write pipeline (saveDraft/publish/unpublish/archive with save-time validation + derivation), a REST mount (GET /v1/cms/<type>), signed preview tokens, and a browser-safe schema→widget <ContentForm> renderer.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -38,8 +38,9 @@
38
38
  "node": ">=24.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@voltro/database": "0.51.0",
42
- "@voltro/plugin-multitenancy": "0.51.0"
41
+ "@voltro/database": "0.52.0",
42
+ "@voltro/plugin-multitenancy": "0.52.0",
43
+ "@voltro/runtime": "0.52.0"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "effect": "^3.22.0",