smoodly 0.0.4 → 0.0.6
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 +408 -34
- package/dist/admin/client-ops.js +2 -2
- package/dist/admin/editor/EditorView.d.ts +3 -2
- package/dist/admin/editor/EditorView.js +133 -80
- package/dist/admin/editor/PageSettings.d.ts +13 -0
- package/dist/admin/editor/PageSettings.js +40 -0
- package/dist/admin/fixed-nodes.d.ts +23 -0
- package/dist/admin/fixed-nodes.js +83 -0
- package/dist/admin/forms/FieldWidget.d.ts +2 -1
- package/dist/admin/forms/FieldWidget.js +16 -16
- package/dist/admin/index.d.ts +2 -1
- package/dist/admin/index.js +1 -0
- package/dist/admin/next/create-admin.js +7 -2
- package/dist/admin/next/op-handler.d.ts +1 -0
- package/dist/admin/next/op-handler.js +7 -2
- package/dist/admin/ops-impl.d.ts +4 -1
- package/dist/admin/ops-impl.js +190 -48
- package/dist/admin/ops.d.ts +39 -11
- package/dist/admin/serialize.d.ts +10 -1
- package/dist/admin/serialize.js +13 -2
- package/dist/admin/shell/AdminApp.js +36 -74
- package/dist/admin/shell/EntryForm.js +34 -10
- package/dist/admin/shell/ErrorPane.d.ts +4 -0
- package/dist/admin/shell/ErrorPane.js +9 -0
- package/dist/admin/shell/ListView.d.ts +8 -5
- package/dist/admin/shell/ListView.js +12 -11
- package/dist/admin/shell/LoginView.js +6 -2
- package/dist/admin/shell/PagesList.d.ts +6 -0
- package/dist/admin/shell/PagesList.js +148 -0
- package/dist/admin/shell/base.d.ts +1 -0
- package/dist/admin/shell/base.js +4 -0
- package/dist/admin/shell/pages-tree.d.ts +39 -0
- package/dist/admin/shell/pages-tree.js +65 -0
- package/dist/admin/ui/format.d.ts +1 -0
- package/dist/admin/ui/format.js +25 -0
- package/dist/admin/ui/primitives.d.ts +89 -0
- package/dist/admin/ui/primitives.js +115 -0
- package/dist/admin/ui/theme.d.ts +5 -0
- package/dist/admin/ui/theme.js +396 -0
- package/dist/collections.d.ts +23 -2
- package/dist/collections.js +19 -0
- package/dist/config.d.ts +11 -1
- package/dist/config.js +71 -13
- package/dist/entry-page.d.ts +16 -0
- package/dist/entry-page.js +6 -0
- package/dist/entry-store.d.ts +94 -7
- package/dist/entry-store.js +175 -26
- package/dist/index.d.ts +24 -8
- package/dist/index.js +14 -3
- package/dist/localize.d.ts +19 -0
- package/dist/localize.js +42 -0
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -1
- package/dist/next/meta.d.ts +0 -6
- package/dist/next/meta.js +0 -11
- package/dist/next/page-renderer.d.ts +26 -18
- package/dist/next/page-renderer.js +79 -56
- package/dist/page.d.ts +10 -3
- package/dist/page.js +12 -2
- package/dist/pairing.d.ts +7 -6
- package/dist/path-index.d.ts +16 -0
- package/dist/path-index.js +33 -0
- package/dist/paths.d.ts +80 -0
- package/dist/paths.js +110 -0
- package/dist/render.d.ts +34 -5
- package/dist/render.js +9 -2
- package/dist/resolve.d.ts +8 -0
- package/dist/resolve.js +19 -7
- package/dist/revalidate.d.ts +10 -2
- package/dist/revalidate.js +13 -8
- package/dist/site.d.ts +85 -0
- package/dist/site.js +164 -0
- package/dist/sql-space.d.ts +1 -1
- package/dist/sql-space.js +2 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +92 -15
- package/dist/store.d.ts +106 -30
- package/dist/store.js +242 -66
- package/dist/supabase-entry-store.d.ts +17 -3
- package/dist/supabase-entry-store.js +198 -22
- package/dist/supabase-path-index.d.ts +11 -0
- package/dist/supabase-path-index.js +52 -0
- package/dist/supabase-store.d.ts +46 -16
- package/dist/supabase-store.js +322 -110
- package/package.json +5 -4
- package/dist/admin/page-path.d.ts +0 -1
- package/dist/admin/page-path.js +0 -6
- package/dist/admin/shell/pages-list.d.ts +0 -22
- package/dist/admin/shell/pages-list.js +0 -31
|
@@ -2,33 +2,53 @@
|
|
|
2
2
|
// Service-role client, server-side only (writes bypass RLS by design —
|
|
3
3
|
// DESIGN.md §1). Reorder calls the smoodly_reorder SQL function: one
|
|
4
4
|
// statement for the whole collection, updated_at untouched.
|
|
5
|
-
import {
|
|
5
|
+
import { collectionDepth, collectionOrder } from "./collections.js";
|
|
6
|
+
import { DEFAULT_ENTRY_LOCALES, entryErrors, schemaOf } from "./entry-store.js";
|
|
6
7
|
import { collectEntryRefs } from "./refs.js";
|
|
8
|
+
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
9
|
+
import { assertDepth, entryPathRows, slugOf } from "./paths.js";
|
|
7
10
|
// entries.id is a Postgres uuid column: filtering on a non-UUID string
|
|
8
11
|
// throws invalid_text_representation. A non-UUID id can't exist in the
|
|
9
12
|
// table, so treat it the same as a missing one — getMany silently omits
|
|
10
13
|
// it, patch raises the contract's "no entry" error — rather than letting
|
|
11
14
|
// the cast error leak through.
|
|
12
15
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
const UNIQUE_VIOLATION = "23505";
|
|
13
17
|
function toRecord(row) {
|
|
14
18
|
return {
|
|
15
19
|
id: row.id,
|
|
16
20
|
collection: row.collection,
|
|
21
|
+
parentId: row.parent_id,
|
|
17
22
|
fields: row.fields,
|
|
23
|
+
i18n: row.i18n,
|
|
18
24
|
status: row.status,
|
|
19
25
|
sort: row.sort,
|
|
26
|
+
locales: row.locales,
|
|
20
27
|
createdAt: Date.parse(row.created_at),
|
|
21
28
|
updatedAt: Date.parse(row.updated_at),
|
|
22
29
|
};
|
|
23
30
|
}
|
|
24
31
|
export class SupabaseEntryStore {
|
|
25
|
-
constructor(db, collections) {
|
|
32
|
+
constructor(db, collections, options = {}) {
|
|
26
33
|
this.db = db;
|
|
27
34
|
this.collections = collections;
|
|
35
|
+
this.paths = options.paths ?? new SupabasePathIndex(db);
|
|
36
|
+
this.locales = options.locales ?? DEFAULT_ENTRY_LOCALES;
|
|
28
37
|
}
|
|
29
38
|
fail(message) {
|
|
30
39
|
throw new Error(`smoodly: ${message}`);
|
|
31
40
|
}
|
|
41
|
+
// entries_sibling_slug_key (space_id, collection, parent_id,
|
|
42
|
+
// fields->>'slug') enforces slug uniqueness among SIBLINGS at the DB
|
|
43
|
+
// level, ahead of the paths rewrite — a collision here never reaches
|
|
44
|
+
// writePaths, so the insert, the update and move's re-parent all need
|
|
45
|
+
// this translation. The text comes from entryErrors so the memory
|
|
46
|
+
// adapter's pre-check and this one read identically.
|
|
47
|
+
failWriteError(error, collection) {
|
|
48
|
+
if (error.code === UNIQUE_VIOLATION)
|
|
49
|
+
throw entryErrors.slugTaken(collection);
|
|
50
|
+
this.fail(error.message);
|
|
51
|
+
}
|
|
32
52
|
async writeRefs(collection, id, fields) {
|
|
33
53
|
const schema = schemaOf(this.collections, collection);
|
|
34
54
|
const deleted = await this.db
|
|
@@ -55,16 +75,85 @@ export class SupabaseEntryStore {
|
|
|
55
75
|
this.fail(inserted.error.message);
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
byIdMap(rows) {
|
|
79
|
+
const map = new Map(rows.map((r) => [r.id, r]));
|
|
80
|
+
return (id) => map.get(id);
|
|
81
|
+
}
|
|
82
|
+
async chain(entry) {
|
|
83
|
+
const chain = [entry];
|
|
84
|
+
let current = entry;
|
|
85
|
+
while (current.parentId !== null) {
|
|
86
|
+
const parent = await this.get(current.collection, current.parentId);
|
|
87
|
+
if (!parent)
|
|
88
|
+
this.fail(`broken parent chain at "${entry.id}".`);
|
|
89
|
+
chain.unshift(parent);
|
|
90
|
+
current = parent;
|
|
91
|
+
}
|
|
92
|
+
return chain;
|
|
93
|
+
}
|
|
94
|
+
async subtree(entry) {
|
|
95
|
+
const out = [entry];
|
|
96
|
+
let frontier = [entry.id];
|
|
97
|
+
while (frontier.length > 0) {
|
|
98
|
+
const res = await this.db.from("entries").select("*").eq("collection", entry.collection).in("parent_id", frontier);
|
|
99
|
+
if (res.error)
|
|
100
|
+
this.fail(res.error.message);
|
|
101
|
+
const rows = res.data.map(toRecord);
|
|
102
|
+
out.push(...rows);
|
|
103
|
+
frontier = rows.map((r) => r.id);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
async writePaths(top) {
|
|
108
|
+
const schema = schemaOf(this.collections, top.collection);
|
|
109
|
+
const ancestors = await this.chain(top);
|
|
110
|
+
const nodes = await this.subtree(top);
|
|
111
|
+
await this.paths.rewrite(nodes.map((n) => ({ kind: "entry", id: n.id })), entryPathRows(nodes, this.byIdMap([...ancestors, ...nodes]), schema, this.locales));
|
|
112
|
+
}
|
|
113
|
+
async assertParent(collection, parentId, subtreeHeight) {
|
|
114
|
+
if (parentId === null)
|
|
115
|
+
return;
|
|
116
|
+
if (!UUID_RE.test(parentId))
|
|
117
|
+
throw entryErrors.notFound(collection, parentId);
|
|
118
|
+
const res = await this.db.from("entries").select("*").eq("id", parentId).maybeSingle();
|
|
61
119
|
if (res.error)
|
|
62
120
|
this.fail(res.error.message);
|
|
63
|
-
|
|
121
|
+
if (!res.data)
|
|
122
|
+
throw entryErrors.notFound(collection, parentId);
|
|
123
|
+
const parent = toRecord(res.data);
|
|
124
|
+
if (parent.collection !== collection)
|
|
125
|
+
throw entryErrors.foreignParent();
|
|
126
|
+
assertDepth((await this.chain(parent)).length + subtreeHeight, collectionDepth(schemaOf(this.collections, collection)), "the entry");
|
|
127
|
+
}
|
|
128
|
+
async create(collection, fields, options = {}) {
|
|
129
|
+
schemaOf(this.collections, collection);
|
|
130
|
+
const parentId = options.parentId ?? null;
|
|
131
|
+
await this.assertParent(collection, parentId, 1);
|
|
132
|
+
const res = await this.db
|
|
133
|
+
.from("entries")
|
|
134
|
+
.insert({ collection, fields, parent_id: parentId, locales: options.locales ?? null })
|
|
135
|
+
.select("*")
|
|
136
|
+
.single();
|
|
137
|
+
if (res.error)
|
|
138
|
+
this.failWriteError(res.error, collection);
|
|
139
|
+
const row = toRecord(res.data);
|
|
140
|
+
try {
|
|
141
|
+
await this.writePaths(row);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
await this.db.from("entries").delete().eq("id", row.id);
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
64
147
|
await this.writeRefs(collection, row.id, fields);
|
|
65
|
-
return
|
|
148
|
+
return row;
|
|
66
149
|
}
|
|
67
150
|
async get(collection, id) {
|
|
151
|
+
// Same short-circuit as UUID_RE elsewhere: a non-UUID id can't exist
|
|
152
|
+
// in the table, and querying it as one throws invalid_text_representation
|
|
153
|
+
// instead of returning "not found" (which `update`'s rollback read and
|
|
154
|
+
// `chain`'s parent walk both rely on).
|
|
155
|
+
if (!UUID_RE.test(id))
|
|
156
|
+
return null;
|
|
68
157
|
const res = await this.db
|
|
69
158
|
.from("entries")
|
|
70
159
|
.select("*")
|
|
@@ -76,19 +165,24 @@ export class SupabaseEntryStore {
|
|
|
76
165
|
return res.data ? toRecord(res.data) : null;
|
|
77
166
|
}
|
|
78
167
|
async list(collection, options) {
|
|
79
|
-
schemaOf(this.collections, collection);
|
|
168
|
+
const schema = schemaOf(this.collections, collection);
|
|
80
169
|
let query = this.db.from("entries").select("*").eq("collection", collection);
|
|
81
170
|
if (options?.status)
|
|
82
171
|
query = query.eq("status", options.status);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
|
|
172
|
+
if (options?.parent !== undefined) {
|
|
173
|
+
query = options.parent === null ? query.is("parent_id", null) : query.eq("parent_id", options.parent);
|
|
174
|
+
}
|
|
175
|
+
const order = options?.order ?? collectionOrder(schema);
|
|
176
|
+
if (order === "manual") {
|
|
177
|
+
query = query.order("sort", { ascending: true, nullsFirst: false }).order("created_at", { ascending: true });
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// `fields->x`, not `->>x`: ordering by the jsonb VALUE, so numbers
|
|
181
|
+
// compare numerically (rank 10 after rank 2) and strings — ISO dates
|
|
182
|
+
// included — alphabetically, matching the memory comparator.
|
|
183
|
+
const column = order.by === "createdAt" ? "created_at" : order.by === "updatedAt" ? "updated_at" : `fields->${order.by}`;
|
|
184
|
+
query = query.order(column, { ascending: order.direction === "asc", nullsFirst: false }).order("id", { ascending: true });
|
|
185
|
+
}
|
|
92
186
|
const res = await query;
|
|
93
187
|
if (res.error)
|
|
94
188
|
this.fail(res.error.message);
|
|
@@ -115,35 +209,111 @@ export class SupabaseEntryStore {
|
|
|
115
209
|
}
|
|
116
210
|
return out;
|
|
117
211
|
}
|
|
118
|
-
|
|
212
|
+
// `touch` defaults to true (the normal edit path always bumps
|
|
213
|
+
// updated_at). Callers that must leave the timestamp alone — a
|
|
214
|
+
// rollback restoring a prior value, or move's re-parent, which the
|
|
215
|
+
// contract says must not count as an edit — pass `{ touch: false }`,
|
|
216
|
+
// which omits updated_at from the payload entirely so the column is
|
|
217
|
+
// left untouched (or keeps whatever value the caller put in `patch`).
|
|
218
|
+
async patch(collection, id, patch, options = {}) {
|
|
119
219
|
if (!UUID_RE.test(id))
|
|
120
|
-
|
|
220
|
+
throw entryErrors.notFound(collection, id);
|
|
221
|
+
const touch = options.touch ?? true;
|
|
222
|
+
const payload = touch ? { ...patch, updated_at: new Date().toISOString() } : { ...patch };
|
|
121
223
|
const res = await this.db
|
|
122
224
|
.from("entries")
|
|
123
|
-
.update(
|
|
225
|
+
.update(payload)
|
|
124
226
|
.eq("collection", collection)
|
|
125
227
|
.eq("id", id)
|
|
126
228
|
.select("*")
|
|
127
229
|
.maybeSingle();
|
|
128
230
|
if (res.error)
|
|
129
|
-
this.
|
|
231
|
+
this.failWriteError(res.error, collection);
|
|
130
232
|
if (!res.data)
|
|
131
|
-
|
|
233
|
+
throw entryErrors.notFound(collection, id);
|
|
132
234
|
return toRecord(res.data);
|
|
133
235
|
}
|
|
134
236
|
async update(collection, id, fields) {
|
|
237
|
+
const before = await this.get(collection, id);
|
|
238
|
+
if (!before)
|
|
239
|
+
throw entryErrors.notFound(collection, id);
|
|
135
240
|
const record = await this.patch(collection, id, { fields });
|
|
241
|
+
try {
|
|
242
|
+
await this.writePaths(record);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
await this.patch(collection, id, { fields: before.fields, updated_at: new Date(before.updatedAt).toISOString() }, { touch: false });
|
|
246
|
+
throw e;
|
|
247
|
+
}
|
|
136
248
|
await this.writeRefs(collection, id, fields);
|
|
137
249
|
return record;
|
|
138
250
|
}
|
|
139
251
|
async setStatus(collection, id, status) {
|
|
140
252
|
return this.patch(collection, id, { status });
|
|
141
253
|
}
|
|
254
|
+
async move(collection, id, to) {
|
|
255
|
+
const entry = await this.get(collection, id);
|
|
256
|
+
if (!entry)
|
|
257
|
+
throw entryErrors.notFound(collection, id);
|
|
258
|
+
const subtree = await this.subtree(entry);
|
|
259
|
+
if (to.parentId !== null && (to.parentId === id || subtree.some((e) => e.id === to.parentId)))
|
|
260
|
+
throw entryErrors.cycle();
|
|
261
|
+
const byId = this.byIdMap(subtree);
|
|
262
|
+
// chainOf walks all the way to a null parentId, but `entry`'s own
|
|
263
|
+
// parentId still points OUTSIDE this subtree map (its pre-move
|
|
264
|
+
// parent) — so it can't be used here directly (page store's `move`
|
|
265
|
+
// hits the same shape and solves it the same way): stop at `entry`
|
|
266
|
+
// itself instead, since that's the map's root for this computation.
|
|
267
|
+
const heightOf = (e) => (e.id === entry.id ? 1 : 1 + heightOf(byId(e.parentId)));
|
|
268
|
+
const height = Math.max(...subtree.map(heightOf));
|
|
269
|
+
await this.assertParent(collection, to.parentId, height);
|
|
270
|
+
const siblings = (await this.list(collection, { parent: to.parentId, order: "manual" })).filter((s) => s.id !== id);
|
|
271
|
+
// Mirror of the memory adapter's pre-check. The index below would
|
|
272
|
+
// catch it on the parent_id update anyway, but raising here keeps the
|
|
273
|
+
// error identical across adapters and leaves the destination
|
|
274
|
+
// siblings' sorts untouched.
|
|
275
|
+
if (to.parentId !== entry.parentId) {
|
|
276
|
+
const slug = slugOf(entry.fields);
|
|
277
|
+
if (slug !== null && siblings.some((s) => slugOf(s.fields) === slug)) {
|
|
278
|
+
throw entryErrors.slugTaken(collection);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// The destination siblings' sorts as they stood BEFORE the reorder.
|
|
282
|
+
// A rollback restores these exact values rather than re-running the
|
|
283
|
+
// reorder RPC, which renumbers 1..N and would silently close gaps
|
|
284
|
+
// left by earlier deletes and moves.
|
|
285
|
+
const previousSorts = siblings.map((s) => ({ id: s.id, sort: s.sort }));
|
|
286
|
+
const moved = await this.patch(collection, id, { parent_id: to.parentId }, { touch: false });
|
|
287
|
+
const ordered = siblings.map((s) => s.id);
|
|
288
|
+
ordered.splice(Math.min(Math.max(to.index ?? siblings.length, 0), siblings.length), 0, id);
|
|
289
|
+
await this.reorder(collection, ordered);
|
|
290
|
+
try {
|
|
291
|
+
await this.writePaths(moved);
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
await this.patch(collection, id, { parent_id: entry.parentId, sort: entry.sort }, { touch: false });
|
|
295
|
+
for (const s of previousSorts)
|
|
296
|
+
await this.patch(collection, s.id, { sort: s.sort }, { touch: false });
|
|
297
|
+
throw e;
|
|
298
|
+
}
|
|
299
|
+
return (await this.get(collection, id));
|
|
300
|
+
}
|
|
142
301
|
async delete(collection, id) {
|
|
302
|
+
if (!UUID_RE.test(id))
|
|
303
|
+
return;
|
|
304
|
+
const entry = await this.get(collection, id);
|
|
305
|
+
if (!entry)
|
|
306
|
+
return;
|
|
307
|
+
const children = await this.db.from("entries").select("id").eq("parent_id", id).limit(1);
|
|
308
|
+
if (children.error)
|
|
309
|
+
this.fail(children.error.message);
|
|
310
|
+
if (children.data.length > 0)
|
|
311
|
+
throw entryErrors.hasChildren(collection, id);
|
|
143
312
|
const usage = await this.referencesTo(collection, id);
|
|
144
313
|
if (usage.length > 0) {
|
|
145
314
|
this.fail(`"${collection}/${id}" is referenced by ${usage.length} document(s) — remove those references first.`);
|
|
146
315
|
}
|
|
316
|
+
await this.paths.remove([{ kind: "entry", id }]);
|
|
147
317
|
const refs = await this.db.from("refs").delete().eq("source_kind", "entry").eq("source_id", id);
|
|
148
318
|
if (refs.error)
|
|
149
319
|
this.fail(refs.error.message);
|
|
@@ -151,6 +321,12 @@ export class SupabaseEntryStore {
|
|
|
151
321
|
if (res.error)
|
|
152
322
|
this.fail(res.error.message);
|
|
153
323
|
}
|
|
324
|
+
resolvePath(locale, path) {
|
|
325
|
+
return this.paths.resolve(locale, path);
|
|
326
|
+
}
|
|
327
|
+
async pathsOf(collection, id) {
|
|
328
|
+
return (await this.get(collection, id)) ? this.paths.pathsOf({ kind: "entry", id }) : {};
|
|
329
|
+
}
|
|
154
330
|
async reorder(collection, orderedIds) {
|
|
155
331
|
schemaOf(this.collections, collection);
|
|
156
332
|
const res = await this.db.rpc("smoodly_reorder", {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { type PathIndex, type PathRow, type PathTarget } from "./path-index.ts";
|
|
3
|
+
export declare class SupabasePathIndex implements PathIndex {
|
|
4
|
+
private db;
|
|
5
|
+
constructor(db: SupabaseClient);
|
|
6
|
+
private fail;
|
|
7
|
+
rewrite(targets: PathTarget[], rows: PathRow[]): Promise<void>;
|
|
8
|
+
remove(targets: PathTarget[]): Promise<void>;
|
|
9
|
+
resolve(locale: string, path: string): Promise<PathTarget | null>;
|
|
10
|
+
pathsOf(target: PathTarget): Promise<Record<string, string>>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { pathTakenError } from "./path-index.js";
|
|
2
|
+
const UNIQUE_VIOLATION = "23505";
|
|
3
|
+
export class SupabasePathIndex {
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
fail(message) {
|
|
8
|
+
throw new Error(`smoodly: ${message}`);
|
|
9
|
+
}
|
|
10
|
+
async rewrite(targets, rows) {
|
|
11
|
+
const res = await this.db.rpc("smoodly_rewrite_paths", {
|
|
12
|
+
p_targets: targets.map((t) => ({ kind: t.kind, id: t.id })),
|
|
13
|
+
p_rows: rows.map((r) => ({ locale: r.locale, path: r.path, kind: r.kind, id: r.id })),
|
|
14
|
+
});
|
|
15
|
+
if (res.error) {
|
|
16
|
+
if (res.error.code === UNIQUE_VIOLATION) {
|
|
17
|
+
// details: Key (space_id, locale, path)=(…, fi, /yritys) already exists.
|
|
18
|
+
const m = /\(space_id, locale, path\)=\([^,]+, ([^,]+), (.+)\) already exists/.exec(res.error.details ?? "");
|
|
19
|
+
throw m ? pathTakenError(m[1], m[2]) : pathTakenError("?", "?");
|
|
20
|
+
}
|
|
21
|
+
this.fail(res.error.message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async remove(targets) {
|
|
25
|
+
await this.rewrite(targets, []);
|
|
26
|
+
}
|
|
27
|
+
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
|
+
if (res.error)
|
|
35
|
+
this.fail(res.error.message);
|
|
36
|
+
const row = res.data;
|
|
37
|
+
return row ? { kind: row.kind, id: row.target_id } : null;
|
|
38
|
+
}
|
|
39
|
+
async pathsOf(target) {
|
|
40
|
+
const res = await this.db
|
|
41
|
+
.from("paths")
|
|
42
|
+
.select("locale, path")
|
|
43
|
+
.eq("kind", target.kind)
|
|
44
|
+
.eq("target_id", target.id);
|
|
45
|
+
if (res.error)
|
|
46
|
+
this.fail(res.error.message);
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const r of res.data)
|
|
49
|
+
out[r.locale] = r.path;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { type PageRecord, type PageStore, type PageStoreOptions, type PageVersion } from "./store.ts";
|
|
2
|
+
import { type CreatePageInput, type PageRecord, type PageStore, type PageStoreOptions, type PageVersion } from "./store.ts";
|
|
3
3
|
import type { PageTree } from "./render.tsx";
|
|
4
|
+
import { type PathTarget } from "./paths.ts";
|
|
4
5
|
export declare class SupabasePageStore implements PageStore {
|
|
5
6
|
private db;
|
|
6
7
|
private options;
|
|
8
|
+
private paths;
|
|
9
|
+
private opts;
|
|
7
10
|
constructor(db: SupabaseClient, options?: PageStoreOptions);
|
|
8
|
-
private
|
|
9
|
-
private
|
|
11
|
+
private fail;
|
|
12
|
+
private isHome;
|
|
13
|
+
private row;
|
|
14
|
+
private must;
|
|
15
|
+
/** A one-row existence check — the shape `deletePage` needs too. */
|
|
16
|
+
private hasChildren;
|
|
17
|
+
private siblings;
|
|
18
|
+
/** The node's ancestors (root first, node last), by walking parent_id. */
|
|
19
|
+
private chain;
|
|
20
|
+
/** The node and every descendant, breadth-first. */
|
|
21
|
+
private subtree;
|
|
22
|
+
/** Rewrite path rows for a subtree: ancestors are loaded once so the
|
|
23
|
+
* chain lookup is a map, then ONE RPC replaces every target's rows. */
|
|
24
|
+
private writePaths;
|
|
25
|
+
/** `touch` defaults to true (the normal edit path bumps updated_at).
|
|
26
|
+
* A rollback restoring prior column values passes `{ touch: false }`,
|
|
27
|
+
* which omits updated_at entirely — undoing a write is not an edit
|
|
28
|
+
* (same pattern as SupabaseEntryStore's `patch`). */
|
|
29
|
+
private update;
|
|
10
30
|
private insertVersion;
|
|
11
|
-
private setPointers;
|
|
12
|
-
createPage({ slug, template }: {
|
|
13
|
-
slug: string;
|
|
14
|
-
template: string;
|
|
15
|
-
}): Promise<PageRecord>;
|
|
16
|
-
getPage(slug: string): Promise<PageRecord | null>;
|
|
17
|
-
getPageById(id: string): Promise<PageRecord | null>;
|
|
18
|
-
saveDraft(slug: string, tree: PageTree): Promise<PageVersion>;
|
|
19
|
-
publish(slug: string): Promise<PageRecord>;
|
|
20
31
|
private versionTree;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
private writeRefs;
|
|
33
|
+
private reorder;
|
|
34
|
+
private hardDelete;
|
|
35
|
+
createPage(input: CreatePageInput): Promise<PageRecord>;
|
|
36
|
+
getPage(id: string): Promise<PageRecord | null>;
|
|
24
37
|
listPages(): Promise<PageRecord[]>;
|
|
25
|
-
|
|
38
|
+
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
39
|
+
pathsOf(id: string): Promise<{}>;
|
|
40
|
+
rename(id: string, patch: {
|
|
41
|
+
locale: string;
|
|
42
|
+
slug?: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
}): Promise<PageRecord>;
|
|
45
|
+
move(id: string, to: {
|
|
46
|
+
parentId: string | null;
|
|
47
|
+
index?: number;
|
|
48
|
+
}): Promise<PageRecord>;
|
|
49
|
+
setTemplate(id: string, template: string): Promise<PageRecord>;
|
|
50
|
+
saveDraft(id: string, tree: PageTree): Promise<PageVersion>;
|
|
51
|
+
publish(id: string): Promise<PageRecord>;
|
|
52
|
+
getDraftTree(id: string): Promise<PageTree | null>;
|
|
53
|
+
getPublishedTree(id: string): Promise<PageTree | null>;
|
|
54
|
+
listVersions(id: string): Promise<PageVersion[]>;
|
|
55
|
+
deletePage(id: string): Promise<void>;
|
|
26
56
|
}
|