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.
Files changed (89) hide show
  1. package/README.md +408 -34
  2. package/dist/admin/client-ops.js +2 -2
  3. package/dist/admin/editor/EditorView.d.ts +3 -2
  4. package/dist/admin/editor/EditorView.js +133 -80
  5. package/dist/admin/editor/PageSettings.d.ts +13 -0
  6. package/dist/admin/editor/PageSettings.js +40 -0
  7. package/dist/admin/fixed-nodes.d.ts +23 -0
  8. package/dist/admin/fixed-nodes.js +83 -0
  9. package/dist/admin/forms/FieldWidget.d.ts +2 -1
  10. package/dist/admin/forms/FieldWidget.js +16 -16
  11. package/dist/admin/index.d.ts +2 -1
  12. package/dist/admin/index.js +1 -0
  13. package/dist/admin/next/create-admin.js +7 -2
  14. package/dist/admin/next/op-handler.d.ts +1 -0
  15. package/dist/admin/next/op-handler.js +7 -2
  16. package/dist/admin/ops-impl.d.ts +4 -1
  17. package/dist/admin/ops-impl.js +190 -48
  18. package/dist/admin/ops.d.ts +39 -11
  19. package/dist/admin/serialize.d.ts +10 -1
  20. package/dist/admin/serialize.js +13 -2
  21. package/dist/admin/shell/AdminApp.js +36 -74
  22. package/dist/admin/shell/EntryForm.js +34 -10
  23. package/dist/admin/shell/ErrorPane.d.ts +4 -0
  24. package/dist/admin/shell/ErrorPane.js +9 -0
  25. package/dist/admin/shell/ListView.d.ts +8 -5
  26. package/dist/admin/shell/ListView.js +12 -11
  27. package/dist/admin/shell/LoginView.js +6 -2
  28. package/dist/admin/shell/PagesList.d.ts +6 -0
  29. package/dist/admin/shell/PagesList.js +148 -0
  30. package/dist/admin/shell/base.d.ts +1 -0
  31. package/dist/admin/shell/base.js +4 -0
  32. package/dist/admin/shell/pages-tree.d.ts +39 -0
  33. package/dist/admin/shell/pages-tree.js +65 -0
  34. package/dist/admin/ui/format.d.ts +1 -0
  35. package/dist/admin/ui/format.js +25 -0
  36. package/dist/admin/ui/primitives.d.ts +89 -0
  37. package/dist/admin/ui/primitives.js +115 -0
  38. package/dist/admin/ui/theme.d.ts +5 -0
  39. package/dist/admin/ui/theme.js +396 -0
  40. package/dist/collections.d.ts +23 -2
  41. package/dist/collections.js +19 -0
  42. package/dist/config.d.ts +11 -1
  43. package/dist/config.js +71 -13
  44. package/dist/entry-page.d.ts +16 -0
  45. package/dist/entry-page.js +6 -0
  46. package/dist/entry-store.d.ts +94 -7
  47. package/dist/entry-store.js +175 -26
  48. package/dist/index.d.ts +24 -8
  49. package/dist/index.js +14 -3
  50. package/dist/localize.d.ts +19 -0
  51. package/dist/localize.js +42 -0
  52. package/dist/next/index.d.ts +2 -2
  53. package/dist/next/index.js +1 -1
  54. package/dist/next/meta.d.ts +0 -6
  55. package/dist/next/meta.js +0 -11
  56. package/dist/next/page-renderer.d.ts +26 -18
  57. package/dist/next/page-renderer.js +79 -56
  58. package/dist/page.d.ts +10 -3
  59. package/dist/page.js +12 -2
  60. package/dist/pairing.d.ts +7 -6
  61. package/dist/path-index.d.ts +16 -0
  62. package/dist/path-index.js +33 -0
  63. package/dist/paths.d.ts +80 -0
  64. package/dist/paths.js +110 -0
  65. package/dist/render.d.ts +34 -5
  66. package/dist/render.js +9 -2
  67. package/dist/resolve.d.ts +8 -0
  68. package/dist/resolve.js +19 -7
  69. package/dist/revalidate.d.ts +10 -2
  70. package/dist/revalidate.js +13 -8
  71. package/dist/site.d.ts +85 -0
  72. package/dist/site.js +164 -0
  73. package/dist/sql-space.d.ts +1 -1
  74. package/dist/sql-space.js +2 -0
  75. package/dist/sql.d.ts +1 -1
  76. package/dist/sql.js +92 -15
  77. package/dist/store.d.ts +106 -30
  78. package/dist/store.js +242 -66
  79. package/dist/supabase-entry-store.d.ts +17 -3
  80. package/dist/supabase-entry-store.js +198 -22
  81. package/dist/supabase-path-index.d.ts +11 -0
  82. package/dist/supabase-path-index.js +52 -0
  83. package/dist/supabase-store.d.ts +46 -16
  84. package/dist/supabase-store.js +322 -110
  85. package/package.json +5 -4
  86. package/dist/admin/page-path.d.ts +0 -1
  87. package/dist/admin/page-path.js +0 -6
  88. package/dist/admin/shell/pages-list.d.ts +0 -22
  89. 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 { schemaOf } from "./entry-store.js";
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
- async create(collection, fields) {
59
- schemaOf(this.collections, collection);
60
- const res = await this.db.from("entries").insert({ collection, fields }).select("*").single();
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
- const row = res.data;
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 toRecord(row);
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
- const order = options?.order ?? "created";
84
- if (order === "updated")
85
- query = query.order("updated_at", { ascending: false });
86
- else if (order === "manual")
87
- query = query
88
- .order("sort", { ascending: true, nullsFirst: false })
89
- .order("created_at", { ascending: true });
90
- else
91
- query = query.order("created_at", { ascending: true }).order("id", { ascending: true });
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
- async patch(collection, id, patch) {
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
- this.fail(`no entry "${id}" in "${collection}".`);
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({ ...patch, updated_at: new Date().toISOString() })
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.fail(res.error.message);
231
+ this.failWriteError(res.error, collection);
130
232
  if (!res.data)
131
- this.fail(`no entry "${id}" in "${collection}".`);
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
+ }
@@ -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 writeRefs;
9
- private pageRow;
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
- getDraftTree(slug: string): Promise<PageTree | null>;
22
- getPublishedTree(slug: string): Promise<PageTree | null>;
23
- listVersions(slug: string): Promise<PageVersion[]>;
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
- deletePage(slug: string): Promise<void>;
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
  }