@voltro/cms 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +347 -0
- package/dist/ContentForm-D0YGVkRu.js +278 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +376 -0
- package/dist/web.d.ts +312 -0
- package/dist/web.js +2 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as ee, u as te } from "./ContentForm-D0YGVkRu.js";
|
|
2
|
+
import { and as f, boolean as p, date as m, eq as h, gt as ne, gte as re, id as g, integer as _, json as ie, lt as ae, lte as v, neq as y, table as oe, text as b, timestamp as x } from "@voltro/database";
|
|
3
|
+
import { tenant as se } from "@voltro/plugin-multitenancy/mixin";
|
|
4
|
+
import { createHmac as S, timingSafeEqual as ce } from "node:crypto";
|
|
5
|
+
import { Effect as C } from "effect";
|
|
6
|
+
//#region src/contentType.ts
|
|
7
|
+
var w = [
|
|
8
|
+
"draft",
|
|
9
|
+
"published",
|
|
10
|
+
"archived"
|
|
11
|
+
], T = [
|
|
12
|
+
"id",
|
|
13
|
+
"status",
|
|
14
|
+
"publishAt",
|
|
15
|
+
"tenantId",
|
|
16
|
+
"createdAt",
|
|
17
|
+
"updatedAt",
|
|
18
|
+
"createdBy",
|
|
19
|
+
"updatedBy"
|
|
20
|
+
], le = (e) => {
|
|
21
|
+
let t = {}, n = [], r = [];
|
|
22
|
+
for (let [i, a] of Object.entries(e.fields)) {
|
|
23
|
+
if (T.includes(i)) throw Error(`defineContentType('${e.name}'): field '${i}' collides with a reserved lifecycle column (${T.join(", ")}). Rename it.`);
|
|
24
|
+
let o = a;
|
|
25
|
+
if (o.unique && o.kind !== "string") throw Error(`defineContentType('${e.name}'): field '${i}' is marked unique() but is a '${o.kind}' field — per-tenant uniqueness is only supported on string fields (a slug). Remove unique() or make it a Schema.String.`);
|
|
26
|
+
t[i] = o, o.derived && n.push(i), o.unique && r.push(i);
|
|
27
|
+
}
|
|
28
|
+
if (e.list) {
|
|
29
|
+
let n = /* @__PURE__ */ new Set([...Object.keys(t), ...T]);
|
|
30
|
+
for (let t of e.list.columns) if (!n.has(t)) throw Error(`defineContentType('${e.name}'): list.columns entry '${t}' is not a declared field (or lifecycle column) of this content type.`);
|
|
31
|
+
let r = e.list.defaultSort?.field;
|
|
32
|
+
if (r !== void 0 && !n.has(r)) throw Error(`defineContentType('${e.name}'): list.defaultSort.field '${r}' is not a declared field (or lifecycle column) of this content type.`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name: e.name,
|
|
36
|
+
displayName: e.displayName,
|
|
37
|
+
pluralName: e.pluralName,
|
|
38
|
+
fields: t,
|
|
39
|
+
...e.list ? { list: e.list } : {},
|
|
40
|
+
...e.idPrefix ? { idPrefix: e.idPrefix } : {},
|
|
41
|
+
derivedFields: n,
|
|
42
|
+
uniqueFields: r
|
|
43
|
+
};
|
|
44
|
+
}, E = (e) => {
|
|
45
|
+
switch (e.kind) {
|
|
46
|
+
case "string": return e.isOptional ? b().nullable() : b();
|
|
47
|
+
case "number": return e.isOptional ? _().nullable() : _();
|
|
48
|
+
case "boolean": return e.isOptional ? p().nullable() : p();
|
|
49
|
+
case "date": return e.isOptional ? m().nullable() : m();
|
|
50
|
+
case "datetime": return e.isOptional ? x().nullable() : x();
|
|
51
|
+
case "literal": {
|
|
52
|
+
let t = e.literals ?? [], n = t.length > 0 ? b().oneOf(t) : b();
|
|
53
|
+
return e.isOptional ? n.nullable() : n;
|
|
54
|
+
}
|
|
55
|
+
case "richText":
|
|
56
|
+
case "array":
|
|
57
|
+
case "struct": {
|
|
58
|
+
let t = ie();
|
|
59
|
+
return e.isOptional ? t.nullable() : t;
|
|
60
|
+
}
|
|
61
|
+
case "media":
|
|
62
|
+
case "reference": return e.isOptional ? b().nullable() : b();
|
|
63
|
+
}
|
|
64
|
+
}, ue = (e) => {
|
|
65
|
+
let t = {};
|
|
66
|
+
for (let [n, r] of Object.entries(e.fields)) t[n] = E(r);
|
|
67
|
+
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
|
+
id: r,
|
|
71
|
+
status: b().oneOf(w),
|
|
72
|
+
...t === "drafts" ? { publishAt: x().nullable() } : {},
|
|
73
|
+
...ue(e)
|
|
74
|
+
}).with(se()).reactive();
|
|
75
|
+
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);
|
|
81
|
+
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
|
+
let i = {
|
|
83
|
+
t: e,
|
|
84
|
+
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 = {}) => {
|
|
89
|
+
if (!e) return null;
|
|
90
|
+
let n = A(t.secret);
|
|
91
|
+
if (n === null) return null;
|
|
92
|
+
let r = e.indexOf(".");
|
|
93
|
+
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);
|
|
95
|
+
if (s.length !== o.length || !ce(s, o)) return null;
|
|
96
|
+
try {
|
|
97
|
+
let e = JSON.parse(k(i).toString("utf8"));
|
|
98
|
+
return typeof e.t != "string" || typeof e.i != "string" || typeof e.exp != "number" || e.exp < Math.floor(Date.now() / 1e3) ? null : {
|
|
99
|
+
type: e.t,
|
|
100
|
+
id: e.i
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}, he = (e, t, n) => {
|
|
106
|
+
switch (t) {
|
|
107
|
+
case "=": return h(e, n);
|
|
108
|
+
case "!=": return y(e, n);
|
|
109
|
+
case "<": return ae(e, n);
|
|
110
|
+
case "<=": return v(e, n);
|
|
111
|
+
case ">": return ne(e, n);
|
|
112
|
+
case ">=": return re(e, n);
|
|
113
|
+
}
|
|
114
|
+
}, M = (e) => {
|
|
115
|
+
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
|
+
return {
|
|
117
|
+
table: e.table,
|
|
118
|
+
predicate: n,
|
|
119
|
+
order: e.order,
|
|
120
|
+
take: e.take,
|
|
121
|
+
skip: e.skip,
|
|
122
|
+
projection: void 0
|
|
123
|
+
};
|
|
124
|
+
}, N = (e, t) => ({
|
|
125
|
+
where(n, r, i) {
|
|
126
|
+
return N(e, {
|
|
127
|
+
...t,
|
|
128
|
+
predicates: [...t.predicates, he(n, r, i)]
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
orderBy(n, r = "asc") {
|
|
132
|
+
return N(e, {
|
|
133
|
+
...t,
|
|
134
|
+
order: [...t.order, {
|
|
135
|
+
column: n,
|
|
136
|
+
direction: r
|
|
137
|
+
}]
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
limit(n) {
|
|
141
|
+
return N(e, {
|
|
142
|
+
...t,
|
|
143
|
+
take: n
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
offset(n) {
|
|
147
|
+
return N(e, {
|
|
148
|
+
...t,
|
|
149
|
+
skip: n
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
async all() {
|
|
153
|
+
return e.query(M(t));
|
|
154
|
+
},
|
|
155
|
+
async one() {
|
|
156
|
+
return (await e.query(M({
|
|
157
|
+
...t,
|
|
158
|
+
take: 1
|
|
159
|
+
})))[0] ?? null;
|
|
160
|
+
}
|
|
161
|
+
}), P = (e, t, n) => {
|
|
162
|
+
let r = new Map(t.map((e) => [e.name, e])), i = (e) => {
|
|
163
|
+
let t = r.get(e);
|
|
164
|
+
if (!t) throw Error(`cms.contentType('${e}'): unknown content type. Register it via defineContentType and pass it to cmsContext / createCmsClient.`);
|
|
165
|
+
return t;
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
contentType(t) {
|
|
169
|
+
return i(t), N(e, {
|
|
170
|
+
table: `${t}_published`,
|
|
171
|
+
predicates: [],
|
|
172
|
+
order: [],
|
|
173
|
+
take: void 0,
|
|
174
|
+
skip: void 0,
|
|
175
|
+
excludeArchived: !0
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
async preview(t, r, a) {
|
|
179
|
+
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`;
|
|
181
|
+
return (await e.query({
|
|
182
|
+
table: s,
|
|
183
|
+
predicate: h("id", r),
|
|
184
|
+
order: [],
|
|
185
|
+
take: 1,
|
|
186
|
+
skip: void 0,
|
|
187
|
+
projection: void 0
|
|
188
|
+
}))[0] ?? null;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}, ge = (e, t, n = {}) => P(e, t, n.secret), F = (e) => P(e.store, e.types, e.secret), I = (e) => {
|
|
192
|
+
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
|
+
return e.cms;
|
|
194
|
+
}, L = (e) => `${e.name}_drafts`, R = (e) => `${e.name}_published`, z = (e, t, n) => ({
|
|
195
|
+
table: e,
|
|
196
|
+
predicate: h("id", t),
|
|
197
|
+
order: [],
|
|
198
|
+
take: 1,
|
|
199
|
+
skip: void 0,
|
|
200
|
+
projection: void 0,
|
|
201
|
+
...n === !0 ? { crossTenant: !0 } : {}
|
|
202
|
+
}), B = async (e, t, n) => (await e.query(z(t, n)))[0], V = (e, t) => {
|
|
203
|
+
let n = {};
|
|
204
|
+
for (let r of Object.keys(e.fields)) r in t && (n[r] = t[r]);
|
|
205
|
+
return n;
|
|
206
|
+
}, H = async (t, n, r) => {
|
|
207
|
+
let i = u(n, r);
|
|
208
|
+
e(n, i);
|
|
209
|
+
let a = L(n), o = V(n, i), s = i.id;
|
|
210
|
+
if (s !== void 0) {
|
|
211
|
+
let e = await B(t, a, s);
|
|
212
|
+
if (e !== void 0) return await t.update(a, s, {
|
|
213
|
+
...o,
|
|
214
|
+
status: "draft"
|
|
215
|
+
}) ?? e;
|
|
216
|
+
if ((await t.query(z(a, s, !0))).length > 0) throw new c({
|
|
217
|
+
contentType: n.name,
|
|
218
|
+
id: s
|
|
219
|
+
});
|
|
220
|
+
return t.insert(a, {
|
|
221
|
+
...o,
|
|
222
|
+
id: s,
|
|
223
|
+
status: "draft"
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return t.insert(a, {
|
|
227
|
+
...o,
|
|
228
|
+
status: "draft"
|
|
229
|
+
});
|
|
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 }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
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"
|
|
257
|
+
});
|
|
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);
|
|
261
|
+
if (r === void 0 && i === void 0) throw new d({
|
|
262
|
+
contentType: t.name,
|
|
263
|
+
id: n,
|
|
264
|
+
stage: "draft"
|
|
265
|
+
});
|
|
266
|
+
r !== void 0 && await e.update(L(t), n, { status: "archived" }), i !== void 0 && await e.update(R(t), n, { status: "archived" });
|
|
267
|
+
}), K = (e, t) => C.tryPromise({
|
|
268
|
+
try: e,
|
|
269
|
+
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) => {
|
|
271
|
+
let i = J(t), a = await e.query({
|
|
272
|
+
table: i,
|
|
273
|
+
predicate: h("id", n),
|
|
274
|
+
order: [],
|
|
275
|
+
take: 1,
|
|
276
|
+
skip: void 0,
|
|
277
|
+
projection: void 0
|
|
278
|
+
});
|
|
279
|
+
if (a[0] === void 0) throw new d({
|
|
280
|
+
contentType: t.name,
|
|
281
|
+
id: n,
|
|
282
|
+
stage: "draft"
|
|
283
|
+
});
|
|
284
|
+
return await e.update(i, n, { publishAt: r }) ?? a[0];
|
|
285
|
+
}, Se = (e, t) => ({
|
|
286
|
+
table: J(e),
|
|
287
|
+
predicate: f(v("publishAt", t), h("status", "draft")),
|
|
288
|
+
order: [],
|
|
289
|
+
take: void 0,
|
|
290
|
+
skip: void 0,
|
|
291
|
+
projection: void 0
|
|
292
|
+
}), X = async (e, t, n = /* @__PURE__ */ new Date()) => {
|
|
293
|
+
let r = await e.query(Se(t, n)), i = [];
|
|
294
|
+
for (let n of r) {
|
|
295
|
+
let r = n.id;
|
|
296
|
+
typeof r == "string" && (await U(e, t, r), await e.update(J(t), r, { publishAt: null }), i.push(r));
|
|
297
|
+
}
|
|
298
|
+
return i;
|
|
299
|
+
}, Ce = (e, t, n, r) => C.tryPromise({
|
|
300
|
+
try: () => Y(e, t, n, r),
|
|
301
|
+
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) => {
|
|
303
|
+
if (t == null) return t;
|
|
304
|
+
switch (e.kind) {
|
|
305
|
+
case "media": return typeof t == "string" ? await n(t) ?? t : t;
|
|
306
|
+
case "array": {
|
|
307
|
+
let r = e.element;
|
|
308
|
+
return r === void 0 || !Array.isArray(t) ? t : Promise.all(t.map((e) => Z(r, e, n)));
|
|
309
|
+
}
|
|
310
|
+
case "struct": {
|
|
311
|
+
let r = e.struct;
|
|
312
|
+
if (r === void 0 || typeof t != "object" || Array.isArray(t)) return t;
|
|
313
|
+
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));
|
|
315
|
+
return a;
|
|
316
|
+
}
|
|
317
|
+
default: return t;
|
|
318
|
+
}
|
|
319
|
+
}, Q = async (e, t, n) => {
|
|
320
|
+
let r = { ...t };
|
|
321
|
+
for (let [i, a] of Object.entries(e.fields)) {
|
|
322
|
+
let e = a;
|
|
323
|
+
e.kind !== "media" && e.kind !== "array" && e.kind !== "struct" || i in t && (r[i] = await Z(e, t[i], n));
|
|
324
|
+
}
|
|
325
|
+
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) => {
|
|
327
|
+
let t = e.authorization;
|
|
328
|
+
return t && t.toLowerCase().startsWith("bearer ") ? t.slice(7).trim() : e["x-api-key"];
|
|
329
|
+
}, ke = async (e, t) => {
|
|
330
|
+
if (e.method.toUpperCase() !== "GET") return {
|
|
331
|
+
status: 405,
|
|
332
|
+
body: { error: "method_not_allowed" }
|
|
333
|
+
};
|
|
334
|
+
if (!e.path.startsWith(De)) return {
|
|
335
|
+
status: 404,
|
|
336
|
+
body: { error: "not_found" }
|
|
337
|
+
};
|
|
338
|
+
let n = Oe(e.headers), r = n ? t.apiKeys.find((e) => e.key === n) : void 0;
|
|
339
|
+
if (!r) return {
|
|
340
|
+
status: 401,
|
|
341
|
+
body: { error: "unauthorized" }
|
|
342
|
+
};
|
|
343
|
+
let i = e.path.slice(8).split("/").filter((e) => e.length > 0), a = i[0];
|
|
344
|
+
if (!a || !t.types.some((e) => e.name === a)) return {
|
|
345
|
+
status: 404,
|
|
346
|
+
body: { error: "unknown_content_type" }
|
|
347
|
+
};
|
|
348
|
+
if (!(r.types === void 0 || r.types.includes("*") || r.types.includes(a))) return {
|
|
349
|
+
status: 403,
|
|
350
|
+
body: {
|
|
351
|
+
error: "forbidden",
|
|
352
|
+
type: a
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
if (i.length >= 2) {
|
|
356
|
+
let e = i.slice(1).join("/"), n = await t.cms.contentType(a).where("slug", "=", e).one();
|
|
357
|
+
return n ? {
|
|
358
|
+
status: 200,
|
|
359
|
+
body: n
|
|
360
|
+
} : {
|
|
361
|
+
status: 404,
|
|
362
|
+
body: {
|
|
363
|
+
error: "not_found",
|
|
364
|
+
type: a,
|
|
365
|
+
slug: e
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
let o = t.defaultLimit ?? 20, s = t.maxLimit ?? 100, c = Number(e.query?.limit), l = Number.isFinite(c) && c > 0 ? Math.min(c, s) : o;
|
|
370
|
+
return {
|
|
371
|
+
status: 200,
|
|
372
|
+
body: await t.cms.contentType(a).limit(l).all()
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
//#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 };
|
package/dist/web.d.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Schema as Schema_2 } from 'effect';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compute every `derivedFrom` field from its source value. Returns a NEW
|
|
6
|
+
* row — the input is not mutated. A caller-provided value for a derived
|
|
7
|
+
* field is OVERWRITTEN (derived values are the content type's opinion,
|
|
8
|
+
* mirroring the column `.computed()` semantics). Derivations run in
|
|
9
|
+
* declaration order against the progressively-updated row, so a derived
|
|
10
|
+
* field may source another, earlier-declared derived field.
|
|
11
|
+
*/
|
|
12
|
+
export declare const applyDerivations: (type: Pick<ContentType, "fields" | "derivedFields">, row: Readonly<Record<string, unknown>>) => Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
/** Everything the write pipeline can fail with on the typed channel. */
|
|
15
|
+
export declare type CmsWriteError = ContentValidationFailed | ContentNotFound | ContentIdConflict;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Auto-generated content form. Renders one labelled widget per field,
|
|
19
|
+
* driven entirely by each field's `editorHint`. Stateless — the caller
|
|
20
|
+
* owns `value` + `onChange`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ContentForm: (props: ContentFormProps) => ReactNode;
|
|
23
|
+
|
|
24
|
+
export declare interface ContentFormProps {
|
|
25
|
+
readonly contentType: ContentFormType;
|
|
26
|
+
readonly value: ContentValue;
|
|
27
|
+
readonly onChange: (next: ContentValue) => void;
|
|
28
|
+
/** Override the widget for one or more editor hints (e.g. a TipTap richText). */
|
|
29
|
+
readonly widgets?: Partial<Record<EditorHint, Widget>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The content-type shape ContentForm reads (structural — avoids a value import). */
|
|
33
|
+
export declare interface ContentFormType {
|
|
34
|
+
readonly fields: Readonly<Record<string, FieldSchema>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Raised by `saveDraft` when the caller supplies an explicit `id` that
|
|
39
|
+
* already exists OUTSIDE the caller's scope (another tenant's row).
|
|
40
|
+
* Refusing here keeps a caller-pinned id from ever updating — or, on a
|
|
41
|
+
* store without DB-enforced PKs, overwriting — a foreign row.
|
|
42
|
+
*/
|
|
43
|
+
export declare class ContentIdConflict extends ContentIdConflict_base {
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare const ContentIdConflict_base: Schema_2.TaggedErrorClass<ContentIdConflict, "ContentIdConflict", {
|
|
47
|
+
readonly _tag: Schema_2.tag<"ContentIdConflict">;
|
|
48
|
+
} & {
|
|
49
|
+
contentType: typeof Schema_2.String;
|
|
50
|
+
id: typeof Schema_2.String;
|
|
51
|
+
}>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Raised by `publish` / `unpublish` / `archive` when the row id doesn't
|
|
55
|
+
* exist in the caller's scope. On a tenant-scoped request store this is
|
|
56
|
+
* ALSO the cross-tenant answer: another tenant's row is indistinguishable
|
|
57
|
+
* from a missing one (the scoped read never sees it).
|
|
58
|
+
*/
|
|
59
|
+
export declare class ContentNotFound extends ContentNotFound_base {
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
declare const ContentNotFound_base: Schema_2.TaggedErrorClass<ContentNotFound, "ContentNotFound", {
|
|
63
|
+
readonly _tag: Schema_2.tag<"ContentNotFound">;
|
|
64
|
+
} & {
|
|
65
|
+
contentType: typeof Schema_2.String;
|
|
66
|
+
id: typeof Schema_2.String;
|
|
67
|
+
/** Which lifecycle table the id was looked up in. */
|
|
68
|
+
stage: Schema_2.Literal<["draft", "published"]>;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
/** A registered content type. */
|
|
72
|
+
declare interface ContentType {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly displayName: string;
|
|
75
|
+
readonly pluralName: string;
|
|
76
|
+
readonly fields: Readonly<Record<string, FieldSchema>>;
|
|
77
|
+
readonly list?: ListConfig;
|
|
78
|
+
readonly idPrefix?: string;
|
|
79
|
+
/** Field names whose value is computed-on-save (derivedFrom). */
|
|
80
|
+
readonly derivedFields: ReadonlyArray<string>;
|
|
81
|
+
/** Field names marked unique-per-tenant (composite `(tenantId, field)` UNIQUE). */
|
|
82
|
+
readonly uniqueFields: ReadonlyArray<string>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Raised by `validateContent` / `saveDraft` when a row violates the
|
|
87
|
+
* content type's field rules (pattern / maxLength / typed kinds /
|
|
88
|
+
* literal sets / rich-text content policy). Carries EVERY violation,
|
|
89
|
+
* per field, so an editor can annotate the whole form in one pass.
|
|
90
|
+
*/
|
|
91
|
+
export declare class ContentValidationFailed extends ContentValidationFailed_base {
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
declare const ContentValidationFailed_base: Schema_2.TaggedErrorClass<ContentValidationFailed, "ContentValidationFailed", {
|
|
95
|
+
readonly _tag: Schema_2.tag<"ContentValidationFailed">;
|
|
96
|
+
} & {
|
|
97
|
+
contentType: typeof Schema_2.String;
|
|
98
|
+
violations: Schema_2.Array$<Schema_2.Struct<{
|
|
99
|
+
field: typeof Schema_2.String;
|
|
100
|
+
rule: typeof Schema_2.String;
|
|
101
|
+
message: typeof Schema_2.String;
|
|
102
|
+
}>>;
|
|
103
|
+
}>;
|
|
104
|
+
|
|
105
|
+
export declare type ContentValue = Record<string, unknown>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Collect every save-time rule violation for `row` against `type` —
|
|
109
|
+
* required presence, per-kind type checks, `maxLength`, `pattern`,
|
|
110
|
+
* `Literal` value sets, and the rich-text content policy
|
|
111
|
+
* (`allowImages` / `allowEmbeds: false` reject image/embed nodes or the
|
|
112
|
+
* `<img>` / `<iframe>` / `<embed>` markers in string values).
|
|
113
|
+
*
|
|
114
|
+
* Pure and never throws — usable in a browser form ("annotate every
|
|
115
|
+
* invalid field") as well as on the server. `id` is allowed (must be a
|
|
116
|
+
* string); the lifecycle/mixin columns the pipeline owns (`status`,
|
|
117
|
+
* `tenantId`, audit who/when) are tolerated on input and ignored; any
|
|
118
|
+
* OTHER undeclared key is an `unknown` violation (usually a typo'd
|
|
119
|
+
* field name).
|
|
120
|
+
*
|
|
121
|
+
* NOTE on ordering: run `applyDerivations` first when validating a row
|
|
122
|
+
* that is about to be written — a derived field's rules apply to the
|
|
123
|
+
* derived value, not to a caller value that's about to be overwritten.
|
|
124
|
+
*/
|
|
125
|
+
export declare const contentViolations: (type: Pick<ContentType, "name" | "fields">, row: Readonly<Record<string, unknown>>) => ReadonlyArray<FieldViolation>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mark a field computed-on-save from another field. The transform runs
|
|
129
|
+
* before the row is written — `saveDraft` applies it (via
|
|
130
|
+
* `applyDerivations`) ahead of validation, and `publish` copies the
|
|
131
|
+
* derived value. A caller-provided value is overwritten, mirroring the
|
|
132
|
+
* column `.computed()` semantics.
|
|
133
|
+
*
|
|
134
|
+
* ```ts
|
|
135
|
+
* slug: Schema.String.pipe(pattern(/^[a-z0-9-]+$/), derivedFrom('title', slugify))
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export declare const derivedFrom: (source: string, transform: (value: unknown, row: Record<string, unknown>) => unknown) => FieldRefinement;
|
|
139
|
+
|
|
140
|
+
/** A field marked computed-on-save from another field's value. */
|
|
141
|
+
export declare interface DerivedSpec {
|
|
142
|
+
readonly source: string;
|
|
143
|
+
readonly transform: (value: unknown, row: Record<string, unknown>) => unknown;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The widget the editor renders for a field — drives ContentForm. */
|
|
147
|
+
export declare type EditorHint = 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'richText' | 'media' | 'reference' | 'array' | 'struct';
|
|
148
|
+
|
|
149
|
+
/** Discriminator for how a field compiles to a column + serialises. */
|
|
150
|
+
export declare type FieldKind = 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'literal' | 'richText' | 'media' | 'reference' | 'array' | 'struct';
|
|
151
|
+
|
|
152
|
+
/** A refinement applied through `.pipe(...)`. */
|
|
153
|
+
export declare type FieldRefinement = (field: FieldSchema) => FieldSchema;
|
|
154
|
+
|
|
155
|
+
/** A fully-resolved field descriptor stored on a content type. */
|
|
156
|
+
export declare interface FieldSchema {
|
|
157
|
+
readonly kind: FieldKind;
|
|
158
|
+
readonly editorHint: EditorHint;
|
|
159
|
+
/** When true the field is nullable in the DB + optional in the editor. */
|
|
160
|
+
readonly isOptional: boolean;
|
|
161
|
+
/** Max string length — drives textarea-vs-input + save-time validation. */
|
|
162
|
+
readonly maxLength?: number;
|
|
163
|
+
/** Source-form regex (string) for save-time validation. */
|
|
164
|
+
readonly pattern?: string;
|
|
165
|
+
/** Closed value set for `literal` fields → `<select>` + CHECK constraint. */
|
|
166
|
+
readonly literals?: ReadonlyArray<string>;
|
|
167
|
+
/**
|
|
168
|
+
* When true, the field is unique PER TENANT — the derived tables carry a
|
|
169
|
+
* composite `(tenantId, <field>)` DB UNIQUE constraint (see contentType.ts).
|
|
170
|
+
* Only meaningful on a `string` field (typically a slug).
|
|
171
|
+
*/
|
|
172
|
+
readonly unique?: boolean;
|
|
173
|
+
/** RichText options. */
|
|
174
|
+
readonly richText?: RichTextOptions;
|
|
175
|
+
/** Referenced content-type name for `reference` fields. */
|
|
176
|
+
readonly referenceType?: string;
|
|
177
|
+
/** Element field for `array` fields. */
|
|
178
|
+
readonly element?: FieldSchema;
|
|
179
|
+
/** Nested field map for `struct` fields. */
|
|
180
|
+
readonly struct?: Readonly<Record<string, FieldSchema>>;
|
|
181
|
+
/** Computed-on-save derivation (set via `derivedFrom`). */
|
|
182
|
+
readonly derived?: DerivedSpec;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One save-time rule violation, addressed to a field path. */
|
|
186
|
+
export declare interface FieldViolation {
|
|
187
|
+
/** Dotted/indexed field path, e.g. `title`, `meta.ogTitle`, `tags[2]`. */
|
|
188
|
+
readonly field: string;
|
|
189
|
+
/** The rule that failed: `required` | `type` | `maxLength` | `pattern` | `literal` | `richText` | `unknown`. */
|
|
190
|
+
readonly rule: string;
|
|
191
|
+
readonly message: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* List-view configuration for the consuming editor/template: which
|
|
196
|
+
* columns its list page shows and the default sort. Validated by
|
|
197
|
+
* `defineContentType` — every referenced name must be a declared field
|
|
198
|
+
* or a lifecycle column, so a typo fails at definition time instead of
|
|
199
|
+
* rendering an empty column.
|
|
200
|
+
*/
|
|
201
|
+
declare interface ListConfig {
|
|
202
|
+
readonly columns: ReadonlyArray<string>;
|
|
203
|
+
readonly defaultSort?: {
|
|
204
|
+
readonly field: string;
|
|
205
|
+
readonly dir: 'asc' | 'desc';
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Cap a string field's length. Past `value` the editor renders a textarea. */
|
|
210
|
+
export declare const maxLength: (value: number) => FieldRefinement;
|
|
211
|
+
|
|
212
|
+
/** Constrain a string field to a regex (source form, validated on save). */
|
|
213
|
+
export declare const pattern: (re: RegExp) => FieldRefinement;
|
|
214
|
+
|
|
215
|
+
export declare interface PipeableField extends FieldSchema {
|
|
216
|
+
readonly isOptional: boolean;
|
|
217
|
+
pipe(...refinements: ReadonlyArray<FieldRefinement>): PipeableField;
|
|
218
|
+
/** Mark the field optional → nullable column + non-required editor input. */
|
|
219
|
+
optional(): PipeableField;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Options carried by a RichText field, enforced at save time by
|
|
224
|
+
* `validateContent`: an explicit `false` REJECTS the content class
|
|
225
|
+
* (image/embed nodes in a structured document, or `<img>` / `<iframe>` /
|
|
226
|
+
* `<embed>` markers in a string value); `true` or omitted allows it.
|
|
227
|
+
*/
|
|
228
|
+
export declare interface RichTextOptions {
|
|
229
|
+
readonly allowImages?: boolean;
|
|
230
|
+
readonly allowEmbeds?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The CMS field namespace. Scalars mirror the effect/Schema names the
|
|
235
|
+
* docs use; the CMS-only constructors (RichText / Media / Reference /
|
|
236
|
+
* Array / Struct) carry their editor metadata. `optional()` is exposed
|
|
237
|
+
* as a property accessor on each scalar through the underlying field.
|
|
238
|
+
*/
|
|
239
|
+
export declare const Schema: {
|
|
240
|
+
/** Single-line text. `.pipe(maxLength(N>120))` switches it to a textarea. */
|
|
241
|
+
readonly String: PipeableField;
|
|
242
|
+
/** Numeric input. */
|
|
243
|
+
readonly Number: PipeableField;
|
|
244
|
+
/** Toggle. */
|
|
245
|
+
readonly Boolean: PipeableField;
|
|
246
|
+
/** Date-only picker. */
|
|
247
|
+
readonly Date: PipeableField;
|
|
248
|
+
/** Date + time picker. */
|
|
249
|
+
readonly DateTime: PipeableField;
|
|
250
|
+
readonly maxLength: (value: number) => FieldRefinement;
|
|
251
|
+
readonly pattern: (re: RegExp) => FieldRefinement;
|
|
252
|
+
readonly unique: () => FieldRefinement;
|
|
253
|
+
/** Closed value set → `<select>` widget + a CHECK constraint. */
|
|
254
|
+
readonly Literal: (values_0: string, ...values: string[]) => PipeableField;
|
|
255
|
+
/**
|
|
256
|
+
* Rich text, stored as a JSON document (e.g. a TipTap doc). The shipped
|
|
257
|
+
* default widget is a plain textarea placeholder — swap in a real editor
|
|
258
|
+
* via the `ContentForm` `widgets` override. `allowImages` /
|
|
259
|
+
* `allowEmbeds: false` reject that content class at save time.
|
|
260
|
+
*/
|
|
261
|
+
readonly RichText: (options?: RichTextOptions) => PipeableField;
|
|
262
|
+
/**
|
|
263
|
+
* A storage object key (e.g. one issued by `@voltro/plugin-storage`),
|
|
264
|
+
* stored as text. Resolve the key to a served URL after a read with
|
|
265
|
+
* `resolveMedia(type, row, resolver)` — the app supplies the resolver
|
|
266
|
+
* (built from its storage layer's `getUrl`/`mintUrl`), so the core
|
|
267
|
+
* package stays decoupled from any storage plugin.
|
|
268
|
+
*/
|
|
269
|
+
readonly Media: () => PipeableField;
|
|
270
|
+
/** A reference to another content type's published rows. */
|
|
271
|
+
readonly Reference: (type: string) => PipeableField;
|
|
272
|
+
/** Repeatable field. Stored as a JSON array. */
|
|
273
|
+
readonly Array: (element: FieldSchema) => PipeableField;
|
|
274
|
+
/** Nested object. Stored as a JSON document. */
|
|
275
|
+
readonly Struct: (fields: Readonly<Record<string, FieldSchema>>) => PipeableField;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/** The canonical slug helper: lowercase, hyphenate, strip non-url chars. */
|
|
279
|
+
export declare const slugify: (value: unknown) => string;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Mark a string field unique PER TENANT. The derived `<type>_drafts` +
|
|
283
|
+
* `<type>_published` tables get a real composite `(tenantId, <field>)`
|
|
284
|
+
* DB UNIQUE constraint (the tenant() mixin's `tenantId` is NOT NULL, so
|
|
285
|
+
* no partial/NULL machinery is needed) — the database rejects a second
|
|
286
|
+
* row with the same slug in the same tenant, not the application. Pair it
|
|
287
|
+
* with `derivedFrom('title', slugify)` for a per-tenant-unique slug.
|
|
288
|
+
*/
|
|
289
|
+
export declare const unique: () => FieldRefinement;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Validate a row against a content type's save-time rules. Throws
|
|
293
|
+
* `ContentValidationFailed` carrying EVERY violation (per-field detail)
|
|
294
|
+
* when any rule breaks; returns nothing on success. The throwing face of
|
|
295
|
+
* `contentViolations` — `saveDraft` calls this after derivation.
|
|
296
|
+
*/
|
|
297
|
+
export declare const validateContent: (type: Pick<ContentType, "name" | "fields">, row: Readonly<Record<string, unknown>>) => void;
|
|
298
|
+
|
|
299
|
+
export declare type Widget = (props: WidgetProps) => ReactNode;
|
|
300
|
+
|
|
301
|
+
/** Resolve the widget for a field, honouring `widgets` overrides. */
|
|
302
|
+
export declare const widgetFor: (hint: EditorHint, overrides?: Partial<Record<EditorHint, Widget>>) => Widget;
|
|
303
|
+
|
|
304
|
+
/** Props for a single field widget. */
|
|
305
|
+
export declare interface WidgetProps {
|
|
306
|
+
readonly name: string;
|
|
307
|
+
readonly field: FieldSchema;
|
|
308
|
+
readonly value: unknown;
|
|
309
|
+
readonly onChange: (value: unknown) => void;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export { }
|
package/dist/web.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, m as o, n as s, o as c, p as l, r as u, s as d, t as f, u as p } from "./ContentForm-D0YGVkRu.js";
|
|
2
|
+
export { f as ContentForm, c as ContentIdConflict, d as ContentNotFound, t as ContentValidationFailed, a as Schema, u as applyDerivations, i as contentViolations, p as derivedFrom, n as maxLength, r as pattern, l as slugify, o as unique, e as validateContent, s as widgetFor };
|