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/path-index.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
const sameTarget = (a, b) => a.kind === b.kind && a.id === b.id;
|
|
2
2
|
const key = (locale, path) => `${locale} ${path}`;
|
|
3
3
|
export const pathTakenError = (locale, path) => new Error(`smoodly: the path "${path}" (${locale}) is already taken.`);
|
|
4
|
+
export const pathLiveError = (locale, path) => new Error(`smoodly: "${path}" (${locale}) is a live address.`);
|
|
5
|
+
const strip = ({ redirect: _redirect, ...row }) => row;
|
|
4
6
|
export class MemoryPathIndex {
|
|
5
7
|
constructor() {
|
|
6
8
|
this.rows = [];
|
|
7
9
|
}
|
|
8
10
|
async rewrite(targets, rows) {
|
|
9
|
-
const
|
|
11
|
+
const ofTarget = (r) => targets.find((t) => sameTarget(t, r));
|
|
12
|
+
const newKeys = new Set(rows.map((r) => key(r.locale, r.path)));
|
|
13
|
+
// 1. redirect rows at the new live addresses go, whoever they point at (live wins, rename-back reclaims)
|
|
14
|
+
let kept = this.rows.filter((r) => !(r.redirect && newKeys.has(key(r.locale, r.path))));
|
|
15
|
+
// 2. the targets' old live rows: flipped to redirects in the named locales unless the address stays live
|
|
16
|
+
kept = kept.map((r) => {
|
|
17
|
+
const t = !r.redirect ? ofTarget(r) : undefined;
|
|
18
|
+
return t && t.redirectLocales?.includes(r.locale) && !newKeys.has(key(r.locale, r.path)) ? { ...r, redirect: true } : r;
|
|
19
|
+
});
|
|
20
|
+
// 3. the targets' remaining live rows go — and so do their redirect rows in a locale the
|
|
21
|
+
// target has no new live row in (a removed translation: nothing could resolve them)
|
|
22
|
+
const liveLocales = new Set(rows.map((r) => `${r.kind} ${r.id} ${r.locale}`));
|
|
23
|
+
kept = kept.filter((r) => !ofTarget(r) || (r.redirect && liveLocales.has(`${r.kind} ${r.id} ${r.locale}`)));
|
|
24
|
+
// 4. the new live rows: a collision now is with another target's live row
|
|
10
25
|
const taken = new Set(kept.map((r) => key(r.locale, r.path)));
|
|
11
26
|
for (const row of rows) {
|
|
12
27
|
const k = key(row.locale, row.path);
|
|
@@ -14,20 +29,38 @@ export class MemoryPathIndex {
|
|
|
14
29
|
throw pathTakenError(row.locale, row.path);
|
|
15
30
|
taken.add(k);
|
|
16
31
|
}
|
|
17
|
-
this.rows = [...kept, ...rows.map((r) => ({ ...r }))];
|
|
32
|
+
this.rows = [...kept, ...rows.map((r) => ({ ...r, redirect: false }))];
|
|
18
33
|
}
|
|
19
34
|
async remove(targets) {
|
|
20
35
|
this.rows = this.rows.filter((r) => !targets.some((t) => sameTarget(t, r)));
|
|
21
36
|
}
|
|
22
37
|
async resolve(locale, path) {
|
|
23
38
|
const row = this.rows.find((r) => r.locale === locale && r.path === path);
|
|
24
|
-
return row ? { kind: row.kind, id: row.id } : null;
|
|
39
|
+
return row ? { kind: row.kind, id: row.id, redirect: row.redirect } : null;
|
|
25
40
|
}
|
|
26
41
|
async pathsOf(target) {
|
|
27
42
|
const out = {};
|
|
28
43
|
for (const r of this.rows)
|
|
29
|
-
if (sameTarget(r, target))
|
|
44
|
+
if (!r.redirect && sameTarget(r, target))
|
|
30
45
|
out[r.locale] = r.path;
|
|
31
46
|
return out;
|
|
32
47
|
}
|
|
48
|
+
async redirectsOf(target) {
|
|
49
|
+
return this.rows.filter((r) => r.redirect && sameTarget(r, target)).map(strip);
|
|
50
|
+
}
|
|
51
|
+
async listLive(locale) {
|
|
52
|
+
return this.rows.filter((r) => !r.redirect && r.locale === locale).map(strip);
|
|
53
|
+
}
|
|
54
|
+
async listRedirects(locale) {
|
|
55
|
+
return this.rows.filter((r) => r.redirect && r.locale === locale).map(strip);
|
|
56
|
+
}
|
|
57
|
+
async addRedirect(row) {
|
|
58
|
+
const existing = this.rows.find((r) => r.locale === row.locale && r.path === row.path);
|
|
59
|
+
if (existing && !existing.redirect)
|
|
60
|
+
throw pathLiveError(row.locale, row.path);
|
|
61
|
+
this.rows = [...this.rows.filter((r) => r !== existing), { ...row, redirect: true }];
|
|
62
|
+
}
|
|
63
|
+
async removeRedirect(locale, path) {
|
|
64
|
+
this.rows = this.rows.filter((r) => !(r.redirect && r.locale === locale && r.path === path));
|
|
65
|
+
}
|
|
33
66
|
}
|
package/dist/paths.d.ts
CHANGED
|
@@ -91,6 +91,22 @@ export declare function entrySegment(entry: EntryLike, locale: string): string |
|
|
|
91
91
|
export declare function entryPathRows(targets: EntryLike[], byId: (id: string) => EntryLike | undefined, collection: {
|
|
92
92
|
path?: string | Record<string, string>;
|
|
93
93
|
}, set: LocaleSet): PathRow[];
|
|
94
|
+
/** The locales a node is published in — the ones whose old addresses may
|
|
95
|
+
* stay as redirects (spec 2026-09-26 redirects §2: only published rows). */
|
|
96
|
+
export declare function publishedLocales(node: {
|
|
97
|
+
locales: Record<string, {
|
|
98
|
+
status: "draft" | "published";
|
|
99
|
+
}>;
|
|
100
|
+
}): string[];
|
|
101
|
+
/** The rewrite targets for a subtree: with `redirect`, each names its published locales. */
|
|
102
|
+
export declare function rewriteTargets<T extends {
|
|
103
|
+
id: string;
|
|
104
|
+
locales: Record<string, {
|
|
105
|
+
status: "draft" | "published";
|
|
106
|
+
}>;
|
|
107
|
+
}>(kind: "page" | "entry", nodes: T[], redirect: boolean): (PathTarget & {
|
|
108
|
+
redirectLocales?: string[];
|
|
109
|
+
})[];
|
|
94
110
|
/** A target's address in one locale (spec 2026-09-15 §1). A page always
|
|
95
111
|
* has both; an entry's slug is null when its row claims no URL, and its
|
|
96
112
|
* path null then or when the collection has no `path`. */
|
package/dist/paths.js
CHANGED
|
@@ -130,6 +130,15 @@ export function entryPathRows(targets, byId, collection, set) {
|
|
|
130
130
|
}
|
|
131
131
|
return rows;
|
|
132
132
|
}
|
|
133
|
+
/** The locales a node is published in — the ones whose old addresses may
|
|
134
|
+
* stay as redirects (spec 2026-09-26 redirects §2: only published rows). */
|
|
135
|
+
export function publishedLocales(node) {
|
|
136
|
+
return Object.keys(node.locales).filter((l) => node.locales[l].status === "published");
|
|
137
|
+
}
|
|
138
|
+
/** The rewrite targets for a subtree: with `redirect`, each names its published locales. */
|
|
139
|
+
export function rewriteTargets(kind, nodes, redirect) {
|
|
140
|
+
return nodes.map((n) => (redirect ? { kind, id: n.id, redirectLocales: publishedLocales(n) } : { kind, id: n.id }));
|
|
141
|
+
}
|
|
133
142
|
/** The page's address in EVERY supported locale, in config order: null
|
|
134
143
|
* where the node is absent, where an ancestor is, or where `advertised`
|
|
135
144
|
* says no — the site passes "published, or draft mode", so an
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { EMPTY_ATTR, ZONE_ATTR } from "./marks.js";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { EMPTY_ATTR, NODE_ATTR, ZONE_ATTR } from "./marks.js";
|
|
3
3
|
import { SMD_KEY } from "./section-wrapper.js";
|
|
4
4
|
import { DEFAULT_STYLE_VALUES } from "./styles.js";
|
|
5
5
|
import { CUSTOM_NODE_TYPE, createCustomSection } from "./custom/index.js";
|
|
@@ -23,6 +23,11 @@ export function Zone({ of: content }) {
|
|
|
23
23
|
return (_jsx(_Fragment, { children: nodes.map((node) => {
|
|
24
24
|
const Component = content.components.get(node.type);
|
|
25
25
|
if (!Component) {
|
|
26
|
+
// Editor mode draws a box the bridge can measure, so the node is selectable in place
|
|
27
|
+
// (spec 2026-09-27 editor safety §5); the site renders nothing, as before.
|
|
28
|
+
if (content.editor) {
|
|
29
|
+
return (_jsxs("div", { [NODE_ATTR]: node.id, style: { padding: "24px 16px", border: "1px dashed #b54708", color: "#b54708", font: "500 14px/1.4 system-ui, sans-serif", textAlign: "center" }, children: ["Unknown section `", node.type, "`"] }, node.id));
|
|
30
|
+
}
|
|
26
31
|
console.warn(`smoodly: no registered component for node type "${node.type}" — skipping.`);
|
|
27
32
|
return null;
|
|
28
33
|
}
|
package/dist/schema-version.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
2
2
|
* that adds a table, column, function or policy the package reads; a
|
|
3
3
|
* unit test holds it equal to the highest file in `migrations/`. */
|
|
4
|
-
export declare const SCHEMA_VERSION =
|
|
4
|
+
export declare const SCHEMA_VERSION = 4;
|
|
5
5
|
export declare const SCHEMA_VERSION_KEY = "schema_version";
|
|
6
6
|
export declare const CORE_FILE_RE: RegExp;
|
|
7
7
|
export declare const VIEWS_FILE_RE: RegExp;
|
package/dist/schema-version.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
8
8
|
* that adds a table, column, function or policy the package reads; a
|
|
9
9
|
* unit test holds it equal to the highest file in `migrations/`. */
|
|
10
|
-
export const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 4;
|
|
11
11
|
export const SCHEMA_VERSION_KEY = "schema_version";
|
|
12
12
|
export const CORE_FILE_RE = /^(\d{14})_smoodly_v(\d+)\.sql$/;
|
|
13
13
|
export const VIEWS_FILE_RE = /^(\d{14})_smoodly_views\.sql$/;
|
package/dist/site.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { type GlobalSchema } from "./global.ts";
|
|
|
9
9
|
import { type StyleValues, type StyleValuesInput } from "./styles.ts";
|
|
10
10
|
import { type CustomConfigInput, type ResolvedCustom } from "./custom/index.ts";
|
|
11
11
|
import type { PageRecord, PageStore } from "./store.ts";
|
|
12
|
+
import type { PathHit } from "./path-index.ts";
|
|
12
13
|
export type SiteOptions = {
|
|
13
14
|
stores: () => {
|
|
14
15
|
pages: PageStore;
|
|
@@ -106,7 +107,9 @@ export interface SmoodlySite {
|
|
|
106
107
|
/** The custom section's resolved options the renderer hands renderPage. */
|
|
107
108
|
readonly custom: ResolvedCustom;
|
|
108
109
|
/** One index hit: what lives at (locale, path). Uncached by design — the id it returns is the cache tag. */
|
|
109
|
-
locate(locale: string, path: string): Promise<
|
|
110
|
+
locate(locale: string, path: string): Promise<PathHit | null>;
|
|
111
|
+
/** locale → the target's current live path — where a redirect row sends the visitor (spec 2026-09-26 redirects §7). */
|
|
112
|
+
targetPaths(target: PathTarget): Promise<Record<string, string>>;
|
|
110
113
|
/** Everything the renderer needs for one target: null when it is a folder, unpublished (outside draft), or gone. */
|
|
111
114
|
load(target: PathTarget, locale: string, path: string, options?: LoadOptions): Promise<SiteHit | null>;
|
|
112
115
|
resolve(locale: string, path: string, options?: LoadOptions): Promise<SiteHit | null>;
|
package/dist/site.js
CHANGED
|
@@ -325,13 +325,15 @@ export function createSmoodlySite(options) {
|
|
|
325
325
|
styles,
|
|
326
326
|
custom,
|
|
327
327
|
locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
|
|
328
|
+
targetPaths: (target) => options.stores().pages.targetPaths(target),
|
|
328
329
|
load(target, locale, path, opts = {}) {
|
|
329
330
|
const draft = opts.draft ?? false;
|
|
330
331
|
return target.kind === "page" ? loadPage(target.id, locale, path, draft, opts.version) : loadEntry(target.id, locale, path, draft);
|
|
331
332
|
},
|
|
332
333
|
async resolve(locale, path, opts) {
|
|
334
|
+
// The content API stays strict (§7): a content address is not a redirect.
|
|
333
335
|
const target = await site.locate(locale, path);
|
|
334
|
-
return target ? site.load(target, locale, path, opts) : null;
|
|
336
|
+
return target && !target.redirect ? site.load(target, locale, path, opts) : null;
|
|
335
337
|
},
|
|
336
338
|
...api,
|
|
337
339
|
};
|
package/dist/sql.js
CHANGED
|
@@ -235,12 +235,16 @@ create table if not exists "refs" (
|
|
|
235
235
|
-- and no page or entry can share a URL with another in any locale.
|
|
236
236
|
-- Rows exist for drafts too (the editor needs them); the read policy
|
|
237
237
|
-- admits a row only when its target is published.
|
|
238
|
+
-- A row with "redirect" set is an old address of its target (spec
|
|
239
|
+
-- 2026-09-26 redirects §2): the same primary key, so a redirect can never
|
|
240
|
+
-- sit where a live URL is and a live row taking the address replaces it.
|
|
238
241
|
create table if not exists "paths" (
|
|
239
242
|
${SPACE_COL},
|
|
240
243
|
"locale" text not null,
|
|
241
244
|
"path" text not null,
|
|
242
245
|
"kind" text not null check ("kind" in ('page', 'entry')),
|
|
243
246
|
"target_id" uuid not null,
|
|
247
|
+
"redirect" boolean not null default false,
|
|
244
248
|
primary key ("space_id", "locale", "path")
|
|
245
249
|
);
|
|
246
250
|
|
|
@@ -280,10 +284,24 @@ create index if not exists "assets_space_created_idx"
|
|
|
280
284
|
-- function runs in the caller's transaction, so a primary-key collision
|
|
281
285
|
-- rolls the whole rewrite back. Inserted rows get their space from the
|
|
282
286
|
-- a_set_space trigger (the claim, or the default space).
|
|
287
|
+
-- A target may name redirect_locales: its old live rows there stay as redirects.
|
|
283
288
|
create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql set search_path = public as $$
|
|
284
289
|
delete from "paths" p
|
|
285
|
-
using jsonb_to_recordset(
|
|
290
|
+
using jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid)
|
|
291
|
+
where p."redirect" and p."locale" = r.locale and p."path" = r.path
|
|
292
|
+
and p."space_id" = "smoodly_current_space"();
|
|
293
|
+
update "paths" p set "redirect" = true
|
|
294
|
+
from jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
295
|
+
where not p."redirect" and p."kind" = t.kind and p."target_id" = t.id
|
|
296
|
+
and p."locale" = any (coalesce(t.redirect_locales, '{}'::text[]))
|
|
297
|
+
and not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid)
|
|
298
|
+
where r.locale = p."locale" and r.path = p."path")
|
|
299
|
+
and p."space_id" = "smoodly_current_space"();
|
|
300
|
+
delete from "paths" p
|
|
301
|
+
using jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
286
302
|
where p."kind" = t.kind and p."target_id" = t.id
|
|
303
|
+
and (not p."redirect" or not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid)
|
|
304
|
+
where r.kind = p."kind" and r.id = p."target_id" and r.locale = p."locale"))
|
|
287
305
|
and p."space_id" = "smoodly_current_space"();
|
|
288
306
|
insert into "paths" ("locale", "path", "kind", "target_id")
|
|
289
307
|
select r.locale, r.path, r.kind, r.id
|
package/dist/store.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { EntryStore } from "./entry-store.ts";
|
|
|
3
3
|
import { type RefEdge, type SectionLike } from "./refs.ts";
|
|
4
4
|
import type { Toolset } from "./toolset.ts";
|
|
5
5
|
import type { MemoryRefIndex } from "./ref-index.ts";
|
|
6
|
-
import { type PathIndex } from "./path-index.ts";
|
|
6
|
+
import { type PathHit, type PathIndex, type PathRow } from "./path-index.ts";
|
|
7
7
|
import { type DraftToken } from "./draft-token.ts";
|
|
8
8
|
import { type LocaleSet, type PagePathOptions, type PathTarget } from "./paths.ts";
|
|
9
9
|
export type PageLocale = {
|
|
@@ -52,25 +52,44 @@ export type PageVersion = {
|
|
|
52
52
|
tree: PageTree;
|
|
53
53
|
savedAt: number;
|
|
54
54
|
};
|
|
55
|
+
/** One page for the drift scan (spec 2026-09-27 editor safety §3): locale → draft and live tree; null where there is none. */
|
|
56
|
+
export type PageScan = {
|
|
57
|
+
record: PageRecord;
|
|
58
|
+
draft: Record<string, PageTree | null>;
|
|
59
|
+
live: Record<string, PageTree | null>;
|
|
60
|
+
};
|
|
55
61
|
export interface PageStore {
|
|
56
62
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
57
63
|
getPage(id: string): Promise<PageRecord | null>;
|
|
58
64
|
listPages(): Promise<PageRecord[]>;
|
|
59
|
-
|
|
65
|
+
/** Every page with each locale's draft and live tree, in few reads (spec 2026-09-27 editor safety §3). */
|
|
66
|
+
scanTrees(): Promise<PageScan[]>;
|
|
67
|
+
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
60
68
|
/** The database's schema version from `smoodly_meta`; null when the table is missing (older than v1). Memory: always current (spec 2026-09-26 §5). */
|
|
61
69
|
schemaVersion(): Promise<number | null>;
|
|
62
70
|
/** locale → path, for the locales the page exists in. */
|
|
63
71
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
72
|
+
/** locale → the target's live path, for any page or entry — the renderer's redirect branch (spec 2026-09-26 redirects §7). */
|
|
73
|
+
targetPaths(target: PathTarget): Promise<Record<string, string>>;
|
|
74
|
+
/** locale → this page's old addresses. */
|
|
75
|
+
redirectsOf(id: string): Promise<Record<string, string[]>>;
|
|
76
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
77
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
78
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
79
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
64
80
|
/** Refused when the page already has the locale or its parent lacks it (the parent rule). */
|
|
65
81
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
66
82
|
/** Deletes the row, its versions and its paths. Refused while a
|
|
67
83
|
* descendant has the locale, and for the page's last locale. */
|
|
68
84
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
|
85
|
+
/** `redirect` (default true) keeps a published row's old address as a redirect (spec 2026-09-26 redirects §1). */
|
|
69
86
|
rename(id: string, patch: {
|
|
70
87
|
locale: string;
|
|
71
88
|
slug?: string;
|
|
72
89
|
title?: string;
|
|
73
|
-
}, by?: string
|
|
90
|
+
}, by?: string, options?: {
|
|
91
|
+
redirect?: boolean;
|
|
92
|
+
}): Promise<PageRecord>;
|
|
74
93
|
/**
|
|
75
94
|
* Reparents `id` under `to.parentId`. `to.index` is the moved node's
|
|
76
95
|
* final 0-based position among the destination siblings: the node is
|
|
@@ -81,6 +100,8 @@ export interface PageStore {
|
|
|
81
100
|
move(id: string, to: {
|
|
82
101
|
parentId: string | null;
|
|
83
102
|
index?: number;
|
|
103
|
+
}, options?: {
|
|
104
|
+
redirect?: boolean;
|
|
84
105
|
}): Promise<PageRecord>;
|
|
85
106
|
/** A folder becomes a page: an empty draft version per present locale. */
|
|
86
107
|
setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
|
|
@@ -128,6 +149,8 @@ export type PageStoreOptions = {
|
|
|
128
149
|
};
|
|
129
150
|
export declare const DEFAULT_LOCALES: LocaleSet;
|
|
130
151
|
/** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
|
|
152
|
+
/** Group an index's redirect rows by locale, paths sorted. */
|
|
153
|
+
export declare function groupRedirects(rows: PathRow[]): Record<string, string[]>;
|
|
131
154
|
export declare function refSections(options: PageStoreOptions): SectionLike[] | undefined;
|
|
132
155
|
/** The options every adapter resolves the same way. */
|
|
133
156
|
export declare function pathOptions(options: PageStoreOptions): PagePathOptions & {
|
|
@@ -197,20 +220,31 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
197
220
|
private dropVersions;
|
|
198
221
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
199
222
|
getPage(id: string): Promise<PageRecord | null>;
|
|
223
|
+
scanTrees(): Promise<PageScan[]>;
|
|
200
224
|
listPages(): Promise<PageRecord[]>;
|
|
201
|
-
resolvePath(locale: string, path: string): Promise<
|
|
225
|
+
resolvePath(locale: string, path: string): Promise<PathHit | null>;
|
|
202
226
|
schemaVersion(): Promise<number>;
|
|
203
227
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
228
|
+
targetPaths(target: PathTarget): Promise<Record<string, string>>;
|
|
229
|
+
redirectsOf(id: string): Promise<Record<string, string[]>>;
|
|
230
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
231
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
232
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
233
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
204
234
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
205
235
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
|
206
236
|
rename(id: string, patch: {
|
|
207
237
|
locale: string;
|
|
208
238
|
slug?: string;
|
|
209
239
|
title?: string;
|
|
210
|
-
}, by?: string
|
|
240
|
+
}, by?: string, options?: {
|
|
241
|
+
redirect?: boolean;
|
|
242
|
+
}): Promise<PageRecord>;
|
|
211
243
|
move(id: string, to: {
|
|
212
244
|
parentId: string | null;
|
|
213
245
|
index?: number;
|
|
246
|
+
}, options?: {
|
|
247
|
+
redirect?: boolean;
|
|
214
248
|
}): Promise<PageRecord>;
|
|
215
249
|
setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
|
|
216
250
|
saveDraft(id: string, locale: string, tree: PageTree, by?: string, expected?: DraftToken | null): Promise<PageVersion>;
|
package/dist/store.js
CHANGED
|
@@ -15,9 +15,19 @@ import { customToolset, withCustomSchema } from "./custom/index.js";
|
|
|
15
15
|
import { MemoryPathIndex } from "./path-index.js";
|
|
16
16
|
import { assertFresh } from "./draft-token.js";
|
|
17
17
|
import { SCHEMA_VERSION } from "./schema-version.js";
|
|
18
|
-
import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, } from "./paths.js";
|
|
18
|
+
import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, rewriteTargets, } from "./paths.js";
|
|
19
19
|
export const DEFAULT_LOCALES = { default: "en", supported: ["en"] };
|
|
20
20
|
/** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
|
|
21
|
+
/** Group an index's redirect rows by locale, paths sorted. */
|
|
22
|
+
export function groupRedirects(rows) {
|
|
23
|
+
var _a;
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const r of rows)
|
|
26
|
+
(out[_a = r.locale] ?? (out[_a] = [])).push(r.path);
|
|
27
|
+
for (const l of Object.keys(out))
|
|
28
|
+
out[l].sort();
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
21
31
|
export function refSections(options) {
|
|
22
32
|
return options.sections ? withCustomSchema(options.sections, customToolset(options.custom?.toolset)) : undefined;
|
|
23
33
|
}
|
|
@@ -158,8 +168,8 @@ export class MemoryPageStore {
|
|
|
158
168
|
walk(id);
|
|
159
169
|
return out;
|
|
160
170
|
}
|
|
161
|
-
async writePaths(nodes) {
|
|
162
|
-
await this.paths.rewrite(
|
|
171
|
+
async writePaths(nodes, redirect = true) {
|
|
172
|
+
await this.paths.rewrite(rewriteTargets("page", nodes, redirect), pagePathRows(nodes, this.byId, this.opts));
|
|
163
173
|
}
|
|
164
174
|
/** Every locale's draft and published trees, deduped by version id. */
|
|
165
175
|
liveTrees(page) {
|
|
@@ -267,6 +277,14 @@ export class MemoryPageStore {
|
|
|
267
277
|
const page = this.pages.get(id);
|
|
268
278
|
return page ? this.snapshot(page) : null;
|
|
269
279
|
}
|
|
280
|
+
async scanTrees() {
|
|
281
|
+
const tree = (id) => (id ? structuredClone(this.versions.get(id)?.tree ?? null) : null);
|
|
282
|
+
return (await this.listPages()).map((record) => ({
|
|
283
|
+
record,
|
|
284
|
+
draft: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.draftVersionId)])),
|
|
285
|
+
live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [l, tree(row.publishedVersionId)])),
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
270
288
|
async listPages() {
|
|
271
289
|
return [...this.pages.values()]
|
|
272
290
|
// null sorts last; the SQL equivalent is ORDER BY sort ASC NULLS LAST, created_at ASC.
|
|
@@ -282,6 +300,12 @@ export class MemoryPageStore {
|
|
|
282
300
|
pathsOf(id) {
|
|
283
301
|
return this.paths.pathsOf({ kind: "page", id });
|
|
284
302
|
}
|
|
303
|
+
targetPaths(target) { return this.paths.pathsOf(target); }
|
|
304
|
+
async redirectsOf(id) { return groupRedirects(await this.paths.redirectsOf({ kind: "page", id })); }
|
|
305
|
+
listLive(locale) { return this.paths.listLive(locale); }
|
|
306
|
+
listRedirects(locale) { return this.paths.listRedirects(locale); }
|
|
307
|
+
addRedirect(row) { return this.paths.addRedirect(row); }
|
|
308
|
+
removeRedirect(locale, path) { return this.paths.removeRedirect(locale, path); }
|
|
285
309
|
async addLocale(id, locale, options, by) {
|
|
286
310
|
assertSupportedLocale(locale, this.opts.locales);
|
|
287
311
|
const page = this.must(id);
|
|
@@ -333,7 +357,7 @@ export class MemoryPageStore {
|
|
|
333
357
|
throw pageErrors.localeInUse(locale, blockers.map((b) => b.locales[locale].title));
|
|
334
358
|
delete page.locales[locale];
|
|
335
359
|
try {
|
|
336
|
-
await this.writePaths([page]);
|
|
360
|
+
await this.writePaths([page], false);
|
|
337
361
|
}
|
|
338
362
|
catch (e) {
|
|
339
363
|
page.locales[locale] = removed;
|
|
@@ -343,7 +367,7 @@ export class MemoryPageStore {
|
|
|
343
367
|
this.writeRefs(page);
|
|
344
368
|
return this.snapshot(page);
|
|
345
369
|
}
|
|
346
|
-
async rename(id, patch, by) {
|
|
370
|
+
async rename(id, patch, by, options) {
|
|
347
371
|
const page = this.must(id);
|
|
348
372
|
const row = localeRow(page, patch.locale);
|
|
349
373
|
if (patch.slug !== undefined)
|
|
@@ -366,7 +390,7 @@ export class MemoryPageStore {
|
|
|
366
390
|
if (patch.title !== undefined)
|
|
367
391
|
row.title = patch.title;
|
|
368
392
|
try {
|
|
369
|
-
await this.writePaths(this.subtree(id));
|
|
393
|
+
await this.writePaths(this.subtree(id), options?.redirect ?? true);
|
|
370
394
|
}
|
|
371
395
|
catch (e) {
|
|
372
396
|
Object.assign(row, before);
|
|
@@ -375,7 +399,7 @@ export class MemoryPageStore {
|
|
|
375
399
|
touchRow(row, by);
|
|
376
400
|
return this.snapshot(page);
|
|
377
401
|
}
|
|
378
|
-
async move(id, to) {
|
|
402
|
+
async move(id, to, options) {
|
|
379
403
|
const page = this.must(id);
|
|
380
404
|
if (to.parentId !== null) {
|
|
381
405
|
const parent = this.must(to.parentId);
|
|
@@ -409,7 +433,7 @@ export class MemoryPageStore {
|
|
|
409
433
|
const previousSort = new Map(siblings.map((s) => [s.id, s.sort]));
|
|
410
434
|
siblings.forEach((s, i) => (s.sort = i + 1));
|
|
411
435
|
try {
|
|
412
|
-
await this.writePaths(this.subtree(id));
|
|
436
|
+
await this.writePaths(this.subtree(id), options?.redirect ?? true);
|
|
413
437
|
}
|
|
414
438
|
catch (e) {
|
|
415
439
|
Object.assign(page, before);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
-
import type { AddEntryLocaleOptions, CreateEntryOptions, EntryRecord, EntryStore, EntryStoreOptions, EntryUsage, EntryVersion, ListOptions } from "./entry-store.ts";
|
|
3
|
+
import type { AddEntryLocaleOptions, CreateEntryOptions, EntryRecord, EntryStore, EntryStoreOptions, EntryScan, EntryUsage, EntryVersion, ListOptions } from "./entry-store.ts";
|
|
4
4
|
import { type DraftToken } from "./draft-token.ts";
|
|
5
5
|
export declare class SupabaseEntryStore implements EntryStore {
|
|
6
6
|
private db;
|
|
@@ -61,21 +61,27 @@ export declare class SupabaseEntryStore implements EntryStore {
|
|
|
61
61
|
getMany(collection: string, ids: string[], locale: string, options?: {
|
|
62
62
|
status?: "published";
|
|
63
63
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
64
|
-
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null
|
|
64
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null, options?: {
|
|
65
|
+
redirect?: boolean;
|
|
66
|
+
}): Promise<EntryRecord>;
|
|
65
67
|
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
66
68
|
discardDraft(collection: string, id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
67
69
|
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
68
70
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
71
|
+
scanEntries(): Promise<EntryScan[]>;
|
|
69
72
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
70
73
|
restore(collection: string, id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
71
74
|
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
72
75
|
move(collection: string, id: string, to: {
|
|
73
76
|
parentId: string | null;
|
|
74
77
|
index?: number;
|
|
78
|
+
}, options?: {
|
|
79
|
+
redirect?: boolean;
|
|
75
80
|
}): Promise<EntryRecord>;
|
|
76
81
|
delete(collection: string, id: string): Promise<void>;
|
|
77
|
-
resolvePath(locale: string, path: string): Promise<import("./
|
|
82
|
+
resolvePath(locale: string, path: string): Promise<import("./path-index.ts").PathHit | null>;
|
|
78
83
|
pathsOf(collection: string, id: string): Promise<Record<string, string>>;
|
|
84
|
+
redirectsOf(collection: string, id: string): Promise<Record<string, string[]>>;
|
|
79
85
|
reorder(collection: string, orderedIds: string[]): Promise<void>;
|
|
80
86
|
incoming(targetId: string): Promise<{
|
|
81
87
|
sourceKind: string;
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// calls the smoodly_reorder SQL function: one statement, updated_at
|
|
8
8
|
// untouched.
|
|
9
9
|
import { collectionDepth, collectionOrder } from "./collections.js";
|
|
10
|
-
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isGlobalSchema, schemaOf, splitFields, } from "./entry-store.js";
|
|
10
|
+
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isGlobalSchema, keepLocalizedShared, schemaOf, splitFields, } from "./entry-store.js";
|
|
11
11
|
import { globalEntryId } from "./global-id.js";
|
|
12
12
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
13
|
+
import { groupRedirects } from "./store.js";
|
|
13
14
|
import { assertFresh, draftToken, StaleDraftError } from "./draft-token.js";
|
|
14
|
-
import { assertDepth, assertSupportedLocale, entryPathRows, slugOf } from "./paths.js";
|
|
15
|
+
import { assertDepth, assertSupportedLocale, entryPathRows, rewriteTargets, slugOf } from "./paths.js";
|
|
15
16
|
// entries.id is a Postgres uuid column: filtering on a non-UUID string
|
|
16
17
|
// throws invalid_text_representation. A non-UUID id can't exist in the
|
|
17
18
|
// table, so treat it the same as a missing one.
|
|
@@ -151,11 +152,11 @@ export class SupabaseEntryStore {
|
|
|
151
152
|
return new Map(res.data.map((r) => [r.id, r.fields]));
|
|
152
153
|
}
|
|
153
154
|
// ── the derived indexes ──
|
|
154
|
-
async writePaths(top) {
|
|
155
|
+
async writePaths(top, redirect = true) {
|
|
155
156
|
const schema = this.schema(top.collection);
|
|
156
157
|
const ancestors = await this.chain(top);
|
|
157
158
|
const nodes = await this.subtree(top);
|
|
158
|
-
await this.paths.rewrite(
|
|
159
|
+
await this.paths.rewrite(rewriteTargets("entry", nodes, redirect), entryPathRows(nodes, this.byIdMap([...ancestors, ...nodes]), schema, this.locales));
|
|
159
160
|
}
|
|
160
161
|
/** Every field set the site can render: each locale's live view, plus a
|
|
161
162
|
* published snapshot the draft has moved past (see entryRefEdges). */
|
|
@@ -462,7 +463,7 @@ export class SupabaseEntryStore {
|
|
|
462
463
|
}
|
|
463
464
|
return out;
|
|
464
465
|
}
|
|
465
|
-
async update(collection, id, locale, fields, by, expected) {
|
|
466
|
+
async update(collection, id, locale, fields, by, expected, options) {
|
|
466
467
|
const schema = this.schema(collection);
|
|
467
468
|
const before = await this.must(collection, id);
|
|
468
469
|
const row = entryLocaleRow(before, locale);
|
|
@@ -472,15 +473,16 @@ export class SupabaseEntryStore {
|
|
|
472
473
|
if (isGlobalSchema(schema) && slug !== null)
|
|
473
474
|
throw entryErrors.noSlug(collection);
|
|
474
475
|
await this.assertSiblingSlug(collection, before.parentId, locale, slug, id);
|
|
476
|
+
const nodeFields = keepLocalizedShared(before.fields, shared, schema);
|
|
475
477
|
// The snapshot of what the row is about to hold, written under the guard BEFORE the live
|
|
476
478
|
// columns change (spec 2026-09-26 concurrency §3): a stale save leaves nothing behind.
|
|
477
|
-
const next = { ...before, fields:
|
|
479
|
+
const next = { ...before, fields: nodeFields, locales: { ...before.locales, [locale]: { ...row, fields: localized, slug } } };
|
|
478
480
|
const { created } = await this.recordVersion(next, locale, expected !== undefined);
|
|
479
|
-
await this.updateNode(id, { fields:
|
|
481
|
+
await this.updateNode(id, { fields: nodeFields });
|
|
480
482
|
await this.updateLocale(id, locale, { slug, fields: localized }, { by });
|
|
481
483
|
const record = await this.must(collection, id);
|
|
482
484
|
try {
|
|
483
|
-
await this.writePaths(record);
|
|
485
|
+
await this.writePaths(record, options?.redirect ?? true);
|
|
484
486
|
}
|
|
485
487
|
catch (e) {
|
|
486
488
|
await this.updateNode(id, { fields: before.fields, updated_at: new Date(before.updatedAt).toISOString() }, { touch: false });
|
|
@@ -534,7 +536,7 @@ export class SupabaseEntryStore {
|
|
|
534
536
|
const matched = await this.guardedUpdateLocale(id, locale, { fields: localized, draft_version_id: row.publishedVersionId }, { by, guard: expected !== undefined ? row.draftVersionId : undefined });
|
|
535
537
|
if (!matched)
|
|
536
538
|
throw await this.stale(collection, id, locale);
|
|
537
|
-
await this.updateNode(id, { fields: shared });
|
|
539
|
+
await this.updateNode(id, { fields: keepLocalizedShared(record.fields, shared, schema) });
|
|
538
540
|
if (row.draftVersionId && row.draftVersionId !== row.publishedVersionId) {
|
|
539
541
|
// The pointer moved first, so a failure here leaves a stray row, never a dangling pointer.
|
|
540
542
|
const gone = await this.db.from("entry_versions").delete().eq("id", row.draftVersionId);
|
|
@@ -591,12 +593,32 @@ export class SupabaseEntryStore {
|
|
|
591
593
|
// locale until the next write, never leaked content.
|
|
592
594
|
await this.deleteLocale(id, locale);
|
|
593
595
|
const remaining = await this.must(collection, id);
|
|
594
|
-
await this.writePaths(remaining);
|
|
596
|
+
await this.writePaths(remaining, false);
|
|
595
597
|
await this.updateNode(id, {});
|
|
596
598
|
const final = await this.must(collection, id);
|
|
597
599
|
await this.writeRefs(final);
|
|
598
600
|
return final;
|
|
599
601
|
}
|
|
602
|
+
async scanEntries() {
|
|
603
|
+
const res = await this.db.from("entries").select(SELECT).order("created_at", { ascending: true }).order("id", { ascending: true });
|
|
604
|
+
if (res.error)
|
|
605
|
+
this.fail(res.error.message);
|
|
606
|
+
const records = res.data.map(toRecord);
|
|
607
|
+
const ids = records.flatMap((r) => Object.values(r.locales).flatMap((row) => (row.status === "published" && row.publishedVersionId ? [row.publishedVersionId] : [])));
|
|
608
|
+
// In chunks: one in() over hundreds of ids would overflow the request URL.
|
|
609
|
+
const snaps = new Map();
|
|
610
|
+
for (let i = 0; i < ids.length; i += 100) {
|
|
611
|
+
for (const [id, fields] of await this.versionFields(ids.slice(i, i + 100)))
|
|
612
|
+
snaps.set(id, fields);
|
|
613
|
+
}
|
|
614
|
+
return records.map((record) => ({
|
|
615
|
+
record,
|
|
616
|
+
live: Object.fromEntries(Object.entries(record.locales).map(([l, row]) => [
|
|
617
|
+
l,
|
|
618
|
+
row.status !== "published" ? null : (row.publishedVersionId ? snaps.get(row.publishedVersionId) : undefined) ?? entryFieldsIn(record, l),
|
|
619
|
+
])),
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
600
622
|
async listVersions(collection, id, locale) {
|
|
601
623
|
await this.must(collection, id);
|
|
602
624
|
const res = await this.db
|
|
@@ -620,7 +642,8 @@ export class SupabaseEntryStore {
|
|
|
620
642
|
assertFresh(row, expected);
|
|
621
643
|
if (versionId === row.publishedVersionId)
|
|
622
644
|
return this.discardDraft(collection, id, locale, by, expected);
|
|
623
|
-
const { shared, localized } = splitFields(version.fields, schema);
|
|
645
|
+
const { shared: split, localized } = splitFields(version.fields, schema);
|
|
646
|
+
const shared = keepLocalizedShared(entry.fields, split, schema);
|
|
624
647
|
// The snapshot first, under the guard, as in update.
|
|
625
648
|
const next = { ...entry, fields: shared, locales: { ...entry.locales, [locale]: { ...row, fields: localized } } };
|
|
626
649
|
await this.recordVersion(next, locale, expected !== undefined);
|
|
@@ -639,7 +662,7 @@ export class SupabaseEntryStore {
|
|
|
639
662
|
this.fail(res.error.message);
|
|
640
663
|
return res.data ? toVersion(res.data) : null;
|
|
641
664
|
}
|
|
642
|
-
async move(collection, id, to) {
|
|
665
|
+
async move(collection, id, to, options) {
|
|
643
666
|
if (isGlobalSchema(this.schema(collection)))
|
|
644
667
|
throw entryErrors.owned(collection, "it has no tree position");
|
|
645
668
|
const entry = await this.must(collection, id);
|
|
@@ -671,7 +694,7 @@ export class SupabaseEntryStore {
|
|
|
671
694
|
const ordered = siblings.map((s) => s.id);
|
|
672
695
|
ordered.splice(Math.min(Math.max(to.index ?? siblings.length, 0), siblings.length), 0, id);
|
|
673
696
|
await this.reorder(collection, ordered);
|
|
674
|
-
await this.writePaths(await this.must(collection, id));
|
|
697
|
+
await this.writePaths(await this.must(collection, id), options?.redirect ?? true);
|
|
675
698
|
}
|
|
676
699
|
catch (e) {
|
|
677
700
|
await this.updateNode(id, { parent_id: entry.parentId, sort: entry.sort }, { touch: false });
|
|
@@ -713,6 +736,9 @@ export class SupabaseEntryStore {
|
|
|
713
736
|
async pathsOf(collection, id) {
|
|
714
737
|
return (await this.fetch(collection, id)) ? this.paths.pathsOf({ kind: "entry", id }) : {};
|
|
715
738
|
}
|
|
739
|
+
async redirectsOf(collection, id) {
|
|
740
|
+
return (await this.fetch(collection, id)) ? groupRedirects(await this.paths.redirectsOf({ kind: "entry", id })) : {};
|
|
741
|
+
}
|
|
716
742
|
async reorder(collection, orderedIds) {
|
|
717
743
|
if (isGlobalSchema(this.schema(collection)))
|
|
718
744
|
throw entryErrors.owned(collection, "it has no order");
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { type PathIndex, type PathRow, type PathTarget } from "./path-index.ts";
|
|
2
|
+
import { type PathHit, type PathIndex, type PathRow, type PathTarget, type RewriteTarget } from "./path-index.ts";
|
|
3
3
|
export declare class SupabasePathIndex implements PathIndex {
|
|
4
4
|
private db;
|
|
5
5
|
constructor(db: SupabaseClient);
|
|
6
6
|
private fail;
|
|
7
|
-
rewrite(targets:
|
|
7
|
+
rewrite(targets: RewriteTarget[], rows: PathRow[]): Promise<void>;
|
|
8
8
|
remove(targets: PathTarget[]): Promise<void>;
|
|
9
|
-
resolve(locale: string, path: string): Promise<
|
|
9
|
+
resolve(locale: string, path: string): Promise<PathHit | null>;
|
|
10
|
+
private rowsOf;
|
|
11
|
+
private rowsIn;
|
|
10
12
|
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
13
|
+
redirectsOf(target: PathTarget): Promise<PathRow[]>;
|
|
14
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
15
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
16
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
17
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
11
18
|
}
|