smoodly 0.0.13 → 0.0.14
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 -15
- package/dist/admin/editor/EditorView.js +12 -10
- package/dist/admin/editor/PageInspector.d.ts +21 -0
- package/dist/admin/editor/PageInspector.js +24 -0
- package/dist/admin/editor/PageSettings.d.ts +1 -3
- package/dist/admin/editor/PageSettings.js +7 -6
- package/dist/admin/editor/SeoSettings.d.ts +7 -0
- package/dist/admin/editor/SeoSettings.js +15 -0
- package/dist/admin/forms/FieldWidget.d.ts +1 -1
- package/dist/admin/forms/FieldWidget.js +24 -4
- package/dist/admin/ops-impl.js +72 -11
- package/dist/admin/serialize.d.ts +4 -0
- package/dist/admin/serialize.js +2 -0
- package/dist/admin/tree-ops.d.ts +2 -0
- package/dist/admin/tree-ops.js +5 -1
- package/dist/admin/ui/primitives.d.ts +9 -0
- package/dist/admin/ui/primitives.js +6 -0
- package/dist/admin/ui/resize.d.ts +1 -1
- package/dist/admin/ui/resize.js +1 -1
- package/dist/admin/ui/theme.js +16 -1
- package/dist/admin/validate.js +24 -3
- package/dist/collections.d.ts +6 -5
- package/dist/entry-store.d.ts +18 -1
- package/dist/entry-store.js +27 -2
- package/dist/field-defaults.d.ts +8 -0
- package/dist/field-defaults.js +18 -0
- package/dist/fields.d.ts +26 -1
- package/dist/fields.js +31 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -3
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/next/meta.d.ts +7 -3
- package/dist/next/meta.js +18 -9
- package/dist/next/page-renderer.js +1 -1
- package/dist/next/queries.d.ts +36 -0
- package/dist/next/queries.js +87 -0
- package/dist/page-tree.d.ts +3 -0
- package/dist/page-tree.js +6 -0
- package/dist/page.d.ts +11 -3
- package/dist/page.js +26 -0
- package/dist/pairing.d.ts +2 -1
- package/dist/pairing.js +2 -9
- package/dist/render.d.ts +14 -7
- package/dist/render.js +2 -2
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +8 -0
- package/dist/revalidate.d.ts +6 -0
- package/dist/revalidate.js +6 -0
- package/dist/shared.d.ts +7 -7
- package/dist/site.d.ts +23 -1
- package/dist/site.js +83 -19
- package/dist/store.d.ts +4 -0
- package/dist/store.js +9 -0
- package/dist/supabase-entry-store.js +32 -10
- package/dist/supabase-store.d.ts +1 -0
- package/dist/supabase-store.js +13 -0
- package/package.json +1 -1
package/dist/site.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { entryFieldsIn, schemaOf } from "./entry-store.js";
|
|
1
|
+
import { ARRAY_FIELD_TYPES, entryFieldsIn, localizedKeys, schemaOf } from "./entry-store.js";
|
|
2
2
|
import { buildPageTree, chainOf, entryPathRows, entrySegment, pagePath, } from "./paths.js";
|
|
3
3
|
import { resolveFields, resolveMedia, resolveMediaSets, resolveTree } from "./resolve.js";
|
|
4
4
|
import { elementSchemas } from "./richtext-walk.js";
|
|
5
5
|
import { STYLES_KEY } from "./shared.js";
|
|
6
|
+
import { withDefaults } from "./field-defaults.js";
|
|
6
7
|
/** The `page` prop for a loaded page. `toUrl` is the app's `href` seam
|
|
7
8
|
* bound to the render locale; the default keeps CMS paths as URLs. */
|
|
8
9
|
export function pageContextOf(hit, toUrl = (p) => p) {
|
|
@@ -12,6 +13,7 @@ export function pageContextOf(hit, toUrl = (p) => p) {
|
|
|
12
13
|
title: hit.title,
|
|
13
14
|
path: hit.path,
|
|
14
15
|
url: toUrl(hit.path),
|
|
16
|
+
fields: hit.fields,
|
|
15
17
|
ancestors: hit.ancestors.map(withUrl),
|
|
16
18
|
children: hit.children.map(withUrl),
|
|
17
19
|
};
|
|
@@ -46,11 +48,43 @@ export function createSmoodlySite(options) {
|
|
|
46
48
|
const row = node.locales[locale];
|
|
47
49
|
return node.template !== null && row !== undefined && (draft || row.status === "published");
|
|
48
50
|
};
|
|
51
|
+
const pageFields = (template) => {
|
|
52
|
+
if (template === null)
|
|
53
|
+
return {};
|
|
54
|
+
const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
|
|
55
|
+
return p?.schema.fields ?? {};
|
|
56
|
+
};
|
|
57
|
+
/** The version whose fields a link shows: the draft's in draft mode, else the published one. */
|
|
58
|
+
const versionFor = (node, locale, draft) => {
|
|
59
|
+
const row = node.locales[locale];
|
|
60
|
+
if (!row || node.template === null)
|
|
61
|
+
return null;
|
|
62
|
+
return draft ? row.draftVersionId : row.publishedVersionId;
|
|
63
|
+
};
|
|
64
|
+
/** Raw page fields (no asset resolution) of many nodes in ONE store read
|
|
65
|
+
* (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
|
|
66
|
+
async function linkFields(nodes, locale, draft) {
|
|
67
|
+
const { pages } = options.stores();
|
|
68
|
+
const wanted = new Map(); // node id → version id
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
const v = versionFor(n, locale, draft);
|
|
71
|
+
if (v)
|
|
72
|
+
wanted.set(n.id, v);
|
|
73
|
+
}
|
|
74
|
+
const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
|
|
75
|
+
const out = new Map();
|
|
76
|
+
for (const n of nodes) {
|
|
77
|
+
const v = wanted.get(n.id);
|
|
78
|
+
out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
49
82
|
/** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
|
|
50
|
-
const link = (node, byId, locale, draft) => ({
|
|
83
|
+
const link = (node, byId, locale, draft, fields) => ({
|
|
51
84
|
title: node.locales[locale].title,
|
|
52
85
|
path: pagePath(node, byId, locale, pathOpts),
|
|
53
86
|
hasPage: hasPage(node, locale, draft),
|
|
87
|
+
fields: fields.get(node.id) ?? {},
|
|
54
88
|
});
|
|
55
89
|
async function loadPage(id, locale, path, draft) {
|
|
56
90
|
const { pages } = options.stores();
|
|
@@ -70,15 +104,16 @@ export function createSmoodlySite(options) {
|
|
|
70
104
|
elements: options.elements,
|
|
71
105
|
fetch: fetcher(draft, locale),
|
|
72
106
|
assets: assetFetcher(),
|
|
107
|
+
page: { fields: pageFields(record.template) },
|
|
73
108
|
});
|
|
74
109
|
const all = await pages.listPages();
|
|
75
110
|
const index = new Map(all.map((n) => [n.id, n]));
|
|
76
111
|
const byId = (x) => index.get(x);
|
|
77
112
|
const chain = chainOf(record, byId);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
|
|
114
|
+
const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
|
|
115
|
+
const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
|
|
116
|
+
const children = kids.map((n) => link(n, byId, locale, draft, fields));
|
|
82
117
|
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
83
118
|
const paths = Object.fromEntries(Object.entries(await pages.pathsOf(id)).filter(([l]) => draft || record.locales[l]?.status === "published"));
|
|
84
119
|
return {
|
|
@@ -88,6 +123,7 @@ export function createSmoodlySite(options) {
|
|
|
88
123
|
tree,
|
|
89
124
|
title: row.title,
|
|
90
125
|
path,
|
|
126
|
+
fields: withDefaults(tree.fields, pageFields(record.template)),
|
|
91
127
|
ancestors,
|
|
92
128
|
children,
|
|
93
129
|
paths,
|
|
@@ -131,28 +167,55 @@ export function createSmoodlySite(options) {
|
|
|
131
167
|
path: entryPathRows([record], byId, schema, locales).find((r) => r.locale === locale)?.path ?? null,
|
|
132
168
|
}));
|
|
133
169
|
}
|
|
134
|
-
/** Public in a locale: its row exists there and is published. */
|
|
135
|
-
const publicEntry = (e, locale) => e.locales[locale]?.status === "published";
|
|
136
170
|
async function getEntries(collection, o = {}) {
|
|
137
171
|
const schema = schemaOf(options.collections, collection);
|
|
138
172
|
const locale = localeOf(o);
|
|
173
|
+
const draft = o.draft ?? false;
|
|
139
174
|
const { entries } = options.stores();
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
175
|
+
// A filter that silently matched nothing would be worse than an error (spec 2026-09-11 §3).
|
|
176
|
+
for (const key of Object.keys(o.where ?? {})) {
|
|
177
|
+
if (key === "slug" || localizedKeys(schema).includes(key)) {
|
|
178
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is localized — where filters shared fields only (spec 2026-09-11 §3).`);
|
|
179
|
+
}
|
|
180
|
+
if (!(key in schema.fields))
|
|
181
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is not a field of the collection.`);
|
|
182
|
+
// Any-of reads the text form of the whole field (PostgREST's `->>`), which for
|
|
183
|
+
// an array is its JSON — `["a"]` never matches "a" — so refuse rather than match nothing.
|
|
184
|
+
if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
185
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (o.limit !== undefined && (!Number.isInteger(o.limit) || o.limit < 0)) {
|
|
189
|
+
throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
|
|
190
|
+
}
|
|
191
|
+
// The store filters, orders and limits (spec 2026-09-11 §3): the locale
|
|
192
|
+
// gate — presence in draft mode, published otherwise — is part of the
|
|
193
|
+
// query, so `limit` counts rows that are really there.
|
|
194
|
+
const inLocale = await entries.list(collection, {
|
|
195
|
+
order: o.order, where: o.where, limit: o.limit,
|
|
196
|
+
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
197
|
+
...(draft ? { locale } : { status: "published", locale }),
|
|
198
|
+
});
|
|
199
|
+
// Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
|
|
200
|
+
const all = schema.tree ? await entries.list(collection) : inLocale;
|
|
201
|
+
const fieldsById = draft
|
|
202
|
+
? Object.fromEntries(inLocale.map((e) => [e.id, entryFieldsIn(e, locale)]))
|
|
203
|
+
: await entries.getMany(collection, inLocale.map((e) => e.id), locale, { status: "published" });
|
|
143
204
|
// Refs stay ids on the content API; media resolves (one asset round for
|
|
144
205
|
// the whole list), because a list page's cards need their image URLs.
|
|
145
206
|
const fetchAssets = assetFetcher();
|
|
146
207
|
if (fetchAssets)
|
|
147
|
-
await resolveMediaSets(Object.values(
|
|
148
|
-
return entryItems(schema,
|
|
208
|
+
await resolveMediaSets(Object.values(fieldsById).map((values) => ({ values, descriptors: schema.fields })), fetchAssets, elements);
|
|
209
|
+
return entryItems(schema, inLocale, all, locale, fieldsById);
|
|
149
210
|
}
|
|
150
211
|
const api = {
|
|
151
212
|
async getPagesTree(o) {
|
|
152
213
|
const locale = localeOf(o);
|
|
214
|
+
const draft = o?.draft ?? false;
|
|
153
215
|
const all = await options.stores().pages.listPages();
|
|
154
|
-
const shown = all.filter((n) => visible(n, locale,
|
|
216
|
+
const shown = all.filter((n) => visible(n, locale, draft));
|
|
155
217
|
const index = new Map(shown.map((n) => [n.id, n]));
|
|
218
|
+
const fields = await linkFields(shown, locale, draft);
|
|
156
219
|
const toItem = (node) => ({
|
|
157
220
|
id: node.record.id,
|
|
158
221
|
title: node.record.locales[locale].title,
|
|
@@ -160,7 +223,8 @@ export function createSmoodlySite(options) {
|
|
|
160
223
|
// buildPageTree's nullable path is never null here; pagePath is
|
|
161
224
|
// the same derivation and keeps the type honest.
|
|
162
225
|
path: node.path ?? pagePath(node.record, (x) => index.get(x), locale, pathOpts),
|
|
163
|
-
hasPage: hasPage(node.record, locale,
|
|
226
|
+
hasPage: hasPage(node.record, locale, draft),
|
|
227
|
+
fields: fields.get(node.record.id) ?? {},
|
|
164
228
|
children: node.children.map(toItem),
|
|
165
229
|
});
|
|
166
230
|
return buildPageTree(shown, locale, pathOpts).map(toItem);
|
|
@@ -170,7 +234,7 @@ export function createSmoodlySite(options) {
|
|
|
170
234
|
const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
|
|
171
235
|
if (!path)
|
|
172
236
|
return null;
|
|
173
|
-
const hit = await site.resolve(locale, path);
|
|
237
|
+
const hit = await site.resolve(locale, path, { draft: o?.draft ?? false });
|
|
174
238
|
return hit?.kind === "page" ? hit : null;
|
|
175
239
|
},
|
|
176
240
|
getEntries,
|
|
@@ -178,10 +242,10 @@ export function createSmoodlySite(options) {
|
|
|
178
242
|
schemaOf(options.collections, collection); // an unknown collection is a wiring error on every branch
|
|
179
243
|
const locale = localeOf(o);
|
|
180
244
|
if (idOrPath.startsWith("/")) {
|
|
181
|
-
const hit = await site.resolve(locale, idOrPath);
|
|
245
|
+
const hit = await site.resolve(locale, idOrPath, { draft: o?.draft ?? false });
|
|
182
246
|
return hit?.kind === "entry" && hit.collection === collection ? { entry: hit.entry, path: hit.path } : null;
|
|
183
247
|
}
|
|
184
|
-
const items = await getEntries(collection, { locale });
|
|
248
|
+
const items = await getEntries(collection, { locale, draft: o?.draft ?? false });
|
|
185
249
|
const byId = items.find((i) => i.entry.id === idOrPath);
|
|
186
250
|
if (byId)
|
|
187
251
|
return byId;
|
|
@@ -191,7 +255,7 @@ export function createSmoodlySite(options) {
|
|
|
191
255
|
return items.find((i) => i.entry.parentId === null && entrySegment(i.entry, locale) === idOrPath) ?? null;
|
|
192
256
|
},
|
|
193
257
|
async getEntriesTree(collection, o) {
|
|
194
|
-
const items = await getEntries(collection, { locale: localeOf(o) });
|
|
258
|
+
const items = await getEntries(collection, { locale: localeOf(o), draft: o?.draft ?? false });
|
|
195
259
|
const build = (parentId) => items.filter((i) => i.entry.parentId === parentId).map((i) => ({ ...i, children: build(i.entry.id) }));
|
|
196
260
|
return build(null);
|
|
197
261
|
},
|
package/dist/store.d.ts
CHANGED
|
@@ -78,6 +78,9 @@ export interface PageStore {
|
|
|
78
78
|
publish(id: string, locale: string): Promise<PageRecord>;
|
|
79
79
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
80
80
|
getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
|
|
81
|
+
/** The page fields of many versions in ONE read (spec 2026-09-10 page fields §3):
|
|
82
|
+
* versionId → `tree.fields ?? {}`; ids that do not exist are absent. */
|
|
83
|
+
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
|
81
84
|
listVersions(id: string, locale: string): Promise<PageVersion[]>;
|
|
82
85
|
deletePage(id: string): Promise<void>;
|
|
83
86
|
}
|
|
@@ -179,6 +182,7 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
179
182
|
saveDraft(id: string, locale: string, tree: PageTree): Promise<PageVersion>;
|
|
180
183
|
publish(id: string, locale: string): Promise<PageRecord>;
|
|
181
184
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
185
|
+
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
|
182
186
|
getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
|
|
183
187
|
listVersions(id: string, locale: string): Promise<PageVersion[]>;
|
|
184
188
|
deletePage(id: string): Promise<void>;
|
package/dist/store.js
CHANGED
|
@@ -410,6 +410,15 @@ export class MemoryPageStore {
|
|
|
410
410
|
return null;
|
|
411
411
|
return structuredClone(this.versions.get(row.draftVersionId)?.tree ?? null);
|
|
412
412
|
}
|
|
413
|
+
async fieldsOf(versionIds) {
|
|
414
|
+
const out = {};
|
|
415
|
+
for (const id of versionIds) {
|
|
416
|
+
const v = this.versions.get(id);
|
|
417
|
+
if (v)
|
|
418
|
+
out[id] = structuredClone(v.tree.fields ?? {});
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
413
422
|
async getPublishedTree(id, locale) {
|
|
414
423
|
const row = this.pages.get(id)?.locales[locale];
|
|
415
424
|
if (!row?.publishedVersionId)
|
|
@@ -7,7 +7,7 @@
|
|
|
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 { DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isSharedSchema, schemaOf, splitFields, } from "./entry-store.js";
|
|
10
|
+
import { ARRAY_FIELD_TYPES, DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isSharedSchema, schemaOf, splitFields, } from "./entry-store.js";
|
|
11
11
|
import { sharedEntryId } from "./shared-id.js";
|
|
12
12
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
13
13
|
import { assertDepth, assertSupportedLocale, entryPathRows, slugOf } from "./paths.js";
|
|
@@ -316,10 +316,37 @@ export class SupabaseEntryStore {
|
|
|
316
316
|
}
|
|
317
317
|
async list(collection, options) {
|
|
318
318
|
const schema = this.schema(collection);
|
|
319
|
-
|
|
319
|
+
// A second, aliased embed of entry_locales carries the locale gate as an
|
|
320
|
+
// inner join — presence in the locale, and its status when asked — so
|
|
321
|
+
// `limit` counts rows that are really there; the first embed still returns
|
|
322
|
+
// EVERY locale row of each record (spec 2026-09-11 §3), ordered by locale
|
|
323
|
+
// so a record's key order never depends on physical row order.
|
|
324
|
+
const gate = options?.status !== undefined || options?.locale !== undefined;
|
|
325
|
+
let query = this.db
|
|
326
|
+
.from("entries")
|
|
327
|
+
.select(gate ? `${SELECT}, gate:entry_locales!inner(locale)` : SELECT)
|
|
328
|
+
.eq("collection", collection)
|
|
329
|
+
.order("locale", { referencedTable: "entry_locales", ascending: true });
|
|
330
|
+
if (gate) {
|
|
331
|
+
query = query.eq("gate.locale", options.locale ?? this.locales.default);
|
|
332
|
+
if (options.status !== undefined)
|
|
333
|
+
query = query.eq("gate.status", options.status);
|
|
334
|
+
}
|
|
320
335
|
if (options?.parent !== undefined) {
|
|
321
336
|
query = options.parent === null ? query.is("parent_id", null) : query.eq("parent_id", options.parent);
|
|
322
337
|
}
|
|
338
|
+
for (const [key, want] of Object.entries(options?.where ?? {})) {
|
|
339
|
+
// Any-of compares the text form (`->>`). A scalar is JSONB containment;
|
|
340
|
+
// an array-valued field (checkboxes, refList, list) contains `[value]`,
|
|
341
|
+
// a scalar field `value` — the memory adapter's matchesWhere agrees.
|
|
342
|
+
if (Array.isArray(want)) {
|
|
343
|
+
query = query.in(`fields->>${key}`, want.map(String));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const inArray = ARRAY_FIELD_TYPES.has(schema.fields[key]?.type ?? "");
|
|
347
|
+
query = query.contains("fields", { [key]: inArray ? [want] : want });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
323
350
|
const order = options?.order ?? collectionOrder(schema);
|
|
324
351
|
if (order === "manual") {
|
|
325
352
|
query = query.order("sort", { ascending: true, nullsFirst: false }).order("created_at", { ascending: true });
|
|
@@ -330,17 +357,12 @@ export class SupabaseEntryStore {
|
|
|
330
357
|
const column = order.by === "createdAt" ? "created_at" : order.by === "updatedAt" ? "updated_at" : `fields->${order.by}`;
|
|
331
358
|
query = query.order(column, { ascending: order.direction === "asc", nullsFirst: false }).order("id", { ascending: true });
|
|
332
359
|
}
|
|
360
|
+
if (typeof options?.limit === "number")
|
|
361
|
+
query = query.limit(options.limit);
|
|
333
362
|
const res = await query;
|
|
334
363
|
if (res.error)
|
|
335
364
|
this.fail(res.error.message);
|
|
336
|
-
|
|
337
|
-
// The status filter reads the locale row; collections are small enough
|
|
338
|
-
// to filter here rather than lose the other rows to an !inner embed.
|
|
339
|
-
if (options?.status) {
|
|
340
|
-
const locale = options.locale ?? this.locales.default;
|
|
341
|
-
rows = rows.filter((e) => e.locales[locale]?.status === options.status);
|
|
342
|
-
}
|
|
343
|
-
return rows;
|
|
365
|
+
return res.data.map(({ gate: _gate, ...row }) => toRecord(row));
|
|
344
366
|
}
|
|
345
367
|
async getMany(collection, ids, locale, options) {
|
|
346
368
|
this.schema(collection);
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare class SupabasePageStore implements PageStore {
|
|
|
62
62
|
saveDraft(id: string, locale: string, tree: PageTree): Promise<PageVersion>;
|
|
63
63
|
publish(id: string, locale: string): Promise<PageRecord>;
|
|
64
64
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
65
|
+
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
|
65
66
|
getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
|
|
66
67
|
listVersions(id: string, locale: string): Promise<PageVersion[]>;
|
|
67
68
|
deletePage(id: string): Promise<void>;
|
package/dist/supabase-store.js
CHANGED
|
@@ -453,6 +453,19 @@ export class SupabasePageStore {
|
|
|
453
453
|
const page = await this.row(id);
|
|
454
454
|
return page ? this.versionTree(page.locales[locale]?.draftVersionId ?? null) : null;
|
|
455
455
|
}
|
|
456
|
+
async fieldsOf(versionIds) {
|
|
457
|
+
const ids = versionIds.filter((id) => UUID_RE.test(id));
|
|
458
|
+
if (ids.length === 0)
|
|
459
|
+
return {};
|
|
460
|
+
// PostgREST's JSON arrow selects the `fields` key alone — the rest of the tree never leaves the database.
|
|
461
|
+
const res = await this.db.from("page_versions").select("id, fields:tree->fields").in("id", ids);
|
|
462
|
+
if (res.error)
|
|
463
|
+
this.fail(res.error.message);
|
|
464
|
+
const out = {};
|
|
465
|
+
for (const row of res.data)
|
|
466
|
+
out[row.id] = row.fields ?? {};
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
456
469
|
async getPublishedTree(id, locale) {
|
|
457
470
|
const page = await this.row(id);
|
|
458
471
|
return page ? this.versionTree(page.locales[locale]?.publishedVersionId ?? null) : null;
|
package/package.json
CHANGED