smoodly 0.0.27 → 0.0.28
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 +32 -0
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/editor/EditorView.js +4 -1
- package/dist/admin/editor/PageInspector.d.ts +4 -1
- package/dist/admin/editor/PageInspector.js +2 -2
- package/dist/admin/editor/PageSettings.d.ts +4 -1
- package/dist/admin/editor/PageSettings.js +16 -8
- package/dist/admin/ops-impl.js +86 -7
- package/dist/admin/ops.d.ts +40 -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/EntryForm.js +13 -5
- package/dist/admin/shell/IconRail.d.ts +1 -1
- package/dist/admin/shell/IconRail.js +3 -2
- package/dist/admin/shell/PagesList.js +6 -4
- 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 +18 -14
- 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 +12 -3
- package/dist/admin/shell/settings.js +13 -9
- package/dist/admin/ui/ConfirmDialog.d.ts +1 -1
- package/dist/admin/ui/ConfirmDialog.js +4 -3
- package/dist/admin/ui/confirm.d.ts +13 -2
- package/dist/admin/ui/confirm.js +9 -5
- package/dist/admin/ui/theme.js +3 -0
- package/dist/entry-store.d.ts +18 -6
- package/dist/entry-store.js +13 -8
- 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/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 +30 -5
- package/dist/store.js +24 -8
- package/dist/supabase-entry-store.d.ts +7 -2
- package/dist/supabase-entry-store.js +12 -8
- package/dist/supabase-path-index.d.ts +10 -3
- package/dist/supabase-path-index.js +50 -17
- package/dist/supabase-store.d.ts +13 -2
- package/dist/supabase-store.js +15 -8
- package/migrations/20260926150000_smoodly_v4.sql +41 -0
- package/package.json +1 -1
|
@@ -10,8 +10,9 @@ import { collectionDepth, collectionOrder } from "./collections.js";
|
|
|
10
10
|
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isGlobalSchema, 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);
|
|
@@ -480,7 +481,7 @@ export class SupabaseEntryStore {
|
|
|
480
481
|
await this.updateLocale(id, locale, { slug, fields: localized }, { by });
|
|
481
482
|
const record = await this.must(collection, id);
|
|
482
483
|
try {
|
|
483
|
-
await this.writePaths(record);
|
|
484
|
+
await this.writePaths(record, options?.redirect ?? true);
|
|
484
485
|
}
|
|
485
486
|
catch (e) {
|
|
486
487
|
await this.updateNode(id, { fields: before.fields, updated_at: new Date(before.updatedAt).toISOString() }, { touch: false });
|
|
@@ -591,7 +592,7 @@ export class SupabaseEntryStore {
|
|
|
591
592
|
// locale until the next write, never leaked content.
|
|
592
593
|
await this.deleteLocale(id, locale);
|
|
593
594
|
const remaining = await this.must(collection, id);
|
|
594
|
-
await this.writePaths(remaining);
|
|
595
|
+
await this.writePaths(remaining, false);
|
|
595
596
|
await this.updateNode(id, {});
|
|
596
597
|
const final = await this.must(collection, id);
|
|
597
598
|
await this.writeRefs(final);
|
|
@@ -639,7 +640,7 @@ export class SupabaseEntryStore {
|
|
|
639
640
|
this.fail(res.error.message);
|
|
640
641
|
return res.data ? toVersion(res.data) : null;
|
|
641
642
|
}
|
|
642
|
-
async move(collection, id, to) {
|
|
643
|
+
async move(collection, id, to, options) {
|
|
643
644
|
if (isGlobalSchema(this.schema(collection)))
|
|
644
645
|
throw entryErrors.owned(collection, "it has no tree position");
|
|
645
646
|
const entry = await this.must(collection, id);
|
|
@@ -671,7 +672,7 @@ export class SupabaseEntryStore {
|
|
|
671
672
|
const ordered = siblings.map((s) => s.id);
|
|
672
673
|
ordered.splice(Math.min(Math.max(to.index ?? siblings.length, 0), siblings.length), 0, id);
|
|
673
674
|
await this.reorder(collection, ordered);
|
|
674
|
-
await this.writePaths(await this.must(collection, id));
|
|
675
|
+
await this.writePaths(await this.must(collection, id), options?.redirect ?? true);
|
|
675
676
|
}
|
|
676
677
|
catch (e) {
|
|
677
678
|
await this.updateNode(id, { parent_id: entry.parentId, sort: entry.sort }, { touch: false });
|
|
@@ -713,6 +714,9 @@ export class SupabaseEntryStore {
|
|
|
713
714
|
async pathsOf(collection, id) {
|
|
714
715
|
return (await this.fetch(collection, id)) ? this.paths.pathsOf({ kind: "entry", id }) : {};
|
|
715
716
|
}
|
|
717
|
+
async redirectsOf(collection, id) {
|
|
718
|
+
return (await this.fetch(collection, id)) ? groupRedirects(await this.paths.redirectsOf({ kind: "entry", id })) : {};
|
|
719
|
+
}
|
|
716
720
|
async reorder(collection, orderedIds) {
|
|
717
721
|
if (isGlobalSchema(this.schema(collection)))
|
|
718
722
|
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
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { pathTakenError } from "./path-index.js";
|
|
1
|
+
import { pathLiveError, pathTakenError } from "./path-index.js";
|
|
2
2
|
const UNIQUE_VIOLATION = "23505";
|
|
3
|
+
const toRow = (r) => ({ kind: r.kind, id: r.target_id, locale: r.locale, path: r.path });
|
|
3
4
|
export class SupabasePathIndex {
|
|
4
5
|
constructor(db) {
|
|
5
6
|
this.db = db;
|
|
@@ -9,7 +10,7 @@ export class SupabasePathIndex {
|
|
|
9
10
|
}
|
|
10
11
|
async rewrite(targets, rows) {
|
|
11
12
|
const res = await this.db.rpc("smoodly_rewrite_paths", {
|
|
12
|
-
p_targets: targets.map((t) => ({ kind: t.kind, id: t.id })),
|
|
13
|
+
p_targets: targets.map((t) => ({ kind: t.kind, id: t.id, ...(t.redirectLocales ? { redirect_locales: [...t.redirectLocales] } : {}) })),
|
|
13
14
|
p_rows: rows.map((r) => ({ locale: r.locale, path: r.path, kind: r.kind, id: r.id })),
|
|
14
15
|
});
|
|
15
16
|
if (res.error) {
|
|
@@ -22,31 +23,63 @@ export class SupabasePathIndex {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
async remove(targets) {
|
|
25
|
-
|
|
26
|
+
// Every row of the targets, redirects included: a delete on the target index, not the RPC (which spares redirect rows).
|
|
27
|
+
for (const t of targets) {
|
|
28
|
+
const res = await this.db.from("paths").delete().eq("kind", t.kind).eq("target_id", t.id);
|
|
29
|
+
if (res.error)
|
|
30
|
+
this.fail(res.error.message);
|
|
31
|
+
}
|
|
26
32
|
}
|
|
27
33
|
async resolve(locale, path) {
|
|
28
|
-
const res = await this.db
|
|
29
|
-
.from("paths")
|
|
30
|
-
.select("kind, target_id")
|
|
31
|
-
.eq("locale", locale)
|
|
32
|
-
.eq("path", path)
|
|
33
|
-
.maybeSingle();
|
|
34
|
+
const res = await this.db.from("paths").select("kind, target_id, redirect").eq("locale", locale).eq("path", path).maybeSingle();
|
|
34
35
|
if (res.error)
|
|
35
36
|
this.fail(res.error.message);
|
|
36
37
|
const row = res.data;
|
|
37
|
-
return row ? { kind: row.kind, id: row.target_id } : null;
|
|
38
|
+
return row ? { kind: row.kind, id: row.target_id, redirect: row.redirect } : null;
|
|
38
39
|
}
|
|
39
|
-
async
|
|
40
|
-
const res = await this.db
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
async rowsOf(target, redirect) {
|
|
41
|
+
const res = await this.db.from("paths").select("locale, path, kind, target_id").eq("kind", target.kind).eq("target_id", target.id).eq("redirect", redirect);
|
|
42
|
+
if (res.error)
|
|
43
|
+
this.fail(res.error.message);
|
|
44
|
+
return res.data.map(toRow);
|
|
45
|
+
}
|
|
46
|
+
async rowsIn(locale, redirect) {
|
|
47
|
+
const res = await this.db.from("paths").select("locale, path, kind, target_id").eq("locale", locale).eq("redirect", redirect).order("path");
|
|
45
48
|
if (res.error)
|
|
46
49
|
this.fail(res.error.message);
|
|
50
|
+
return res.data.map(toRow);
|
|
51
|
+
}
|
|
52
|
+
async pathsOf(target) {
|
|
47
53
|
const out = {};
|
|
48
|
-
for (const r of
|
|
54
|
+
for (const r of await this.rowsOf(target, false))
|
|
49
55
|
out[r.locale] = r.path;
|
|
50
56
|
return out;
|
|
51
57
|
}
|
|
58
|
+
redirectsOf(target) {
|
|
59
|
+
return this.rowsOf(target, true);
|
|
60
|
+
}
|
|
61
|
+
listLive(locale) {
|
|
62
|
+
return this.rowsIn(locale, false);
|
|
63
|
+
}
|
|
64
|
+
listRedirects(locale) {
|
|
65
|
+
return this.rowsIn(locale, true);
|
|
66
|
+
}
|
|
67
|
+
async addRedirect(row) {
|
|
68
|
+
const existing = await this.resolve(row.locale, row.path);
|
|
69
|
+
if (existing && !existing.redirect)
|
|
70
|
+
throw pathLiveError(row.locale, row.path);
|
|
71
|
+
if (existing)
|
|
72
|
+
await this.removeRedirect(row.locale, row.path);
|
|
73
|
+
const res = await this.db.from("paths").insert({ locale: row.locale, path: row.path, kind: row.kind, target_id: row.id, redirect: true });
|
|
74
|
+
if (res.error) {
|
|
75
|
+
if (res.error.code === UNIQUE_VIOLATION)
|
|
76
|
+
throw pathLiveError(row.locale, row.path);
|
|
77
|
+
this.fail(res.error.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async removeRedirect(locale, path) {
|
|
81
|
+
const res = await this.db.from("paths").delete().eq("locale", locale).eq("path", path).eq("redirect", true);
|
|
82
|
+
if (res.error)
|
|
83
|
+
this.fail(res.error.message);
|
|
84
|
+
}
|
|
52
85
|
}
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { type AddLocaleOptions, type CreatePageInput, type PageRecord, type PageStore, type PageStoreOptions, type PageVersion } from "./store.ts";
|
|
3
3
|
import type { PageTree } from "./render.tsx";
|
|
4
|
+
import type { PathRow } from "./path-index.ts";
|
|
4
5
|
import { type DraftToken } from "./draft-token.ts";
|
|
5
6
|
import { type PathTarget } from "./paths.ts";
|
|
6
7
|
export declare class SupabasePageStore implements PageStore {
|
|
@@ -60,19 +61,29 @@ export declare class SupabasePageStore implements PageStore {
|
|
|
60
61
|
listPages(): Promise<PageRecord[]>;
|
|
61
62
|
/** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
|
|
62
63
|
private schemaChecked;
|
|
63
|
-
resolvePath(locale: string, path: string): Promise<
|
|
64
|
+
resolvePath(locale: string, path: string): Promise<import("./path-index.ts").PathHit | null>;
|
|
64
65
|
schemaVersion(): Promise<number | null>;
|
|
65
66
|
pathsOf(id: string): Promise<{}>;
|
|
67
|
+
targetPaths(target: PathTarget): Promise<Record<string, string>>;
|
|
68
|
+
redirectsOf(id: string): Promise<Record<string, string[]>>;
|
|
69
|
+
listLive(locale: string): Promise<PathRow[]>;
|
|
70
|
+
listRedirects(locale: string): Promise<PathRow[]>;
|
|
71
|
+
addRedirect(row: PathRow): Promise<void>;
|
|
72
|
+
removeRedirect(locale: string, path: string): Promise<void>;
|
|
66
73
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
67
74
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
|
68
75
|
rename(id: string, patch: {
|
|
69
76
|
locale: string;
|
|
70
77
|
slug?: string;
|
|
71
78
|
title?: string;
|
|
72
|
-
}, by?: string
|
|
79
|
+
}, by?: string, options?: {
|
|
80
|
+
redirect?: boolean;
|
|
81
|
+
}): Promise<PageRecord>;
|
|
73
82
|
move(id: string, to: {
|
|
74
83
|
parentId: string | null;
|
|
75
84
|
index?: number;
|
|
85
|
+
}, options?: {
|
|
86
|
+
redirect?: boolean;
|
|
76
87
|
}): Promise<PageRecord>;
|
|
77
88
|
setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
|
|
78
89
|
saveDraft(id: string, locale: string, tree: PageTree, by?: string, expected?: DraftToken | null): Promise<PageVersion>;
|
package/dist/supabase-store.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
import { assertPublishable, assertSupportedLocale, localeRow, pageErrors, pageRefEdges, pathOptions, refSections, } from "./store.js";
|
|
10
10
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
11
11
|
import { SCHEMA_VERSION_KEY, schemaBehind, schemaBehindMessage } from "./schema-version.js";
|
|
12
|
+
import { groupRedirects } from "./store.js";
|
|
12
13
|
import { assertFresh, draftToken, StaleDraftError } from "./draft-token.js";
|
|
13
|
-
import { assertDepth, assertSegment, isHomeNode, pagePathRows } from "./paths.js";
|
|
14
|
+
import { assertDepth, assertSegment, isHomeNode, pagePathRows, rewriteTargets } from "./paths.js";
|
|
14
15
|
const SELECT = "*, page_locales(*, draft:page_versions!page_locales_draft_version_fkey(saved_at))";
|
|
15
16
|
const UNIQUE_VIOLATION = "23505";
|
|
16
17
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -121,11 +122,11 @@ export class SupabasePageStore {
|
|
|
121
122
|
}
|
|
122
123
|
/** Rewrite path rows for a subtree: ancestors are loaded once so the
|
|
123
124
|
* chain lookup is a map, then ONE RPC replaces every target's rows. */
|
|
124
|
-
async writePaths(top) {
|
|
125
|
+
async writePaths(top, redirect = true) {
|
|
125
126
|
const ancestors = await this.chain(top);
|
|
126
127
|
const nodes = await this.subtree(top);
|
|
127
128
|
const byId = new Map([...ancestors, ...nodes].map((n) => [n.id, n]));
|
|
128
|
-
await this.paths.rewrite(
|
|
129
|
+
await this.paths.rewrite(rewriteTargets("page", nodes, redirect), pagePathRows(nodes, (id) => byId.get(id), this.opts));
|
|
129
130
|
}
|
|
130
131
|
/** `touch` defaults to true (the normal edit path bumps updated_at).
|
|
131
132
|
* A rollback restoring prior column values passes `{ touch: false }`,
|
|
@@ -358,6 +359,12 @@ export class SupabasePageStore {
|
|
|
358
359
|
pathsOf(id) {
|
|
359
360
|
return UUID_RE.test(id) ? this.paths.pathsOf({ kind: "page", id }) : Promise.resolve({});
|
|
360
361
|
}
|
|
362
|
+
targetPaths(target) { return this.paths.pathsOf(target); }
|
|
363
|
+
async redirectsOf(id) { return UUID_RE.test(id) ? groupRedirects(await this.paths.redirectsOf({ kind: "page", id })) : {}; }
|
|
364
|
+
listLive(locale) { return this.paths.listLive(locale); }
|
|
365
|
+
listRedirects(locale) { return this.paths.listRedirects(locale); }
|
|
366
|
+
addRedirect(row) { return this.paths.addRedirect(row); }
|
|
367
|
+
removeRedirect(locale, path) { return this.paths.removeRedirect(locale, path); }
|
|
361
368
|
async addLocale(id, locale, options, by) {
|
|
362
369
|
assertSupportedLocale(locale, this.opts.locales);
|
|
363
370
|
const page = await this.must(id);
|
|
@@ -403,11 +410,11 @@ export class SupabasePageStore {
|
|
|
403
410
|
// content — until the next write of this node fixes it.
|
|
404
411
|
await this.deleteLocale(id, locale);
|
|
405
412
|
const remaining = await this.must(id);
|
|
406
|
-
await this.writePaths(remaining);
|
|
413
|
+
await this.writePaths(remaining, false);
|
|
407
414
|
await this.writeRefs(remaining);
|
|
408
415
|
return remaining;
|
|
409
416
|
}
|
|
410
|
-
async rename(id, patch, by) {
|
|
417
|
+
async rename(id, patch, by, options) {
|
|
411
418
|
const page = await this.must(id);
|
|
412
419
|
const row = localeRow(page, patch.locale);
|
|
413
420
|
if (patch.slug !== undefined)
|
|
@@ -431,7 +438,7 @@ export class SupabasePageStore {
|
|
|
431
438
|
columns.title = patch.title;
|
|
432
439
|
const renamed = await this.updateLocale(id, patch.locale, columns, { by });
|
|
433
440
|
try {
|
|
434
|
-
await this.writePaths(renamed);
|
|
441
|
+
await this.writePaths(renamed, options?.redirect ?? true);
|
|
435
442
|
}
|
|
436
443
|
catch (e) {
|
|
437
444
|
// The audit pair goes back too: a rejected rename is not an edit.
|
|
@@ -440,7 +447,7 @@ export class SupabasePageStore {
|
|
|
440
447
|
}
|
|
441
448
|
return renamed;
|
|
442
449
|
}
|
|
443
|
-
async move(id, to) {
|
|
450
|
+
async move(id, to, options) {
|
|
444
451
|
const page = await this.must(id);
|
|
445
452
|
const subtree = await this.subtree(page);
|
|
446
453
|
if (to.parentId !== null) {
|
|
@@ -485,7 +492,7 @@ export class SupabasePageStore {
|
|
|
485
492
|
ordered.splice(index, 0, id);
|
|
486
493
|
try {
|
|
487
494
|
await this.reorder(ordered);
|
|
488
|
-
await this.writePaths(moved);
|
|
495
|
+
await this.writePaths(moved, options?.redirect ?? true);
|
|
489
496
|
}
|
|
490
497
|
catch (e) {
|
|
491
498
|
await this.updateNode(id, { parent_id: page.parentId, sort: page.sort }, { touch: false });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Smoodly core schema, version 4 (docs/superpowers/specs/2026-09-26-redirects-design.md).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 3: a `redirect` flag on the paths index — a row
|
|
5
|
+
-- with it set is an old address of its target — and the rewrite function
|
|
6
|
+
-- keeps the published old rows as redirects when a target names their
|
|
7
|
+
-- locales. The read policy is unchanged: a redirect row is visible when
|
|
8
|
+
-- its target is published in that locale.
|
|
9
|
+
|
|
10
|
+
set client_min_messages = warning;
|
|
11
|
+
|
|
12
|
+
alter table "paths" add column if not exists "redirect" boolean not null default false;
|
|
13
|
+
|
|
14
|
+
create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql set search_path = public as $$
|
|
15
|
+
delete from "paths" p
|
|
16
|
+
using jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid)
|
|
17
|
+
where p."redirect" and p."locale" = r.locale and p."path" = r.path
|
|
18
|
+
and p."space_id" = "smoodly_current_space"();
|
|
19
|
+
update "paths" p set "redirect" = true
|
|
20
|
+
from jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
21
|
+
where not p."redirect" and p."kind" = t.kind and p."target_id" = t.id
|
|
22
|
+
and p."locale" = any (coalesce(t.redirect_locales, '{}'::text[]))
|
|
23
|
+
and not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid)
|
|
24
|
+
where r.locale = p."locale" and r.path = p."path")
|
|
25
|
+
and p."space_id" = "smoodly_current_space"();
|
|
26
|
+
delete from "paths" p
|
|
27
|
+
using jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
28
|
+
where p."kind" = t.kind and p."target_id" = t.id
|
|
29
|
+
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)
|
|
30
|
+
where r.kind = p."kind" and r.id = p."target_id" and r.locale = p."locale"))
|
|
31
|
+
and p."space_id" = "smoodly_current_space"();
|
|
32
|
+
insert into "paths" ("locale", "path", "kind", "target_id")
|
|
33
|
+
select r.locale, r.path, r.kind, r.id
|
|
34
|
+
from jsonb_to_recordset(p_rows) as r(locale text, path text, kind text, id uuid);
|
|
35
|
+
$$;
|
|
36
|
+
|
|
37
|
+
alter function "smoodly_rewrite_paths"(jsonb, jsonb) set search_path = public;
|
|
38
|
+
revoke execute on function "smoodly_rewrite_paths"(jsonb, jsonb) from public, anon;
|
|
39
|
+
|
|
40
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '4')
|
|
41
|
+
on conflict ("key") do update set "value" = excluded."value";
|
package/package.json
CHANGED