@cancia/astro 0.7.0 → 0.9.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/{chunk-L2VKQJPY.js → chunk-2NVZWZPE.js} +62 -33
- package/dist/{chunk-22DJVJBR.js → chunk-DIV2FFYX.js} +1 -1
- package/dist/{chunk-UR5WC3RA.js → chunk-GBTFXQEA.js} +20 -6
- package/dist/chunk-GJIX7MWC.js +55 -0
- package/dist/{chunk-VFMOVGMC.js → chunk-HQIGNRIU.js} +2 -2
- package/dist/{chunk-BOIQNZAO.js → chunk-KHCFVVYV.js} +53 -1
- package/dist/{chunk-AGNPUTKS.js → chunk-LJSSPOSW.js} +2 -2
- package/dist/{chunk-GNWD7EL2.js → chunk-MXVOJRAM.js} +22 -39
- package/dist/{chunk-NG5GJME5.js → chunk-VL6FO446.js} +8 -1
- package/dist/{chunk-UMCBQXB6.js → chunk-WVHTCFE6.js} +17 -1
- package/dist/content.d.ts +24 -2
- package/dist/content.js +47 -4
- package/dist/endpoints/auth.js +7 -48
- package/dist/endpoints/lists.js +2 -2
- package/dist/endpoints/schemas.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +285 -71
- package/dist/loader/index.d.ts +24 -0
- package/dist/loader/index.js +4 -2
- package/dist/{portable-text-BikSqS9T.d.ts → portable-text-D74aIuHn.d.ts} +25 -1
- package/dist/richtext/CanciaRichRegion.astro +57 -0
- package/dist/richtext/index.d.ts +2 -2
- package/dist/richtext/index.js +7 -3
- package/dist/runtime.js +3 -3
- package/dist/schema/index.d.ts +47 -2
- package/dist/schema/index.js +9 -3
- package/dist/schema/loader.d.ts +23 -0
- package/dist/schema/loader.js +12 -0
- package/dist/storage/index.js +2 -2
- package/dist/transform/text-helper.d.ts +47 -0
- package/dist/transform/text-helper.js +64 -0
- package/package.json +10 -4
|
@@ -67,6 +67,52 @@ function hashRev(value) {
|
|
|
67
67
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, rmSync } from "fs";
|
|
68
68
|
import { dirname as dirname2, join } from "path";
|
|
69
69
|
import { randomUUID } from "crypto";
|
|
70
|
+
|
|
71
|
+
// src/storage/order.ts
|
|
72
|
+
function applyOrder(order, ids) {
|
|
73
|
+
const result = [];
|
|
74
|
+
const seen = /* @__PURE__ */ new Set();
|
|
75
|
+
for (const id of order) {
|
|
76
|
+
if (ids.has(id)) {
|
|
77
|
+
result.push(id);
|
|
78
|
+
seen.add(id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
82
|
+
return [...result, ...extras];
|
|
83
|
+
}
|
|
84
|
+
function orderAfterCreate(order, id) {
|
|
85
|
+
return order.includes(id) ? order : [...order, id];
|
|
86
|
+
}
|
|
87
|
+
function orderAfterDelete(order, id, stillExistsElsewhere) {
|
|
88
|
+
if (stillExistsElsewhere) return order;
|
|
89
|
+
const next = order.filter((existingId) => existingId !== id);
|
|
90
|
+
return next.length === order.length ? order : next;
|
|
91
|
+
}
|
|
92
|
+
var UnknownEntryError = class extends Error {
|
|
93
|
+
constructor(id, listName) {
|
|
94
|
+
super(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
95
|
+
this.id = id;
|
|
96
|
+
this.listName = listName;
|
|
97
|
+
this.name = "UnknownEntryError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function planReorder(ids, allIds, listName) {
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
if (!allIds.has(id)) throw new UnknownEntryError(id, listName);
|
|
103
|
+
}
|
|
104
|
+
const keep = new Set(ids);
|
|
105
|
+
const dropped = [...allIds].filter((id) => !keep.has(id));
|
|
106
|
+
return { order: ids, dropped };
|
|
107
|
+
}
|
|
108
|
+
function entryExistsMessage(id, listName, locale) {
|
|
109
|
+
return `List entry "${id}" already exists in "${listName}" (${locale})`;
|
|
110
|
+
}
|
|
111
|
+
function entryNotFoundMessage(id, listName, locale) {
|
|
112
|
+
return `List entry "${id}" not found in "${listName}" (${locale})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/storage/json-file-v2.ts
|
|
70
116
|
function readJsonFile(filePath, fallback) {
|
|
71
117
|
if (!existsSync2(filePath)) return fallback;
|
|
72
118
|
try {
|
|
@@ -168,18 +214,6 @@ function makeListStore(listsDir) {
|
|
|
168
214
|
function writeEntry(site, listName, locale, entry) {
|
|
169
215
|
writeJsonFile(entryPath(listsDir, listName, site, locale, entry.id), entry);
|
|
170
216
|
}
|
|
171
|
-
function applyOrder(order, ids) {
|
|
172
|
-
const result = [];
|
|
173
|
-
const seen = /* @__PURE__ */ new Set();
|
|
174
|
-
for (const id of order) {
|
|
175
|
-
if (ids.has(id)) {
|
|
176
|
-
result.push(id);
|
|
177
|
-
seen.add(id);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
181
|
-
return [...result, ...extras];
|
|
182
|
-
}
|
|
183
217
|
return {
|
|
184
218
|
async list(site, listName, locale) {
|
|
185
219
|
const order = readOrder(listsDir, listName, site);
|
|
@@ -218,7 +252,7 @@ function makeListStore(listsDir) {
|
|
|
218
252
|
const finalId = id ?? randomUUID();
|
|
219
253
|
const existing = readEntry(site, listName, finalId, locale);
|
|
220
254
|
if (existing) {
|
|
221
|
-
throw new Error(
|
|
255
|
+
throw new Error(entryExistsMessage(finalId, listName, locale));
|
|
222
256
|
}
|
|
223
257
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
224
258
|
const entry = {
|
|
@@ -230,15 +264,14 @@ function makeListStore(listsDir) {
|
|
|
230
264
|
};
|
|
231
265
|
writeEntry(site, listName, locale, entry);
|
|
232
266
|
const order = readOrder(listsDir, listName, site);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
267
|
+
const nextOrder = orderAfterCreate(order, finalId);
|
|
268
|
+
if (nextOrder !== order) writeOrder(listsDir, listName, site, nextOrder);
|
|
236
269
|
return toEntry(entry);
|
|
237
270
|
},
|
|
238
271
|
async update(site, listName, id, locale, data, rev) {
|
|
239
272
|
const existing = readEntry(site, listName, id, locale);
|
|
240
273
|
if (!existing) {
|
|
241
|
-
throw new Error(
|
|
274
|
+
throw new Error(entryNotFoundMessage(id, listName, locale));
|
|
242
275
|
}
|
|
243
276
|
if (hashRev(existing.data) !== rev) throw new RevConflictError();
|
|
244
277
|
const updated = {
|
|
@@ -254,13 +287,9 @@ function makeListStore(listsDir) {
|
|
|
254
287
|
if (!existsSync2(path)) return;
|
|
255
288
|
rmSync(path);
|
|
256
289
|
const stillExists = listLocales(listsDir, listName, site).some((loc) => existsSync2(entryPath(listsDir, listName, site, loc, id)));
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (next.length !== order.length) {
|
|
261
|
-
writeOrder(listsDir, listName, site, next);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
290
|
+
const order = readOrder(listsDir, listName, site);
|
|
291
|
+
const next = orderAfterDelete(order, id, stillExists);
|
|
292
|
+
if (next !== order) writeOrder(listsDir, listName, site, next);
|
|
264
293
|
},
|
|
265
294
|
async reorder(site, listName, ids) {
|
|
266
295
|
const locales = listLocales(listsDir, listName, site);
|
|
@@ -270,20 +299,14 @@ function makeListStore(listsDir) {
|
|
|
270
299
|
allIds.add(id);
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
throw new Error(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
const supplied = new Set(ids);
|
|
279
|
-
for (const id of allIds) {
|
|
280
|
-
if (supplied.has(id)) continue;
|
|
302
|
+
const { order, dropped } = planReorder(ids, allIds, listName);
|
|
303
|
+
for (const id of dropped) {
|
|
281
304
|
for (const loc of locales) {
|
|
282
305
|
const path = entryPath(listsDir, listName, site, loc, id);
|
|
283
306
|
if (existsSync2(path)) rmSync(path);
|
|
284
307
|
}
|
|
285
308
|
}
|
|
286
|
-
writeOrder(listsDir, listName, site,
|
|
309
|
+
writeOrder(listsDir, listName, site, order);
|
|
287
310
|
},
|
|
288
311
|
async translations(site, listName) {
|
|
289
312
|
const locales = listLocales(listsDir, listName, site);
|
|
@@ -320,5 +343,11 @@ export {
|
|
|
320
343
|
createJsonFileAdapter,
|
|
321
344
|
canonicalize,
|
|
322
345
|
hashRev,
|
|
346
|
+
applyOrder,
|
|
347
|
+
orderAfterCreate,
|
|
348
|
+
orderAfterDelete,
|
|
349
|
+
planReorder,
|
|
350
|
+
entryExistsMessage,
|
|
351
|
+
entryNotFoundMessage,
|
|
323
352
|
createJsonFileAdapterV2
|
|
324
353
|
};
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createJsonFileAdapterV2
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2NVZWZPE.js";
|
|
4
|
+
import {
|
|
5
|
+
isDraft
|
|
6
|
+
} from "./chunk-WVHTCFE6.js";
|
|
4
7
|
|
|
5
8
|
// src/loader/index.ts
|
|
6
9
|
import { join } from "path";
|
|
7
10
|
function makeId(locale, entryId) {
|
|
8
11
|
return `${locale}/${entryId}`;
|
|
9
12
|
}
|
|
10
|
-
async function syncOnce(ctx, lists, list, site) {
|
|
13
|
+
async function syncOnce(ctx, lists, list, site, schema, includeDrafts) {
|
|
11
14
|
ctx.store.clear();
|
|
12
|
-
const
|
|
15
|
+
const all = await lists.list(site, list);
|
|
16
|
+
const drafts = schema?.draftField && !includeDrafts ? all.filter((e) => isDraft(schema, e.data)) : [];
|
|
17
|
+
const draftIds = new Set(drafts.map((e) => `${e.locale}/${e.id}`));
|
|
18
|
+
const entries = draftIds.size ? all.filter((e) => !draftIds.has(`${e.locale}/${e.id}`)) : all;
|
|
13
19
|
for (const entry of entries) {
|
|
14
20
|
const id = makeId(entry.locale, entry.id);
|
|
15
21
|
const data = {
|
|
@@ -24,8 +30,9 @@ async function syncOnce(ctx, lists, list, site) {
|
|
|
24
30
|
digest: entry._rev
|
|
25
31
|
});
|
|
26
32
|
}
|
|
33
|
+
const skipped = drafts.length ? ` (${drafts.length} draft${drafts.length === 1 ? "" : "s"} skipped)` : "";
|
|
27
34
|
ctx.logger.info(
|
|
28
|
-
`cancia: loaded ${entries.length} entr${entries.length === 1 ? "y" : "ies"} from list "${list}"`
|
|
35
|
+
`cancia: loaded ${entries.length} entr${entries.length === 1 ? "y" : "ies"} from list "${list}"${skipped}`
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
function canciaLoader(opts) {
|
|
@@ -41,7 +48,7 @@ function canciaLoader(opts) {
|
|
|
41
48
|
projectRoot: opts.projectRoot ?? process.cwd()
|
|
42
49
|
});
|
|
43
50
|
const { lists } = storage;
|
|
44
|
-
await syncOnce(ctx, lists, opts.list, opts.site);
|
|
51
|
+
await syncOnce(ctx, lists, opts.list, opts.site, opts.schema, opts.includeDrafts);
|
|
45
52
|
if (ctx.watcher && !watcherAttached) {
|
|
46
53
|
watcherAttached = true;
|
|
47
54
|
const root = opts.projectRoot ?? process.cwd();
|
|
@@ -53,7 +60,14 @@ function canciaLoader(opts) {
|
|
|
53
60
|
pendingTimer = null;
|
|
54
61
|
inflight = inflight.catch(() => {
|
|
55
62
|
}).then(
|
|
56
|
-
() => syncOnce(
|
|
63
|
+
() => syncOnce(
|
|
64
|
+
latestCtx,
|
|
65
|
+
lists,
|
|
66
|
+
opts.list,
|
|
67
|
+
opts.site,
|
|
68
|
+
opts.schema,
|
|
69
|
+
opts.includeDrafts
|
|
70
|
+
).catch((err) => {
|
|
57
71
|
latestCtx.logger.error(`cancia: resync failed \u2014 ${err.message}`);
|
|
58
72
|
})
|
|
59
73
|
);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/auth-core.ts
|
|
2
|
+
var MAX_FAILURES = 5;
|
|
3
|
+
var WINDOW_MS = 6e4;
|
|
4
|
+
function createRateLimiter() {
|
|
5
|
+
return /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
function getIp(req) {
|
|
8
|
+
return req.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? req.headers.get("x-real-ip") ?? "unknown";
|
|
9
|
+
}
|
|
10
|
+
function isRateLimited(limiter, ip, now = Date.now()) {
|
|
11
|
+
const bucket = limiter.get(ip);
|
|
12
|
+
if (!bucket || now > bucket.resetAt) return false;
|
|
13
|
+
return bucket.failures >= MAX_FAILURES;
|
|
14
|
+
}
|
|
15
|
+
function recordFailure(limiter, ip, now = Date.now()) {
|
|
16
|
+
const bucket = limiter.get(ip);
|
|
17
|
+
if (!bucket || now > bucket.resetAt) {
|
|
18
|
+
limiter.set(ip, { failures: 1, resetAt: now + WINDOW_MS });
|
|
19
|
+
} else {
|
|
20
|
+
bucket.failures += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function clearFailures(limiter, ip) {
|
|
24
|
+
limiter.delete(ip);
|
|
25
|
+
}
|
|
26
|
+
var json = (body, status) => new Response(JSON.stringify(body), {
|
|
27
|
+
status,
|
|
28
|
+
headers: { "Content-Type": "application/json" }
|
|
29
|
+
});
|
|
30
|
+
async function runAuth(cfg) {
|
|
31
|
+
const { request, secret, limiter } = cfg;
|
|
32
|
+
const now = cfg.now ?? Date.now();
|
|
33
|
+
const ip = getIp(request);
|
|
34
|
+
if (isRateLimited(limiter, ip, now)) {
|
|
35
|
+
return json({ error: "Too many attempts. Try again in a minute." }, 429);
|
|
36
|
+
}
|
|
37
|
+
let token;
|
|
38
|
+
try {
|
|
39
|
+
const body = await request.json();
|
|
40
|
+
token = body?.token;
|
|
41
|
+
} catch {
|
|
42
|
+
return json({ error: "Invalid request body" }, 400);
|
|
43
|
+
}
|
|
44
|
+
if (!token || !secret || token !== secret) {
|
|
45
|
+
recordFailure(limiter, ip, now);
|
|
46
|
+
return json({ error: "Invalid token" }, 401);
|
|
47
|
+
}
|
|
48
|
+
clearFailures(limiter, ip);
|
|
49
|
+
return json({ ok: true }, 200);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
createRateLimiter,
|
|
54
|
+
runAuth
|
|
55
|
+
};
|
|
@@ -53,6 +53,56 @@ var ptBlockSchema = z.object({
|
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
var portableTextSubsetSchema = z.array(ptBlockSchema);
|
|
56
|
+
function parseRichValue(raw) {
|
|
57
|
+
if (raw == null) return [];
|
|
58
|
+
let candidate = raw;
|
|
59
|
+
if (typeof raw === "string") {
|
|
60
|
+
const trimmed = raw.trim();
|
|
61
|
+
if (trimmed === "") return [];
|
|
62
|
+
if (trimmed.startsWith("[")) {
|
|
63
|
+
try {
|
|
64
|
+
candidate = JSON.parse(trimmed);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
return [textBlock(raw)];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(candidate)) return [];
|
|
73
|
+
const sanitised = candidate.map(stripUnsafeHrefs);
|
|
74
|
+
const parsed = portableTextSubsetSchema.safeParse(sanitised);
|
|
75
|
+
return parsed.success ? parsed.data : [];
|
|
76
|
+
}
|
|
77
|
+
function serializeRichValue(value) {
|
|
78
|
+
return value.length === 0 ? "" : JSON.stringify(value);
|
|
79
|
+
}
|
|
80
|
+
function textBlock(text) {
|
|
81
|
+
return {
|
|
82
|
+
_type: "block",
|
|
83
|
+
_key: "legacy",
|
|
84
|
+
style: "normal",
|
|
85
|
+
markDefs: [],
|
|
86
|
+
children: [{ _type: "span", _key: "legacy0", text, marks: [] }]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function stripUnsafeHrefs(block) {
|
|
90
|
+
if (!block || typeof block !== "object") return block;
|
|
91
|
+
const b = block;
|
|
92
|
+
if (!Array.isArray(b.markDefs)) return block;
|
|
93
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
94
|
+
const markDefs = b.markDefs.filter((def) => {
|
|
95
|
+
const href = def?.href;
|
|
96
|
+
if (typeof href === "string" && isSafeHref(href)) return true;
|
|
97
|
+
if (typeof def?._key === "string") dropped.add(def._key);
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
if (dropped.size === 0) return block;
|
|
101
|
+
const children = Array.isArray(b.children) ? b.children.map(
|
|
102
|
+
(span) => Array.isArray(span?.marks) ? { ...span, marks: span.marks.filter((m) => !dropped.has(m)) } : span
|
|
103
|
+
) : b.children;
|
|
104
|
+
return { ...b, markDefs, children };
|
|
105
|
+
}
|
|
56
106
|
|
|
57
107
|
export {
|
|
58
108
|
PT_STYLES,
|
|
@@ -60,5 +110,7 @@ export {
|
|
|
60
110
|
PT_DECORATORS,
|
|
61
111
|
isSafeHref,
|
|
62
112
|
ptBlockSchema,
|
|
63
|
-
portableTextSubsetSchema
|
|
113
|
+
portableTextSubsetSchema,
|
|
114
|
+
parseRichValue,
|
|
115
|
+
serializeRichValue
|
|
64
116
|
};
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
createGitBackedAdapter,
|
|
7
7
|
createSqliteAdapterV2
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MXVOJRAM.js";
|
|
9
9
|
import {
|
|
10
10
|
createJsonFileAdapter,
|
|
11
11
|
createJsonFileAdapterV2
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-2NVZWZPE.js";
|
|
13
13
|
|
|
14
14
|
// src/routes/upload.ts
|
|
15
15
|
import { writeFile, mkdir } from "fs/promises";
|
|
@@ -2,8 +2,14 @@ import {
|
|
|
2
2
|
createGitHubClient
|
|
3
3
|
} from "./chunk-U7V53JX7.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
applyOrder,
|
|
6
|
+
entryExistsMessage,
|
|
7
|
+
entryNotFoundMessage,
|
|
8
|
+
hashRev,
|
|
9
|
+
orderAfterCreate,
|
|
10
|
+
orderAfterDelete,
|
|
11
|
+
planReorder
|
|
12
|
+
} from "./chunk-2NVZWZPE.js";
|
|
7
13
|
import {
|
|
8
14
|
RevConflictError
|
|
9
15
|
} from "./chunk-7IA5B5CF.js";
|
|
@@ -119,18 +125,6 @@ function rowToEntry(row) {
|
|
|
119
125
|
_rev: hashRev(data)
|
|
120
126
|
};
|
|
121
127
|
}
|
|
122
|
-
function applyOrder(order, ids) {
|
|
123
|
-
const result = [];
|
|
124
|
-
const seen = /* @__PURE__ */ new Set();
|
|
125
|
-
for (const id of order) {
|
|
126
|
-
if (ids.has(id)) {
|
|
127
|
-
result.push(id);
|
|
128
|
-
seen.add(id);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
const extras = [...ids].filter((id) => !seen.has(id)).sort();
|
|
132
|
-
return [...result, ...extras];
|
|
133
|
-
}
|
|
134
128
|
function makeListStore(db) {
|
|
135
129
|
const getOrderStmt = db.prepare("SELECT ids FROM list_order WHERE site=? AND list=?");
|
|
136
130
|
const upsertOrderStmt = db.prepare(
|
|
@@ -178,32 +172,25 @@ function makeListStore(db) {
|
|
|
178
172
|
(site, listName, id, locale, dataJson, now) => {
|
|
179
173
|
insertEntryStmt.run(site, listName, id, locale, dataJson, now, now);
|
|
180
174
|
const order = readOrder(site, listName);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
175
|
+
const next = orderAfterCreate(order, id);
|
|
176
|
+
if (next !== order) writeOrder(site, listName, next);
|
|
184
177
|
}
|
|
185
178
|
);
|
|
186
179
|
const deleteTx = db.transaction((site, listName, id, locale) => {
|
|
187
180
|
deleteEntryStmt.run(site, listName, id, locale);
|
|
188
181
|
const stillExists = distinctIds(site, listName).has(id);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (next.length !== order.length) {
|
|
193
|
-
writeOrder(site, listName, next);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
182
|
+
const order = readOrder(site, listName);
|
|
183
|
+
const next = orderAfterDelete(order, id, stillExists);
|
|
184
|
+
if (next !== order) writeOrder(site, listName, next);
|
|
196
185
|
});
|
|
197
|
-
const reorderTx = db.transaction(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for (const id of all) {
|
|
201
|
-
if (!supplied.has(id)) {
|
|
186
|
+
const reorderTx = db.transaction(
|
|
187
|
+
(site, listName, order, dropped) => {
|
|
188
|
+
for (const id of dropped) {
|
|
202
189
|
deleteEntryAllLocalesStmt.run(site, listName, id);
|
|
203
190
|
}
|
|
191
|
+
writeOrder(site, listName, order);
|
|
204
192
|
}
|
|
205
|
-
|
|
206
|
-
});
|
|
193
|
+
);
|
|
207
194
|
return {
|
|
208
195
|
async list(site, listName, locale) {
|
|
209
196
|
const order = readOrder(site, listName);
|
|
@@ -244,7 +231,7 @@ function makeListStore(db) {
|
|
|
244
231
|
const finalId = id ?? randomUUID();
|
|
245
232
|
const existing = getEntryStmt.get(site, listName, finalId, locale);
|
|
246
233
|
if (existing) {
|
|
247
|
-
throw new Error(
|
|
234
|
+
throw new Error(entryExistsMessage(finalId, listName, locale));
|
|
248
235
|
}
|
|
249
236
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
250
237
|
createTx(site, listName, finalId, locale, JSON.stringify(data), now);
|
|
@@ -260,7 +247,7 @@ function makeListStore(db) {
|
|
|
260
247
|
async update(site, listName, id, locale, data, rev) {
|
|
261
248
|
const existing = getEntryStmt.get(site, listName, id, locale);
|
|
262
249
|
if (!existing) {
|
|
263
|
-
throw new Error(
|
|
250
|
+
throw new Error(entryNotFoundMessage(id, listName, locale));
|
|
264
251
|
}
|
|
265
252
|
const existingData = JSON.parse(existing.data);
|
|
266
253
|
if (hashRev(existingData) !== rev) throw new RevConflictError();
|
|
@@ -282,12 +269,8 @@ function makeListStore(db) {
|
|
|
282
269
|
},
|
|
283
270
|
async reorder(site, listName, ids) {
|
|
284
271
|
const all = distinctIds(site, listName);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new Error(`Cannot reorder: entry "${id}" not found in "${listName}"`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
reorderTx(site, listName, ids);
|
|
272
|
+
const { order, dropped } = planReorder(ids, all, listName);
|
|
273
|
+
reorderTx(site, listName, order, dropped);
|
|
291
274
|
},
|
|
292
275
|
async translations(site, listName) {
|
|
293
276
|
const rows = allEntriesStmt.all(site, listName);
|
|
@@ -32,8 +32,15 @@ async function loadListSchema(projectRoot, listName, overridePath) {
|
|
|
32
32
|
const schemas = await loadSchemas(projectRoot, overridePath);
|
|
33
33
|
return schemas[listName] ?? null;
|
|
34
34
|
}
|
|
35
|
+
function clearSchemaCache() {
|
|
36
|
+
_cache = null;
|
|
37
|
+
_cachedPath = null;
|
|
38
|
+
_cachedMtime = 0;
|
|
39
|
+
}
|
|
35
40
|
|
|
36
41
|
export {
|
|
42
|
+
resolveSchemasPath,
|
|
37
43
|
loadSchemas,
|
|
38
|
-
loadListSchema
|
|
44
|
+
loadListSchema,
|
|
45
|
+
clearSchemaCache
|
|
39
46
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isSafeHref,
|
|
3
3
|
portableTextSubsetSchema
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KHCFVVYV.js";
|
|
5
5
|
|
|
6
6
|
// src/schema/index.ts
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
function isDraft(schema, data) {
|
|
9
|
+
if (!schema.draftField) return false;
|
|
10
|
+
return data[schema.draftField] === true;
|
|
11
|
+
}
|
|
8
12
|
function defineList(opts) {
|
|
9
13
|
return {
|
|
10
14
|
label: opts.label,
|
|
@@ -12,6 +16,7 @@ function defineList(opts) {
|
|
|
12
16
|
titleField: opts.titleField,
|
|
13
17
|
bodyField: opts.bodyField,
|
|
14
18
|
slugField: opts.slugField,
|
|
19
|
+
draftField: opts.draftField,
|
|
15
20
|
fields: opts.fields,
|
|
16
21
|
validator: z.object(opts.fields)
|
|
17
22
|
};
|
|
@@ -23,6 +28,15 @@ var defineField = {
|
|
|
23
28
|
email: (o) => z.string().email().meta({ widget: "email", ...o }),
|
|
24
29
|
datetime: (o) => z.string().datetime().meta({ widget: "datetime", ...o }),
|
|
25
30
|
checkbox: (o) => z.boolean().meta({ widget: "checkbox", ...o }),
|
|
31
|
+
/**
|
|
32
|
+
* The draft flag for a list's `draftField`. A checkbox that DEFAULTS to
|
|
33
|
+
* false, which is what makes it safe to add to an existing list: every
|
|
34
|
+
* stored entry that predates the field validates and reads as published
|
|
35
|
+
* rather than vanishing from the built site.
|
|
36
|
+
*
|
|
37
|
+
* Label defaults to "Draft" so the affordance reads the same on every list.
|
|
38
|
+
*/
|
|
39
|
+
draft: (o) => z.boolean().default(false).meta({ widget: "checkbox", label: "Draft", ...o }),
|
|
26
40
|
number: (o) => {
|
|
27
41
|
let n = z.number();
|
|
28
42
|
if (o?.min != null) n = n.min(o.min);
|
|
@@ -167,6 +181,7 @@ function describeList(name, schema) {
|
|
|
167
181
|
titleField: schema.titleField,
|
|
168
182
|
bodyField: schema.bodyField,
|
|
169
183
|
slugField: schema.slugField,
|
|
184
|
+
draftField: schema.draftField,
|
|
170
185
|
fields
|
|
171
186
|
};
|
|
172
187
|
}
|
|
@@ -202,6 +217,7 @@ function isExternalHref(href) {
|
|
|
202
217
|
|
|
203
218
|
export {
|
|
204
219
|
z,
|
|
220
|
+
isDraft,
|
|
205
221
|
defineList,
|
|
206
222
|
defineField,
|
|
207
223
|
slugify,
|
package/dist/content.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ interface LinkValue {
|
|
|
9
9
|
}
|
|
10
10
|
/** Resolve a link field, falling back to the values authored in the page. */
|
|
11
11
|
type TranslateLink = (key: string, fallback: LinkValue) => LinkValue;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a rich-text region (plan 051).
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` when there is NO stored override — which the region renders as
|
|
16
|
+
* "show the markup authored in the page". That is deliberate and load-bearing:
|
|
17
|
+
* a rich fallback cannot be a JS string, so the authored markup IS the fallback,
|
|
18
|
+
* which is what keeps an empty DB rendering byte-identically.
|
|
19
|
+
*
|
|
20
|
+
* `[]` is a different answer from `null`: it means a stored, deliberately empty
|
|
21
|
+
* value — the client removed the prose on purpose — and renders nothing.
|
|
22
|
+
*/
|
|
23
|
+
type TranslateRich = (key: string) => RichValue | null;
|
|
24
|
+
/** A Portable-Text subset document. Structurally typed to keep this subpath Vite-free. */
|
|
25
|
+
type RichValue = Record<string, unknown>[];
|
|
12
26
|
interface ContentReaderOptions {
|
|
13
27
|
/** Site id (the `?site=` query param and the storage `site` column). */
|
|
14
28
|
site: string;
|
|
@@ -57,10 +71,18 @@ interface ContentReader {
|
|
|
57
71
|
* authored in the page. Never throws; drops an unsafe href.
|
|
58
72
|
*/
|
|
59
73
|
makeTLink(cms: ContentMap): TranslateLink;
|
|
60
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Build a rich-text resolver over a content map. Returns `null` when no
|
|
76
|
+
* override is stored, which a rich region renders as "show the markup
|
|
77
|
+
* authored in the page" — the reason an empty DB still renders identically.
|
|
78
|
+
* `[]` means a stored, deliberately empty value and renders nothing.
|
|
79
|
+
*/
|
|
80
|
+
makeRich(cms: ContentMap): TranslateRich;
|
|
81
|
+
/** Sugar: `const { t, tLink, tRich, cms } = await reader.get()` in frontmatter. */
|
|
61
82
|
get(): Promise<{
|
|
62
83
|
t: Translate;
|
|
63
84
|
tLink: TranslateLink;
|
|
85
|
+
tRich: TranslateRich;
|
|
64
86
|
cms: ContentMap;
|
|
65
87
|
}>;
|
|
66
88
|
}
|
|
@@ -71,4 +93,4 @@ interface ContentReader {
|
|
|
71
93
|
*/
|
|
72
94
|
declare function createContentReader(opts: ContentReaderOptions): ContentReader;
|
|
73
95
|
|
|
74
|
-
export { type ContentMap, type ContentReader, type ContentReaderOptions, type LinkValue, type Translate, type TranslateLink, createContentReader };
|
|
96
|
+
export { type ContentMap, type ContentReader, type ContentReaderOptions, type LinkValue, type RichValue, type Translate, type TranslateLink, type TranslateRich, createContentReader };
|
package/dist/content.js
CHANGED
|
@@ -2,9 +2,9 @@ import "./chunk-FOSOWSXV.js";
|
|
|
2
2
|
import "./chunk-CJDIVWO3.js";
|
|
3
3
|
import {
|
|
4
4
|
createSqliteAdapterV2
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-MXVOJRAM.js";
|
|
6
6
|
import "./chunk-U7V53JX7.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-2NVZWZPE.js";
|
|
8
8
|
import "./chunk-7IA5B5CF.js";
|
|
9
9
|
|
|
10
10
|
// src/content.ts
|
|
@@ -37,6 +37,46 @@ function parseLink(raw, fallback) {
|
|
|
37
37
|
}
|
|
38
38
|
return { label: raw, href: fallback.href };
|
|
39
39
|
}
|
|
40
|
+
function parseRich(raw) {
|
|
41
|
+
if (raw === void 0) return null;
|
|
42
|
+
const trimmed = raw.trim();
|
|
43
|
+
if (trimmed === "") return [];
|
|
44
|
+
if (!trimmed.startsWith("[")) {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
_type: "block",
|
|
48
|
+
_key: "legacy",
|
|
49
|
+
style: "normal",
|
|
50
|
+
markDefs: [],
|
|
51
|
+
children: [{ _type: "span", _key: "legacy0", text: raw, marks: [] }]
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(trimmed);
|
|
57
|
+
if (!Array.isArray(parsed)) return [];
|
|
58
|
+
return parsed.map(stripUnsafeMarkDefs);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function stripUnsafeMarkDefs(block) {
|
|
64
|
+
if (!block || typeof block !== "object") return block;
|
|
65
|
+
const b = block;
|
|
66
|
+
if (!Array.isArray(b.markDefs)) return block;
|
|
67
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
68
|
+
const markDefs = b.markDefs.filter((def) => {
|
|
69
|
+
const href = def?.href;
|
|
70
|
+
if (typeof href === "string" && isSafeHref(href)) return true;
|
|
71
|
+
if (typeof def?._key === "string") dropped.add(def._key);
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
if (dropped.size === 0) return block;
|
|
75
|
+
const children = Array.isArray(b.children) ? b.children.map(
|
|
76
|
+
(span) => Array.isArray(span?.marks) ? { ...span, marks: span.marks.filter((m) => !dropped.has(m)) } : span
|
|
77
|
+
) : b.children;
|
|
78
|
+
return { ...b, markDefs, children };
|
|
79
|
+
}
|
|
40
80
|
function createContentReader(opts) {
|
|
41
81
|
const site = opts.site;
|
|
42
82
|
const lang = opts.lang ?? "en";
|
|
@@ -73,11 +113,14 @@ function createContentReader(opts) {
|
|
|
73
113
|
function makeTLink(cms) {
|
|
74
114
|
return (key, fallback) => parseLink(cms[`${key}.${lang}`], fallback);
|
|
75
115
|
}
|
|
116
|
+
function makeRich(cms) {
|
|
117
|
+
return (key) => parseRich(cms[`${key}.${lang}`]);
|
|
118
|
+
}
|
|
76
119
|
async function get() {
|
|
77
120
|
const cms = await getCMS();
|
|
78
|
-
return { t: makeT(cms), tLink: makeTLink(cms), cms };
|
|
121
|
+
return { t: makeT(cms), tLink: makeTLink(cms), tRich: makeRich(cms), cms };
|
|
79
122
|
}
|
|
80
|
-
return { getCMS, makeT, makeTLink, get };
|
|
123
|
+
return { getCMS, makeT, makeTLink, makeRich, get };
|
|
81
124
|
}
|
|
82
125
|
export {
|
|
83
126
|
createContentReader
|