@thebes/cadmus 0.3.0 → 0.4.1
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/cms/index.cjs +231 -0
- package/dist/cms/index.cjs.map +1 -1
- package/dist/cms/index.d.cts +2 -2
- package/dist/cms/index.d.ts +2 -2
- package/dist/cms/index.js +221 -1
- package/dist/cms/index.js.map +1 -1
- package/dist/hono/index.d.cts +1 -1
- package/dist/hono/index.d.ts +1 -1
- package/dist/{index-BRZrCTsN.d.cts → index-CWrSoiPh.d.cts} +234 -2
- package/dist/index-CWrSoiPh.d.cts.map +1 -0
- package/dist/{index-BRZrCTsN.d.ts → index-CWrSoiPh.d.ts} +234 -2
- package/dist/index-CWrSoiPh.d.ts.map +1 -0
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/storage/index.cjs.map +1 -1
- package/dist/storage/index.d.cts +31 -2
- package/dist/storage/index.d.cts.map +1 -1
- package/dist/storage/index.d.ts +31 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/index-BRZrCTsN.d.cts.map +0 -1
- package/dist/index-BRZrCTsN.d.ts.map +0 -1
package/dist/cms/index.cjs
CHANGED
|
@@ -307,6 +307,83 @@ function defineCmsConfig(config) {
|
|
|
307
307
|
return resolved;
|
|
308
308
|
}
|
|
309
309
|
//#endregion
|
|
310
|
+
//#region src/cms/patch.ts
|
|
311
|
+
function deepEqual(a, b) {
|
|
312
|
+
if (a === b) return true;
|
|
313
|
+
if (a === null || b === null) return false;
|
|
314
|
+
if (typeof a !== typeof b) return false;
|
|
315
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
316
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
317
|
+
if (a.length !== b.length) return false;
|
|
318
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
319
|
+
}
|
|
320
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
321
|
+
const ak = Object.keys(a);
|
|
322
|
+
const bk = Object.keys(b);
|
|
323
|
+
if (ak.length !== bk.length) return false;
|
|
324
|
+
return ak.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
|
|
325
|
+
}
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Field-level diff between two document snapshots — the per-field
|
|
330
|
+
* added/removed/changed list a version-history UI renders. Values are
|
|
331
|
+
* compared structurally (deep-equal), so a field only shows as `changed`
|
|
332
|
+
* when its content actually differs.
|
|
333
|
+
*/
|
|
334
|
+
function diffDocuments(before, after, options = {}) {
|
|
335
|
+
const ignore = new Set(options.ignore ?? []);
|
|
336
|
+
const keys = options.fields ? options.fields : [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
|
337
|
+
const changes = [];
|
|
338
|
+
for (const path of keys) {
|
|
339
|
+
if (ignore.has(path)) continue;
|
|
340
|
+
const inBefore = Object.hasOwn(before, path);
|
|
341
|
+
const inAfter = Object.hasOwn(after, path);
|
|
342
|
+
if (inBefore && !inAfter) changes.push({
|
|
343
|
+
path,
|
|
344
|
+
kind: "removed",
|
|
345
|
+
before: before[path]
|
|
346
|
+
});
|
|
347
|
+
else if (!inBefore && inAfter) changes.push({
|
|
348
|
+
path,
|
|
349
|
+
kind: "added",
|
|
350
|
+
after: after[path]
|
|
351
|
+
});
|
|
352
|
+
else if (inBefore && inAfter && !deepEqual(before[path], after[path])) changes.push({
|
|
353
|
+
path,
|
|
354
|
+
kind: "changed",
|
|
355
|
+
before: before[path],
|
|
356
|
+
after: after[path]
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return changes;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The {@link Patch} that transforms `before` into `after`: `set` for each
|
|
363
|
+
* added/changed field, `unset` for each removed field. `applyPatch(before,
|
|
364
|
+
* computePatch(before, after))` deep-equals `after`.
|
|
365
|
+
*/
|
|
366
|
+
function computePatch(before, after) {
|
|
367
|
+
return diffDocuments(before, after).map((change) => change.kind === "removed" ? {
|
|
368
|
+
op: "unset",
|
|
369
|
+
path: change.path
|
|
370
|
+
} : {
|
|
371
|
+
op: "set",
|
|
372
|
+
path: change.path,
|
|
373
|
+
value: change.after
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Apply a {@link Patch} to a document, returning a new document (the input is
|
|
378
|
+
* never mutated). Unknown ops are ignored defensively.
|
|
379
|
+
*/
|
|
380
|
+
function applyPatch(doc, patch) {
|
|
381
|
+
const next = { ...doc };
|
|
382
|
+
for (const op of patch) if (op.op === "set") next[op.path] = op.value;
|
|
383
|
+
else if (op.op === "unset") delete next[op.path];
|
|
384
|
+
return next;
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
310
387
|
//#region src/cms/validation.ts
|
|
311
388
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
312
389
|
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -895,6 +972,21 @@ function createVersionedLocalApi(db, table, versionsTable, config, registry) {
|
|
|
895
972
|
const [row] = await db.update(table).set({ publishedVersionId: null }).where((0, drizzle_orm.eq)(idColumn, id)).returning();
|
|
896
973
|
if (!row) notFound(config, id);
|
|
897
974
|
return row;
|
|
975
|
+
},
|
|
976
|
+
async diffVersions(context, fromVersionId, toVersionId) {
|
|
977
|
+
await checkAccess(config, "read", context);
|
|
978
|
+
const rows = await db.select().from(versionsTable).where((0, drizzle_orm.inArray)(versionsIdColumn, [fromVersionId, toVersionId]));
|
|
979
|
+
const byId = new Map(rows.map((r) => [r.id, r.versionData]));
|
|
980
|
+
const before = byId.get(fromVersionId);
|
|
981
|
+
const after = byId.get(toVersionId);
|
|
982
|
+
if (!before) notFoundVersion(config, fromVersionId);
|
|
983
|
+
if (!after) notFoundVersion(config, toVersionId);
|
|
984
|
+
return diffDocuments(before, after, { ignore: [
|
|
985
|
+
"id",
|
|
986
|
+
"createdAt",
|
|
987
|
+
"status",
|
|
988
|
+
"publishedVersionId"
|
|
989
|
+
] });
|
|
898
990
|
}
|
|
899
991
|
};
|
|
900
992
|
}
|
|
@@ -908,6 +1000,50 @@ function getCollectionsMeta(config) {
|
|
|
908
1000
|
}));
|
|
909
1001
|
}
|
|
910
1002
|
//#endregion
|
|
1003
|
+
//#region src/cms/migrate.ts
|
|
1004
|
+
/** Identity helper — gives a migration definition its type + a greppable call site. */
|
|
1005
|
+
function defineMigration(migration) {
|
|
1006
|
+
return migration;
|
|
1007
|
+
}
|
|
1008
|
+
function patchToUpdate(patch) {
|
|
1009
|
+
const values = {};
|
|
1010
|
+
for (const op of patch) values[op.path] = op.op === "set" ? op.value : null;
|
|
1011
|
+
return values;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Run a migration over every document in a collection. Reads all documents
|
|
1015
|
+
* through `api.find`, applies `migration.document`, and (unless `dryRun`)
|
|
1016
|
+
* writes the resulting patch through `api.update`. Returns a report of what
|
|
1017
|
+
* changed — run it `dryRun` first, then apply.
|
|
1018
|
+
*/
|
|
1019
|
+
async function runMigration(migration, options) {
|
|
1020
|
+
const { api, context, dryRun = false } = options;
|
|
1021
|
+
const rows = await api.find(context);
|
|
1022
|
+
const changes = [];
|
|
1023
|
+
const errors = [];
|
|
1024
|
+
let changed = 0;
|
|
1025
|
+
for (const before of rows) try {
|
|
1026
|
+
const patch = computePatch(before, await migration.document(before) ?? before);
|
|
1027
|
+
if (patch.length === 0) continue;
|
|
1028
|
+
changes.push({
|
|
1029
|
+
id: before.id,
|
|
1030
|
+
patch
|
|
1031
|
+
});
|
|
1032
|
+
changed++;
|
|
1033
|
+
if (!dryRun) await api.update(context, before.id, patchToUpdate(patch));
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
errors.push(`document ${before.id}: ${String(err)}`);
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
migration: migration.name,
|
|
1039
|
+
dryRun,
|
|
1040
|
+
scanned: rows.length,
|
|
1041
|
+
changed,
|
|
1042
|
+
changes,
|
|
1043
|
+
errors
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
911
1047
|
//#region src/cms/schema-gen.ts
|
|
912
1048
|
function toSnakeCase(value) {
|
|
913
1049
|
return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
@@ -1106,6 +1242,90 @@ function buildStudioStructure(config, options = {}) {
|
|
|
1106
1242
|
}));
|
|
1107
1243
|
}
|
|
1108
1244
|
//#endregion
|
|
1245
|
+
//#region src/cms/visual-editing.ts
|
|
1246
|
+
/** The data attribute editable regions are tagged with. */
|
|
1247
|
+
const EDIT_ATTR = "data-cadmus-edit";
|
|
1248
|
+
/** `postMessage` payload type for a click-to-edit selection. */
|
|
1249
|
+
const VISUAL_EDIT_MESSAGE = "cadmus:visual-edit";
|
|
1250
|
+
function encodeEditRef(ref) {
|
|
1251
|
+
return `${ref.collection}:${ref.id}:${ref.field}`;
|
|
1252
|
+
}
|
|
1253
|
+
/** Parse an {@link EditRef} string, or null if malformed. */
|
|
1254
|
+
function decodeEditRef(value) {
|
|
1255
|
+
const parts = value.split(":");
|
|
1256
|
+
if (parts.length !== 3) return null;
|
|
1257
|
+
const [collection, idRaw, field] = parts;
|
|
1258
|
+
const id = Number.parseInt(idRaw, 10);
|
|
1259
|
+
if (!collection || !field || !Number.isFinite(id)) return null;
|
|
1260
|
+
return {
|
|
1261
|
+
collection,
|
|
1262
|
+
id,
|
|
1263
|
+
field
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Attribute object to spread onto a rendered element so the overlay can map
|
|
1268
|
+
* it back to its source field, e.g. `<h1 {...editAttr({collection:'pages',
|
|
1269
|
+
* id, field:'title'})}>`.
|
|
1270
|
+
*/
|
|
1271
|
+
function editAttr(ref) {
|
|
1272
|
+
return { [EDIT_ATTR]: encodeEditRef(ref) };
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Mount the click-to-edit overlay. Browser-only — call from a preview page's
|
|
1276
|
+
* client script. Highlights `[data-cadmus-edit]` elements on hover and, on
|
|
1277
|
+
* click, calls `onSelect` and posts a {@link VisualEditingMessage} to the
|
|
1278
|
+
* parent window. Returns a cleanup function that removes the listeners.
|
|
1279
|
+
*/
|
|
1280
|
+
function mountVisualEditing(options = {}) {
|
|
1281
|
+
const { onSelect, targetOrigin = "*", highlightColor = "#56c6be" } = options;
|
|
1282
|
+
const closest = (target) => {
|
|
1283
|
+
if (!(target instanceof Element)) return null;
|
|
1284
|
+
const el = target.closest(`[${EDIT_ATTR}]`);
|
|
1285
|
+
return el instanceof HTMLElement ? el : null;
|
|
1286
|
+
};
|
|
1287
|
+
let previous = null;
|
|
1288
|
+
const clearHighlight = () => {
|
|
1289
|
+
if (previous) {
|
|
1290
|
+
previous.el.style.outline = previous.outline;
|
|
1291
|
+
previous = null;
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
const onOver = (event) => {
|
|
1295
|
+
const el = closest(event.target);
|
|
1296
|
+
if (!el || el === previous?.el) return;
|
|
1297
|
+
clearHighlight();
|
|
1298
|
+
previous = {
|
|
1299
|
+
el,
|
|
1300
|
+
outline: el.style.outline
|
|
1301
|
+
};
|
|
1302
|
+
el.style.outline = `2px solid ${highlightColor}`;
|
|
1303
|
+
el.style.outlineOffset = "2px";
|
|
1304
|
+
el.style.cursor = "pointer";
|
|
1305
|
+
};
|
|
1306
|
+
const onClick = (event) => {
|
|
1307
|
+
const el = closest(event.target);
|
|
1308
|
+
if (!el) return;
|
|
1309
|
+
const ref = decodeEditRef(el.getAttribute("data-cadmus-edit") ?? "");
|
|
1310
|
+
if (!ref) return;
|
|
1311
|
+
event.preventDefault();
|
|
1312
|
+
event.stopPropagation();
|
|
1313
|
+
onSelect?.(ref, el);
|
|
1314
|
+
const message = {
|
|
1315
|
+
type: VISUAL_EDIT_MESSAGE,
|
|
1316
|
+
ref
|
|
1317
|
+
};
|
|
1318
|
+
window.parent?.postMessage(message, targetOrigin);
|
|
1319
|
+
};
|
|
1320
|
+
document.addEventListener("mouseover", onOver, true);
|
|
1321
|
+
document.addEventListener("click", onClick, true);
|
|
1322
|
+
return () => {
|
|
1323
|
+
clearHighlight();
|
|
1324
|
+
document.removeEventListener("mouseover", onOver, true);
|
|
1325
|
+
document.removeEventListener("click", onClick, true);
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1109
1329
|
//#region src/cms/webhooks.ts
|
|
1110
1330
|
/**
|
|
1111
1331
|
* Builds an `afterChange` hook that enqueues a `WebhookMessage` for every
|
|
@@ -1186,7 +1406,10 @@ async function deliverWebhookMessage(message) {
|
|
|
1186
1406
|
}
|
|
1187
1407
|
//#endregion
|
|
1188
1408
|
exports.DEFAULT_STUDIO_GROUP = DEFAULT_STUDIO_GROUP;
|
|
1409
|
+
exports.EDIT_ATTR = EDIT_ATTR;
|
|
1189
1410
|
exports.Rule = Rule;
|
|
1411
|
+
exports.VISUAL_EDIT_MESSAGE = VISUAL_EDIT_MESSAGE;
|
|
1412
|
+
exports.applyPatch = applyPatch;
|
|
1190
1413
|
exports.assertValid = assertValid;
|
|
1191
1414
|
exports.buildStudioStructure = buildStudioStructure;
|
|
1192
1415
|
exports.can = can;
|
|
@@ -1195,24 +1418,32 @@ exports.collectionSearchTableName = collectionSearchTableName;
|
|
|
1195
1418
|
exports.collectionSearchTableSQL = collectionSearchTableSQL;
|
|
1196
1419
|
exports.collectionToTable = collectionToTable;
|
|
1197
1420
|
exports.collectionVersionsTable = collectionVersionsTable;
|
|
1421
|
+
exports.computePatch = computePatch;
|
|
1198
1422
|
exports.createBlockRegistry = createBlockRegistry;
|
|
1199
1423
|
exports.createLocalApi = createLocalApi;
|
|
1200
1424
|
exports.createVersionedLocalApi = createVersionedLocalApi;
|
|
1201
1425
|
exports.createWebhookHook = createWebhookHook;
|
|
1426
|
+
exports.decodeEditRef = decodeEditRef;
|
|
1202
1427
|
exports.defineCmsConfig = defineCmsConfig;
|
|
1203
1428
|
exports.defineCollection = defineCollection;
|
|
1204
1429
|
exports.defineField = defineField;
|
|
1430
|
+
exports.defineMigration = defineMigration;
|
|
1205
1431
|
exports.deliverWebhookMessage = deliverWebhookMessage;
|
|
1432
|
+
exports.diffDocuments = diffDocuments;
|
|
1433
|
+
exports.editAttr = editAttr;
|
|
1434
|
+
exports.encodeEditRef = encodeEditRef;
|
|
1206
1435
|
exports.extractSearchText = extractSearchText;
|
|
1207
1436
|
exports.flattenDoc = flattenDoc;
|
|
1208
1437
|
exports.flattenFields = flattenFields;
|
|
1209
1438
|
exports.generateSchemaSource = generateSchemaSource;
|
|
1210
1439
|
exports.getCollectionsMeta = getCollectionsMeta;
|
|
1211
1440
|
exports.getRegisteredApi = getRegisteredApi;
|
|
1441
|
+
exports.mountVisualEditing = mountVisualEditing;
|
|
1212
1442
|
exports.nestDoc = nestDoc;
|
|
1213
1443
|
exports.relationshipJoinTables = relationshipJoinTables;
|
|
1214
1444
|
exports.renderBlocksToString = renderBlocksToString;
|
|
1215
1445
|
exports.rule = rule;
|
|
1446
|
+
exports.runMigration = runMigration;
|
|
1216
1447
|
exports.validateDocument = validateDocument;
|
|
1217
1448
|
|
|
1218
1449
|
//# sourceMappingURL=index.cjs.map
|