smoodly 0.0.27 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -3
- package/dist/admin/client-ops.js +2 -0
- package/dist/admin/drift-copy.d.ts +19 -0
- package/dist/admin/drift-copy.js +89 -0
- package/dist/admin/drift.d.ts +76 -0
- package/dist/admin/drift.js +264 -0
- package/dist/admin/editor/EditorView.js +70 -15
- package/dist/admin/editor/InlineRichtext.js +3 -1
- package/dist/admin/editor/Outline.d.ts +7 -0
- package/dist/admin/editor/Outline.js +8 -5
- package/dist/admin/editor/PageInspector.d.ts +9 -1
- package/dist/admin/editor/PageInspector.js +4 -2
- package/dist/admin/editor/PageSettings.d.ts +4 -1
- package/dist/admin/editor/PageSettings.js +33 -15
- package/dist/admin/editor/UnknownNode.d.ts +7 -0
- package/dist/admin/editor/UnknownNode.js +8 -0
- package/dist/admin/field-paths.d.ts +3 -0
- package/dist/admin/field-paths.js +26 -0
- package/dist/admin/forms/OrphanFields.d.ts +8 -0
- package/dist/admin/forms/OrphanFields.js +11 -0
- package/dist/admin/forms/RefPicker.js +6 -2
- package/dist/admin/forms/RichtextElementCard.js +9 -3
- package/dist/admin/forms/RichtextWidget.js +3 -1
- package/dist/admin/forms/TagsField.js +1 -1
- package/dist/admin/forms/richtext-form-context.d.ts +8 -0
- package/dist/admin/forms/richtext-form-context.js +5 -0
- package/dist/admin/health.d.ts +23 -0
- package/dist/admin/health.js +42 -0
- package/dist/admin/ops-impl.js +124 -14
- package/dist/admin/ops.d.ts +53 -1
- package/dist/admin/redirect-path.d.ts +6 -0
- package/dist/admin/redirect-path.js +33 -0
- package/dist/admin/shell/AddRedirectModal.d.ts +11 -0
- package/dist/admin/shell/AddRedirectModal.js +29 -0
- package/dist/admin/shell/AdminApp.js +2 -3
- package/dist/admin/shell/ContentHealthPage.d.ts +10 -0
- package/dist/admin/shell/ContentHealthPage.js +37 -0
- package/dist/admin/shell/EntriesList.js +4 -4
- package/dist/admin/shell/EntryForm.js +32 -7
- package/dist/admin/shell/ErrorPane.d.ts +4 -2
- package/dist/admin/shell/ErrorPane.js +5 -4
- package/dist/admin/shell/GlobalForm.js +22 -2
- package/dist/admin/shell/IconRail.d.ts +1 -1
- package/dist/admin/shell/IconRail.js +3 -2
- package/dist/admin/shell/PagesList.js +9 -7
- package/dist/admin/shell/RedirectsPage.d.ts +11 -0
- package/dist/admin/shell/RedirectsPage.js +75 -0
- package/dist/admin/shell/SettingsView.d.ts +4 -2
- package/dist/admin/shell/SettingsView.js +22 -14
- package/dist/admin/shell/health-list.d.ts +27 -0
- package/dist/admin/shell/health-list.js +47 -0
- package/dist/admin/shell/live-url.d.ts +10 -2
- package/dist/admin/shell/live-url.js +9 -4
- package/dist/admin/shell/members-list.js +1 -1
- package/dist/admin/shell/redirects-list.d.ts +12 -0
- package/dist/admin/shell/redirects-list.js +15 -0
- package/dist/admin/shell/settings.d.ts +16 -3
- package/dist/admin/shell/settings.js +16 -9
- package/dist/admin/tree-ops.d.ts +4 -0
- package/dist/admin/tree-ops.js +15 -1
- package/dist/admin/ui/Combobox.d.ts +8 -2
- package/dist/admin/ui/Combobox.js +48 -16
- package/dist/admin/ui/ConfirmDialog.d.ts +1 -1
- package/dist/admin/ui/ConfirmDialog.js +5 -4
- package/dist/admin/ui/DriftNotice.d.ts +6 -0
- package/dist/admin/ui/DriftNotice.js +10 -0
- package/dist/admin/ui/combo-box.d.ts +13 -0
- package/dist/admin/ui/combo-box.js +8 -0
- package/dist/admin/ui/confirm.d.ts +21 -2
- package/dist/admin/ui/confirm.js +26 -9
- package/dist/admin/ui/theme.js +32 -3
- package/dist/admin/validate.js +15 -47
- package/dist/entry-store.d.ts +31 -6
- package/dist/entry-store.js +39 -11
- package/dist/index.d.ts +2 -2
- package/dist/next/page-renderer.js +28 -9
- package/dist/path-index.d.ts +28 -4
- package/dist/path-index.js +37 -4
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +9 -0
- package/dist/render.js +7 -2
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/site.d.ts +4 -1
- package/dist/site.js +3 -1
- package/dist/sql.js +19 -1
- package/dist/store.d.ts +39 -5
- package/dist/store.js +32 -8
- package/dist/supabase-entry-store.d.ts +9 -3
- package/dist/supabase-entry-store.js +39 -13
- package/dist/supabase-path-index.d.ts +10 -3
- package/dist/supabase-path-index.js +50 -17
- package/dist/supabase-store.d.ts +15 -3
- package/dist/supabase-store.js +42 -8
- package/migrations/20260926150000_smoodly_v4.sql +41 -0
- package/package.json +1 -1
package/dist/admin/ui/theme.js
CHANGED
|
@@ -671,6 +671,13 @@ body:has(.sm-admin){margin:0}
|
|
|
671
671
|
.sm-cell--title{font-weight:500}
|
|
672
672
|
.sm-cell--grip{overflow:visible;display:flex;align-items:center;margin-left:-4px}
|
|
673
673
|
.sm-empty{padding:28px 8px;color:var(--text-muted)}
|
|
674
|
+
/* Settings › Content health (spec 2026-09-27 editor safety §4): headed sections of grouped rows. */
|
|
675
|
+
.sm-health section+section{margin-top:24px}
|
|
676
|
+
.sm-health__section{font-size:var(--text-sm);font-weight:600;letter-spacing:var(--caps-tracking);text-transform:var(--caps-transform);color:var(--text-muted);margin:0 0 8px;padding:0 8px}
|
|
677
|
+
.sm-health__group{display:flex;flex-direction:column;gap:2px;margin-bottom:16px}
|
|
678
|
+
.sm-health__group > .sm-btn{align-self:flex-start;margin-top:4px}
|
|
679
|
+
.sm-health__headline{display:flex;justify-content:space-between;gap:12px;font-weight:600;padding:6px 8px;margin:0}
|
|
680
|
+
.sm-health__row{grid-template-columns:minmax(160px,1fr) 140px 90px 120px 70px}
|
|
674
681
|
/* ── drag and drop (spec 2026-09-09 §7) ─────────────────────────────── */
|
|
675
682
|
/* The grip is faint until its row is hovered or it has focus. */
|
|
676
683
|
.sm-grip{
|
|
@@ -795,6 +802,12 @@ body:has(.sm-admin){margin:0}
|
|
|
795
802
|
font-family:var(--font-sans);font-size:var(--text-xs);font-weight:600;letter-spacing:var(--tracking-tag);text-transform:uppercase;color:var(--text-muted);
|
|
796
803
|
}
|
|
797
804
|
.sm-stale{padding:8px 20px 0}
|
|
805
|
+
/* Drift (spec 2026-09-27 editor safety §5): the document notice under the stale line, the orphan group at a form's foot. */
|
|
806
|
+
.sm-drift{padding:8px 20px 0}
|
|
807
|
+
.sm-orphans{margin-top:16px;border-top:1px solid var(--line);padding-top:12px}
|
|
808
|
+
.sm-orphans__head{cursor:pointer;font-size:var(--text-sm);font-weight:600;letter-spacing:var(--caps-tracking);text-transform:var(--caps-transform);color:var(--text-muted)}
|
|
809
|
+
.sm-orphans__row{display:grid;grid-template-columns:minmax(80px,auto) 1fr auto auto;gap:8px;align-items:center;margin-top:8px}
|
|
810
|
+
.sm-orphans__value{color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-sm)}
|
|
798
811
|
.sm-lock{display:inline-flex;align-items:center;margin-left:6px;color:var(--text-muted);vertical-align:middle}
|
|
799
812
|
.sm-presence{margin:0;padding:6px 20px 0;font-size:13px;color:var(--text-muted)}
|
|
800
813
|
.sm-banner{
|
|
@@ -922,6 +935,14 @@ body:has(.sm-admin){margin:0}
|
|
|
922
935
|
.sm-outline-row__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
923
936
|
.sm-outline-row__note{font-size:var(--text-sm);font-weight:500;flex:none;letter-spacing:var(--caps-tracking);text-transform:var(--caps-transform)}
|
|
924
937
|
.sm-outline-row__add{color:var(--accent);font-weight:500;flex:none}
|
|
938
|
+
/* Drift marks (spec 2026-09-27 editor safety §5): a stripe inside the row's left edge, red for broken, muted for left over. */
|
|
939
|
+
.sm-outline-row--broken{box-shadow:inset 3px 0 0 var(--danger)}
|
|
940
|
+
.sm-outline-row--orphan{box-shadow:inset 3px 0 0 var(--line-dashed)}
|
|
941
|
+
.sm-outline-note{margin:0;font-size:var(--text-sm);color:var(--danger)}
|
|
942
|
+
/* A removed zone's one row wraps its sentence rather than cutting it: the inspector is narrow. */
|
|
943
|
+
.sm-outline-row--removed{height:auto;min-height:var(--outline-row-h);padding-block:6px}
|
|
944
|
+
.sm-outline-row--removed .sm-outline-row__label{white-space:normal}
|
|
945
|
+
.sm-rtcard--broken{box-shadow:inset 3px 0 0 var(--danger)}
|
|
925
946
|
/* A row with a grip: the grip takes the left padding's place and the label follows it. */
|
|
926
947
|
.sm-outline-row--grip{padding-left:2px}
|
|
927
948
|
.sm-outline-row--grip .sm-outline-row__label{flex:1}
|
|
@@ -943,12 +964,17 @@ body:has(.sm-admin){margin:0}
|
|
|
943
964
|
width:26px;height:26px;flex:none;border-radius:var(--radius-sm);background:var(--accent-soft);
|
|
944
965
|
color:var(--accent);display:grid;place-items:center;font-size:13px;
|
|
945
966
|
}
|
|
946
|
-
/* Combobox (spec 2026-09-17 §1): the input owns the width, the list hangs under it
|
|
967
|
+
/* Combobox (spec 2026-09-17 §1): the input owns the width, the list hangs under it —
|
|
968
|
+
portaled and fixed at the input's width (--combo-w), above modals and below
|
|
969
|
+
the confirm scrim (2026-09-27); inline under the input in static markup. */
|
|
947
970
|
.sm-combo{position:relative}
|
|
948
971
|
.sm-combo .sm-input{width:100%;padding-right:30px}
|
|
972
|
+
.sm-combo--suffix .sm-input{padding-right:96px}
|
|
973
|
+
.sm-combo__suffix{position:absolute;right:28px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:4px}
|
|
974
|
+
.sm-combo__suffix a.sm-iconbtn{text-decoration:none}
|
|
949
975
|
.sm-combo__chevron{position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--text-muted)}
|
|
950
976
|
.sm-combo__list{position:absolute;top:calc(100% + 4px);left:0;right:0;width:auto;max-height:280px;overflow-y:auto;z-index:60}
|
|
951
|
-
.sm-combo__list--
|
|
977
|
+
.sm-combo__list--floating{position:fixed;top:auto;right:auto;width:var(--combo-w,300px);z-index:75}
|
|
952
978
|
.sm-combo__empty{padding:8px 10px;color:var(--text-muted);font-size:var(--text-sm)}
|
|
953
979
|
/* Reference picker (spec 2026-09-17 §2): the picked entries under the search input. */
|
|
954
980
|
.sm-refpicker{display:flex;flex-direction:column;gap:6px;min-width:0}
|
|
@@ -978,7 +1004,7 @@ body:has(.sm-admin){margin:0}
|
|
|
978
1004
|
.sm-combo--tag{width:180px}
|
|
979
1005
|
.sm-combo--tag .sm-input{height:var(--control-h-sm);padding-right:10px}
|
|
980
1006
|
.sm-combo--tag .sm-combo__chevron{display:none}
|
|
981
|
-
.sm-
|
|
1007
|
+
.sm-combo__list.sm-combo__list--tag{right:auto;width:240px}
|
|
982
1008
|
/* ── date and datetime pickers (spec 2026-09-25) ──────────────────────
|
|
983
1009
|
The text box with a calendar button as its suffix; for datetime an hour
|
|
984
1010
|
and a minute select beside it; the popover a month grid on the menu
|
|
@@ -1152,6 +1178,9 @@ body:has(.sm-admin){margin:0}
|
|
|
1152
1178
|
}
|
|
1153
1179
|
.sm-dialog__context strong{font-weight:500;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:var(--text-md)}
|
|
1154
1180
|
.sm-dialog__context code{font-family:var(--font-mono);flex:none}
|
|
1181
|
+
.sm-confirm__option{margin-top:4px;font-size:var(--text-md);color:var(--text)}
|
|
1182
|
+
.sm-redirects-from{display:flex;flex-direction:column;gap:4px}
|
|
1183
|
+
.sm-redirects-from code{font-size:var(--text-sm)}
|
|
1155
1184
|
/* A form dialog's fields: white fields on the white card get the tile tone instead. */
|
|
1156
1185
|
.sm-dialog__form{display:flex;flex-direction:column;gap:14px}
|
|
1157
1186
|
.sm-dialog__form .sm-input,.sm-dialog__form .sm-select,.sm-dialog__form .sm-inputwrap{background:var(--surface)}
|
package/dist/admin/validate.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// A field whose `.hidden()` condition holds is skipped entirely — a hidden
|
|
6
6
|
// required field is not required (spec 2026-09-14 §12); the same isHidden
|
|
7
7
|
// the form uses decides, over the same scope.
|
|
8
|
-
import { optionValues } from "../fields.js";
|
|
9
8
|
import { isHidden } from "../conditions.js";
|
|
10
9
|
import { cssProblem } from "../css.js";
|
|
11
10
|
import { normalizeTag, sameTag } from "../tags.js";
|
|
@@ -13,6 +12,7 @@ import { hasContent } from "./richtext-doc.js";
|
|
|
13
12
|
import { ELEMENT_NODE, richtextBlocks } from "../richtext-walk.js";
|
|
14
13
|
import { rowsProblems } from "../custom/rows.js";
|
|
15
14
|
import { isCalendarDay, isInstant } from "./forms/date-picker.js";
|
|
15
|
+
import { fits, misfitMessage } from "./drift.js";
|
|
16
16
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
17
17
|
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
18
18
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -32,6 +32,11 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
32
32
|
errors.push({ field: path, message: "Required." });
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
+
// The one type check (spec 2026-09-27 editor safety §2): drift and publish share it.
|
|
36
|
+
if (!fits(d, value)) {
|
|
37
|
+
errors.push({ field: path, message: misfitMessage(d, value) });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
35
40
|
if (d.type === "richtext") {
|
|
36
41
|
if (!hasContent(value)) {
|
|
37
42
|
if (!mayBeAbsent(d))
|
|
@@ -58,26 +63,20 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
58
63
|
}
|
|
59
64
|
// Dates (spec 2026-09-25 date pickers §2): the stored formats exactly, and a day the calendar has.
|
|
60
65
|
if (d.type === "date") {
|
|
61
|
-
if (
|
|
66
|
+
if (!isCalendarDay(value))
|
|
62
67
|
errors.push({ field: path, message: "Not a date (2026-09-02)." });
|
|
63
68
|
return;
|
|
64
69
|
}
|
|
65
70
|
if (d.type === "datetime") {
|
|
66
|
-
if (
|
|
71
|
+
if (!isInstant(value))
|
|
67
72
|
errors.push({ field: path, message: "Not a date and time." });
|
|
68
73
|
return;
|
|
69
74
|
}
|
|
70
|
-
if (d.type === "image" || d.type === "video" || d.type === "file")
|
|
71
|
-
if (typeof value !== "object" || Array.isArray(value) || typeof value.asset !== "string") {
|
|
72
|
-
errors.push({ field: path, message: "Must be an uploaded file." });
|
|
73
|
-
}
|
|
75
|
+
if (d.type === "image" || d.type === "video" || d.type === "file")
|
|
74
76
|
return;
|
|
75
|
-
}
|
|
76
77
|
if (d.type === "list") {
|
|
77
|
-
if (!Array.isArray(value))
|
|
78
|
-
errors.push({ field: path, message: "Must be a list." });
|
|
78
|
+
if (!Array.isArray(value))
|
|
79
79
|
return;
|
|
80
|
-
}
|
|
81
80
|
if (typeof d.min === "number" && value.length < d.min)
|
|
82
81
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
83
82
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -89,37 +88,19 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
89
88
|
return;
|
|
90
89
|
}
|
|
91
90
|
if (d.type === "object") {
|
|
92
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
-
errors.push({ field: path, message: "Must be an object." });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
91
|
validateMap(path, value, subfields(d), errors, elements, { ...scope, self: value });
|
|
97
92
|
return;
|
|
98
93
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
errors.push({ field: path, message: "Must be on or off." });
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (d.type === "select" || d.type === "radio") {
|
|
105
|
-
const allowed = optionValues(d.options);
|
|
106
|
-
if (allowed.length > 0 && !allowed.includes(value)) {
|
|
107
|
-
errors.push({ field: path, message: `Must be one of: ${allowed.join(", ")}.` });
|
|
108
|
-
}
|
|
94
|
+
// The shape of these is fits' (above); what is left is the count.
|
|
95
|
+
if (d.type === "boolean" || d.type === "select" || d.type === "radio" || d.type === "checkboxes")
|
|
109
96
|
return;
|
|
110
|
-
}
|
|
111
97
|
// Refs (spec 2026-09-17 §3): the shape and the count. Whether a target exists
|
|
112
98
|
// and is published is publish integrity's question (store.ts), not this one's.
|
|
113
|
-
if (d.type === "ref")
|
|
114
|
-
if (typeof value !== "string")
|
|
115
|
-
errors.push({ field: path, message: "Must be an entry." });
|
|
99
|
+
if (d.type === "ref")
|
|
116
100
|
return;
|
|
117
|
-
}
|
|
118
101
|
if (d.type === "refList") {
|
|
119
|
-
if (!Array.isArray(value)
|
|
120
|
-
errors.push({ field: path, message: "Must be a list of entries." });
|
|
102
|
+
if (!Array.isArray(value))
|
|
121
103
|
return;
|
|
122
|
-
}
|
|
123
104
|
if (typeof d.min === "number" && value.length < d.min)
|
|
124
105
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
125
106
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -130,10 +111,8 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
130
111
|
return;
|
|
131
112
|
}
|
|
132
113
|
if (d.type === "tags") {
|
|
133
|
-
if (!Array.isArray(value))
|
|
134
|
-
errors.push({ field: path, message: "Must be a list of tags." });
|
|
114
|
+
if (!Array.isArray(value))
|
|
135
115
|
return;
|
|
136
|
-
}
|
|
137
116
|
if (typeof d.min === "number" && value.length < d.min)
|
|
138
117
|
errors.push({ field: path, message: `At least ${plural(d.min, "item")}.` });
|
|
139
118
|
if (typeof d.max === "number" && value.length > d.max)
|
|
@@ -146,17 +125,6 @@ function validateValue(path, value, d, errors, elements, scope) {
|
|
|
146
125
|
});
|
|
147
126
|
return;
|
|
148
127
|
}
|
|
149
|
-
if (d.type === "checkboxes") {
|
|
150
|
-
if (!Array.isArray(value)) {
|
|
151
|
-
errors.push({ field: path, message: "Must be a list." });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const allowed = optionValues(d.options);
|
|
155
|
-
if (value.some((v) => !allowed.includes(v))) {
|
|
156
|
-
errors.push({ field: path, message: `Must be one of: ${allowed.join(", ")}.` });
|
|
157
|
-
}
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
128
|
if (typeof d.max === "number" && typeof value === "string" && value.length > d.max) {
|
|
161
129
|
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
162
130
|
}
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
|
2
2
|
import type { GlobalSchema } from "./global.ts";
|
|
3
3
|
import { type RefEdge, type SectionLike } from "./refs.ts";
|
|
4
4
|
import { MemoryRefIndex } from "./ref-index.ts";
|
|
5
|
-
import { type PathIndex } from "./path-index.ts";
|
|
5
|
+
import { type PathHit, type PathIndex } from "./path-index.ts";
|
|
6
6
|
import { type DraftToken } from "./draft-token.ts";
|
|
7
|
-
import { type LocaleSet
|
|
7
|
+
import { type LocaleSet } from "./paths.ts";
|
|
8
8
|
export type EntryLocale = {
|
|
9
9
|
/** The entry's segment in this locale; null claims no URL. */
|
|
10
10
|
slug: string | null;
|
|
@@ -40,6 +40,12 @@ export type EntryVersion = {
|
|
|
40
40
|
/** When the content last changed: the unpublished draft row is rewritten in place (spec 2026-09-13 history §1). */
|
|
41
41
|
savedAt: number;
|
|
42
42
|
};
|
|
43
|
+
/** One entry row for the drift scan (spec 2026-09-27 editor safety §3), whatever its collection:
|
|
44
|
+
* locale → the live version's merged fields, null where the locale is not published. */
|
|
45
|
+
export type EntryScan = {
|
|
46
|
+
record: EntryRecord;
|
|
47
|
+
live: Record<string, Record<string, unknown> | null>;
|
|
48
|
+
};
|
|
43
49
|
export type EntryUsage = {
|
|
44
50
|
sourceKind: string;
|
|
45
51
|
sourceId: string;
|
|
@@ -96,6 +102,8 @@ export interface EntryStore {
|
|
|
96
102
|
create(collection: string, fields: Record<string, unknown>, options: CreateEntryOptions, by?: string): Promise<EntryRecord>;
|
|
97
103
|
get(collection: string, id: string): Promise<EntryRecord | null>;
|
|
98
104
|
list(collection: string, options?: ListOptions): Promise<EntryRecord[]>;
|
|
105
|
+
/** Every entry row, registered collection or not, with each published locale's live fields (spec 2026-09-27 editor safety §3). */
|
|
106
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
99
107
|
/** Batched read for the resolver: the MERGED fields in `locale`, keyed
|
|
100
108
|
* by id. An id that is missing, absent in the locale, or (with
|
|
101
109
|
* `status`) unpublished there is omitted. With `{ status: "published" }`
|
|
@@ -107,7 +115,10 @@ export interface EntryStore {
|
|
|
107
115
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
108
116
|
/** Replaces the merged fields for `locale`: shared keys on the node
|
|
109
117
|
* (every locale sees them), localized keys and the slug on the row. */
|
|
110
|
-
|
|
118
|
+
/** `redirect` (default true) keeps a published row's old address as a redirect when the slug changes (spec 2026-09-26 redirects §1). */
|
|
119
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null, options?: {
|
|
120
|
+
redirect?: boolean;
|
|
121
|
+
}): Promise<EntryRecord>;
|
|
111
122
|
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
112
123
|
/** The live row goes back to what is published: the published snapshot's fields
|
|
113
124
|
* (shared and localized, split as on save — the slug is never versioned) written over
|
|
@@ -143,6 +154,8 @@ export interface EntryStore {
|
|
|
143
154
|
move(collection: string, id: string, to: {
|
|
144
155
|
parentId: string | null;
|
|
145
156
|
index?: number;
|
|
157
|
+
}, options?: {
|
|
158
|
+
redirect?: boolean;
|
|
146
159
|
}): Promise<EntryRecord>;
|
|
147
160
|
/** Rewrite manual order to 1..N for the given ids, one batched pass. */
|
|
148
161
|
reorder(collection: string, orderedIds: string[]): Promise<void>;
|
|
@@ -153,8 +166,10 @@ export interface EntryStore {
|
|
|
153
166
|
sourceKind: string;
|
|
154
167
|
sourceId: string;
|
|
155
168
|
}[]>;
|
|
156
|
-
resolvePath(locale: string, path: string): Promise<
|
|
169
|
+
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
157
170
|
pathsOf(collection: string, id: string): Promise<Record<string, string>>;
|
|
171
|
+
/** locale → this entry's old addresses. */
|
|
172
|
+
redirectsOf(collection: string, id: string): Promise<Record<string, string[]>>;
|
|
158
173
|
}
|
|
159
174
|
export declare function schemaOf(collections: CollectionSchema<any>[], name: string): CollectionSchema<any>;
|
|
160
175
|
/** A global item is a collection with exactly one entry (spec 2026-09-07). */
|
|
@@ -196,6 +211,10 @@ export type SplitFields = {
|
|
|
196
211
|
* Every key that is neither localized nor `slug` is shared — declared or
|
|
197
212
|
* not — so a stray key never disappears into a locale row. */
|
|
198
213
|
export declare function splitFields(fields: Record<string, unknown>, schema: CollectionSchema<any>): SplitFields;
|
|
214
|
+
/** A localized key's value still in the shared fields — saved while the field was shared — stays
|
|
215
|
+
* there as every locale's fallback: a write never deletes it (spec 2026-09-27 editor safety §7).
|
|
216
|
+
* Reads merge shared, then the row, so each locale shows its own value once it has one. */
|
|
217
|
+
export declare function keepLocalizedShared(before: Record<string, unknown>, shared: Record<string, unknown>, schema: CollectionSchema<any>): Record<string, unknown>;
|
|
199
218
|
export declare function entryLocaleRow(record: EntryRecord, locale: string): EntryLocale;
|
|
200
219
|
/** The merged view of one locale: shared + that locale's fields + its
|
|
201
220
|
* slug. A fresh object; throws for an absent locale. */
|
|
@@ -271,17 +290,22 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
271
290
|
getMany(collection: string, ids: string[], locale: string, options?: {
|
|
272
291
|
status?: "published";
|
|
273
292
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
274
|
-
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null
|
|
293
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null, options?: {
|
|
294
|
+
redirect?: boolean;
|
|
295
|
+
}): Promise<EntryRecord>;
|
|
275
296
|
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
276
297
|
discardDraft(collection: string, id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
277
298
|
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
278
299
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
300
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
279
301
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
280
302
|
restore(collection: string, id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
281
303
|
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
282
304
|
move(collection: string, id: string, to: {
|
|
283
305
|
parentId: string | null;
|
|
284
306
|
index?: number;
|
|
307
|
+
}, options?: {
|
|
308
|
+
redirect?: boolean;
|
|
285
309
|
}): Promise<EntryRecord>;
|
|
286
310
|
delete(collection: string, id: string): Promise<void>;
|
|
287
311
|
reorder(collection: string, orderedIds: string[]): Promise<void>;
|
|
@@ -290,6 +314,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
290
314
|
sourceKind: string;
|
|
291
315
|
sourceId: string;
|
|
292
316
|
}[]>;
|
|
293
|
-
resolvePath(locale: string, path: string): Promise<
|
|
317
|
+
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
294
318
|
pathsOf(collection: string, id: string): Promise<{}>;
|
|
319
|
+
redirectsOf(collection: string, id: string): Promise<Record<string, string[]>>;
|
|
295
320
|
}
|
package/dist/entry-store.js
CHANGED
|
@@ -13,10 +13,11 @@ import { collectionDepth, collectionOrder } from "./collections.js";
|
|
|
13
13
|
import { collectEntryRefs } from "./refs.js";
|
|
14
14
|
import { MemoryRefIndex } from "./ref-index.js";
|
|
15
15
|
import { MemoryPathIndex } from "./path-index.js";
|
|
16
|
+
import { groupRedirects } from "./store.js";
|
|
16
17
|
import { assertFresh } from "./draft-token.js";
|
|
17
18
|
import { globalEntryId } from "./global-id.js";
|
|
18
19
|
import { DEFAULT_SPACE_ID } from "./sql-space.js";
|
|
19
|
-
import { assertDepth, assertSupportedLocale, chainOf, entryPathRows, slugOf, unsupportedLocale, } from "./paths.js";
|
|
20
|
+
import { assertDepth, assertSupportedLocale, chainOf, entryPathRows, slugOf, unsupportedLocale, rewriteTargets, } from "./paths.js";
|
|
20
21
|
/** The field types whose value is an array: a scalar `where` on one is
|
|
21
22
|
* containment, and any-of on one is refused by the site layer (both
|
|
22
23
|
* adapters read the text form of the whole array, never its elements). */
|
|
@@ -97,6 +98,17 @@ export function splitFields(fields, schema) {
|
|
|
97
98
|
}
|
|
98
99
|
return { shared: structuredClone(shared), localized: structuredClone(localized), slug: slugOf(fields) };
|
|
99
100
|
}
|
|
101
|
+
/** A localized key's value still in the shared fields — saved while the field was shared — stays
|
|
102
|
+
* there as every locale's fallback: a write never deletes it (spec 2026-09-27 editor safety §7).
|
|
103
|
+
* Reads merge shared, then the row, so each locale shows its own value once it has one. */
|
|
104
|
+
export function keepLocalizedShared(before, shared, schema) {
|
|
105
|
+
const out = { ...shared };
|
|
106
|
+
for (const key of localizedKeys(schema)) {
|
|
107
|
+
if (key in before && !(key in out))
|
|
108
|
+
out[key] = structuredClone(before[key]);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
100
112
|
export function entryLocaleRow(record, locale) {
|
|
101
113
|
const row = record.locales[locale];
|
|
102
114
|
if (!row)
|
|
@@ -270,8 +282,8 @@ export class MemoryEntryStore {
|
|
|
270
282
|
}
|
|
271
283
|
return sets;
|
|
272
284
|
}
|
|
273
|
-
async writePaths(collection, nodes) {
|
|
274
|
-
await this.paths.rewrite(
|
|
285
|
+
async writePaths(collection, nodes, redirect = true) {
|
|
286
|
+
await this.paths.rewrite(rewriteTargets("entry", nodes, redirect), entryPathRows(nodes, this.byId, this.schema(collection), this.locales));
|
|
275
287
|
}
|
|
276
288
|
writeRefs(entry) {
|
|
277
289
|
this.refs.rewrite({ sourceKind: "entry", sourceId: entry.id }, entryRefEdges(this.liveFieldSets(entry), this.schema(entry.collection), this.elements));
|
|
@@ -396,7 +408,7 @@ export class MemoryEntryStore {
|
|
|
396
408
|
}
|
|
397
409
|
return out;
|
|
398
410
|
}
|
|
399
|
-
async update(collection, id, locale, fields, by, expected) {
|
|
411
|
+
async update(collection, id, locale, fields, by, expected, options) {
|
|
400
412
|
const schema = this.schema(collection);
|
|
401
413
|
const entry = this.must(collection, id);
|
|
402
414
|
const row = entryLocaleRow(entry, locale);
|
|
@@ -407,11 +419,11 @@ export class MemoryEntryStore {
|
|
|
407
419
|
throw entryErrors.noSlug(collection);
|
|
408
420
|
this.assertSiblingSlug(collection, entry.parentId, locale, slug, id);
|
|
409
421
|
const before = { fields: entry.fields, row: { ...row } };
|
|
410
|
-
entry.fields = shared;
|
|
422
|
+
entry.fields = keepLocalizedShared(before.fields, shared, schema);
|
|
411
423
|
row.fields = localized;
|
|
412
424
|
row.slug = slug;
|
|
413
425
|
try {
|
|
414
|
-
await this.writePaths(collection, this.subtree(entry));
|
|
426
|
+
await this.writePaths(collection, this.subtree(entry), options?.redirect ?? true);
|
|
415
427
|
}
|
|
416
428
|
catch (e) {
|
|
417
429
|
entry.fields = before.fields;
|
|
@@ -448,7 +460,7 @@ export class MemoryEntryStore {
|
|
|
448
460
|
if (!snapshot)
|
|
449
461
|
throw entryErrors.nothingPublished(collection, id, locale);
|
|
450
462
|
const { shared, localized } = splitFields(snapshot.fields, schema);
|
|
451
|
-
entry.fields = shared;
|
|
463
|
+
entry.fields = keepLocalizedShared(entry.fields, shared, schema);
|
|
452
464
|
row.fields = localized;
|
|
453
465
|
const abandoned = row.draftVersionId;
|
|
454
466
|
row.draftVersionId = row.publishedVersionId;
|
|
@@ -505,7 +517,7 @@ export class MemoryEntryStore {
|
|
|
505
517
|
throw entryErrors.localeInUse(locale, blockers);
|
|
506
518
|
delete entry.locales[locale];
|
|
507
519
|
try {
|
|
508
|
-
await this.writePaths(collection, [entry]);
|
|
520
|
+
await this.writePaths(collection, [entry], false);
|
|
509
521
|
}
|
|
510
522
|
catch (e) {
|
|
511
523
|
entry.locales[locale] = row;
|
|
@@ -516,6 +528,18 @@ export class MemoryEntryStore {
|
|
|
516
528
|
this.writeRefs(entry);
|
|
517
529
|
return this.snapshot(entry);
|
|
518
530
|
}
|
|
531
|
+
async scanEntries() {
|
|
532
|
+
return [...this.entries.values()].map((entry) => ({
|
|
533
|
+
record: this.snapshot(entry),
|
|
534
|
+
live: Object.fromEntries(Object.entries(entry.locales).map(([l, row]) => {
|
|
535
|
+
if (row.status !== "published")
|
|
536
|
+
return [l, null];
|
|
537
|
+
// A legacy published row without a snapshot serves its live row (spec 2026-09-14 §1).
|
|
538
|
+
const snap = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
|
|
539
|
+
return [l, snap ? structuredClone(snap.fields) : entryFieldsIn(entry, l)];
|
|
540
|
+
})),
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
519
543
|
async listVersions(collection, id, locale) {
|
|
520
544
|
this.must(collection, id);
|
|
521
545
|
return [...this.versions.values()]
|
|
@@ -534,7 +558,7 @@ export class MemoryEntryStore {
|
|
|
534
558
|
if (versionId === row.publishedVersionId)
|
|
535
559
|
return this.discardDraft(collection, id, locale, by, expected);
|
|
536
560
|
const { shared, localized } = splitFields(version.fields, schema);
|
|
537
|
-
entry.fields = shared;
|
|
561
|
+
entry.fields = keepLocalizedShared(entry.fields, shared, schema);
|
|
538
562
|
row.fields = localized; // the slug is the row's own and never versioned
|
|
539
563
|
this.recordVersion(entry, locale);
|
|
540
564
|
this.bump(entry, row, by);
|
|
@@ -546,7 +570,7 @@ export class MemoryEntryStore {
|
|
|
546
570
|
const v = this.versions.get(versionId);
|
|
547
571
|
return v ? structuredClone(v) : null;
|
|
548
572
|
}
|
|
549
|
-
async move(collection, id, to) {
|
|
573
|
+
async move(collection, id, to, options) {
|
|
550
574
|
if (isGlobalSchema(this.schema(collection)))
|
|
551
575
|
throw entryErrors.owned(collection, "it has no tree position");
|
|
552
576
|
const entry = this.must(collection, id);
|
|
@@ -572,7 +596,7 @@ export class MemoryEntryStore {
|
|
|
572
596
|
const previous = new Map(siblings.map((s) => [s.id, s.sort]));
|
|
573
597
|
siblings.forEach((s, i) => (s.sort = i + 1));
|
|
574
598
|
try {
|
|
575
|
-
await this.writePaths(collection, subtree);
|
|
599
|
+
await this.writePaths(collection, subtree, options?.redirect ?? true);
|
|
576
600
|
}
|
|
577
601
|
catch (e) {
|
|
578
602
|
Object.assign(entry, before);
|
|
@@ -621,4 +645,8 @@ export class MemoryEntryStore {
|
|
|
621
645
|
pathsOf(collection, id) {
|
|
622
646
|
return this.row(collection, id) ? this.paths.pathsOf({ kind: "entry", id }) : Promise.resolve({});
|
|
623
647
|
}
|
|
648
|
+
async redirectsOf(collection, id) {
|
|
649
|
+
this.schema(collection);
|
|
650
|
+
return groupRedirects(await this.paths.redirectsOf({ kind: "entry", id }));
|
|
651
|
+
}
|
|
624
652
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export type { PathIndex } from "./path-index.ts";
|
|
|
51
51
|
export { MemoryEntryStore } from "./entry-store.ts";
|
|
52
52
|
export { SupabaseEntryStore } from "./supabase-entry-store.ts";
|
|
53
53
|
export { SupabasePathIndex } from "./supabase-path-index.ts";
|
|
54
|
-
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
54
|
+
export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
|
|
55
55
|
export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
|
|
56
56
|
export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
|
|
57
57
|
export type { AffectedTargets } from "./revalidate.ts";
|
|
@@ -60,7 +60,7 @@ export type { EntryFetcher, ResolveFieldsOptions, AssetFetcher } from "./resolve
|
|
|
60
60
|
export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, pageAlternates, entryAlternates, } from "./paths.ts";
|
|
61
61
|
export type { LocaleSet, PathTarget, PathRow, PageLike, EntryLike, PageTreeNode, PagePathOptions, LocaleAlternate, PageAlternate } from "./paths.ts";
|
|
62
62
|
export { fixedPathFor } from "./localize.ts";
|
|
63
|
-
export type { PageStore, PageRecord, PageLocale, PageVersion, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
|
|
63
|
+
export type { PageStore, PageRecord, PageLocale, PageVersion, PageScan, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
|
|
64
64
|
export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
|
|
65
65
|
export type { SmoodlyConfig, ResolvedConfig } from "./config.ts";
|
|
66
66
|
export type { FieldBuilder, Descriptor, ValueOf, Option, OptionInput, OptionValue } from "./fields.ts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { unstable_cache } from "next/cache";
|
|
3
3
|
import { draftMode } from "next/headers";
|
|
4
|
-
import { notFound } from "next/navigation";
|
|
4
|
+
import { notFound, permanentRedirect } from "next/navigation";
|
|
5
5
|
import { connection } from "next/server";
|
|
6
6
|
import { EditorBridge } from "../admin/next/bridge.js";
|
|
7
7
|
import { fixedPathFor } from "../localize.js";
|
|
@@ -15,18 +15,31 @@ export function createPageRenderer(options) {
|
|
|
15
15
|
const href = options.href ?? ((path) => path);
|
|
16
16
|
const shapes = new Map(options.pages.filter((p) => p.schema.kind === "page").map((p) => [p.schema.name, p]));
|
|
17
17
|
const entryPages = new Map(options.pages.filter((p) => p.schema.kind === "entryPage" && p.schema.collection).map((p) => [p.schema.collection, p]));
|
|
18
|
-
/** locate
|
|
19
|
-
* cached per (locale, path) outside draft mode. */
|
|
20
|
-
async function load(locale, path, draft, version) {
|
|
21
|
-
const target = await site.locate(locale, path);
|
|
22
|
-
if (!target)
|
|
23
|
-
return null;
|
|
18
|
+
/** The hit for a located target (locate is uncached: the id it returns
|
|
19
|
+
* IS the cache tag): load — cached per (locale, path) outside draft mode. */
|
|
20
|
+
async function load(target, locale, path, draft, version) {
|
|
24
21
|
if (draft)
|
|
25
22
|
return site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) });
|
|
26
23
|
return unstable_cache(() => site.load(target, locale, path), ["smoodly-site", locale, path], {
|
|
27
24
|
tags: [target.kind === "page" ? pageTag(target.id) : entryTag(target.id)],
|
|
28
25
|
})();
|
|
29
26
|
}
|
|
27
|
+
/** A redirect row (spec 2026-09-26 redirects §7): the node's CURRENT path in this locale, through href, the query kept; no path there is a 404. */
|
|
28
|
+
async function redirectTo(target, locale, params) {
|
|
29
|
+
const current = (await site.targetPaths(target))[locale];
|
|
30
|
+
if (!current)
|
|
31
|
+
notFound();
|
|
32
|
+
const query = new URLSearchParams();
|
|
33
|
+
for (const [k, v] of Object.entries(params ?? {})) {
|
|
34
|
+
if (typeof v === "string")
|
|
35
|
+
query.append(k, v);
|
|
36
|
+
else
|
|
37
|
+
for (const item of v ?? [])
|
|
38
|
+
query.append(k, item);
|
|
39
|
+
}
|
|
40
|
+
const qs = query.toString();
|
|
41
|
+
permanentRedirect(href(current, locale) + (qs ? `?${qs}` : ""));
|
|
42
|
+
}
|
|
30
43
|
function render(hit, editor, locale) {
|
|
31
44
|
if (hit.kind === "page") {
|
|
32
45
|
const Page = shapes.get(hit.template);
|
|
@@ -67,7 +80,10 @@ export function createPageRenderer(options) {
|
|
|
67
80
|
const editor = draft && params?.["smoodly-editor"] === "1";
|
|
68
81
|
const versionParam = params?.["smoodly-version"];
|
|
69
82
|
const version = draft && typeof versionParam === "string" && versionParam !== "" ? versionParam : undefined;
|
|
70
|
-
const
|
|
83
|
+
const target = await site.locate(locale, path);
|
|
84
|
+
if (target?.redirect)
|
|
85
|
+
await redirectTo(target, locale, params);
|
|
86
|
+
const hit = target ? await load(target, locale, path, draft, version) : null;
|
|
71
87
|
if (!hit || !accept(hit))
|
|
72
88
|
notFound();
|
|
73
89
|
return (_jsxs(_Fragment, { children: [editor ? _jsx(EditorBridge, {}) : null, render(hit, editor, locale)] }));
|
|
@@ -78,7 +94,10 @@ export function createPageRenderer(options) {
|
|
|
78
94
|
const locale = opts?.locale ?? site.locales.default;
|
|
79
95
|
const path = pathFor(locale);
|
|
80
96
|
const { isEnabled: draft } = await draftMode();
|
|
81
|
-
const
|
|
97
|
+
const target = await site.locate(locale, path);
|
|
98
|
+
if (!target || target.redirect)
|
|
99
|
+
return {};
|
|
100
|
+
const hit = await load(target, locale, path, draft);
|
|
82
101
|
if (!hit || !accept(hit))
|
|
83
102
|
return {};
|
|
84
103
|
return { ...(hit.kind === "page" ? metadataFrom(hit.tree.meta, { title: hit.title }) : {}), ...alternates(hit) };
|
package/dist/path-index.d.ts
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
import type { PathRow, PathTarget } from "./paths.ts";
|
|
2
2
|
export type { PathRow, PathTarget } from "./paths.ts";
|
|
3
|
+
/** A rewrite target; `redirectLocales` names the locales whose old live rows stay as redirects (none = drop them). */
|
|
4
|
+
export type RewriteTarget = PathTarget & {
|
|
5
|
+
redirectLocales?: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
/** What lives at an address: the target, and whether the row is a redirect to it. */
|
|
8
|
+
export type PathHit = PathTarget & {
|
|
9
|
+
redirect: boolean;
|
|
10
|
+
};
|
|
3
11
|
export interface PathIndex {
|
|
4
|
-
rewrite(targets:
|
|
12
|
+
rewrite(targets: RewriteTarget[], rows: PathRow[]): Promise<void>;
|
|
13
|
+
/** Every row of the targets, redirects included (a deleted node has no old addresses). */
|
|
5
14
|
remove(targets: PathTarget[]): Promise<void>;
|
|
6
|
-
resolve(locale: string, path: string): Promise<
|
|
15
|
+
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
16
|
+
/** locale → the target's LIVE path. */
|
|
7
17
|
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
18
|
+
/** The target's redirect rows. */
|
|
19
|
+
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
20
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
21
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
22
|
+
/** A redirect by hand: refused (pathLiveError) at a live address; another redirect row there is replaced. */
|
|
23
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
24
|
+
/** Deletes a redirect row; a live row at the address is left alone. */
|
|
25
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
8
26
|
}
|
|
9
27
|
export declare const pathTakenError: (locale: string, path: string) => Error;
|
|
28
|
+
export declare const pathLiveError: (locale: string, path: string) => Error;
|
|
10
29
|
export declare class MemoryPathIndex implements PathIndex {
|
|
11
30
|
private rows;
|
|
12
|
-
rewrite(targets:
|
|
31
|
+
rewrite(targets: RewriteTarget[], rows: PathRow[]): Promise<void>;
|
|
13
32
|
remove(targets: PathTarget[]): Promise<void>;
|
|
14
|
-
resolve(locale: string, path: string): Promise<
|
|
33
|
+
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
15
34
|
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
35
|
+
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
36
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
37
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
38
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
39
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
16
40
|
}
|