smoodly 0.0.45 → 0.0.46
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 +3 -3
- package/dist/addresses.d.ts +6 -7
- package/dist/addresses.js +6 -4
- package/dist/admin/PublicSans-LICENSE.txt +93 -0
- package/dist/admin/auth-config.d.ts +6 -0
- package/dist/admin/auth-config.js +1 -0
- package/dist/admin/browser/fetch-op.d.ts +10 -0
- package/dist/admin/browser/fetch-op.js +44 -0
- package/dist/admin/browser/main.d.ts +1 -0
- package/dist/admin/browser/main.js +21 -0
- package/dist/admin/custom-section.d.ts +1 -1
- package/dist/admin/drift.d.ts +1 -1
- package/dist/admin/editor/EditorView.js +14 -14
- package/dist/admin/editor/autosave.js +1 -1
- package/dist/admin/editor/canvas-address.d.ts +2 -0
- package/dist/admin/editor/canvas-address.js +8 -0
- package/dist/admin/editor/overlay-geometry.d.ts +1 -1
- package/dist/admin/editor/protocol.d.ts +18 -4
- package/dist/admin/editor/protocol.js +15 -1
- package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
- package/dist/admin/editor/useCanvasBridge.js +5 -6
- package/dist/admin/editor/useInlineSession.js +4 -2
- package/dist/admin/files.d.ts +11 -0
- package/dist/admin/files.generated.js +2 -0
- package/dist/admin/files.js +8 -0
- package/dist/admin/forms/useRefOptions.d.ts +2 -2
- package/dist/admin/forms/useRefOptions.js +2 -2
- package/dist/admin/forms/useRichtextEditor.js +4 -4
- package/dist/admin/handler.d.ts +40 -0
- package/dist/admin/handler.js +151 -0
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/ops-impl.d.ts +7 -1
- package/dist/admin/ops-impl.js +110 -33
- package/dist/admin/ops.d.ts +3 -2
- package/dist/admin/page-data.d.ts +21 -0
- package/dist/admin/page-data.js +4 -0
- package/dist/admin/page.d.ts +6 -0
- package/dist/admin/page.js +35 -0
- package/dist/admin/serialize.js +7 -7
- package/dist/admin/shell/AdminApp.js +2 -2
- package/dist/admin/shell/EntryForm.js +1 -1
- package/dist/admin/shell/router.d.ts +3 -16
- package/dist/admin/shell/router.js +3 -39
- package/dist/admin/shell/session.d.ts +2 -5
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +1 -1
- package/dist/admin/ui/fonts.d.ts +12 -0
- package/dist/admin/ui/fonts.js +22 -0
- package/dist/admin/ui/theme.d.ts +13 -4
- package/dist/admin/ui/theme.js +28 -157
- package/dist/assemble.d.ts +60 -0
- package/dist/assemble.js +359 -0
- package/dist/astro/cache.d.ts +34 -0
- package/dist/astro/cache.js +69 -0
- package/dist/astro/components/Custom.astro +21 -0
- package/dist/astro/components/Guarded.astro +24 -0
- package/dist/astro/components/Head.astro +26 -0
- package/dist/astro/components/Image.astro +20 -0
- package/dist/astro/components/RichText.astro +23 -0
- package/dist/astro/components/Section.astro +20 -0
- package/dist/astro/components/Video.astro +5 -0
- package/dist/astro/components/Zone.astro +19 -0
- package/dist/astro/components.d.ts +6 -0
- package/dist/astro/components.js +7 -0
- package/dist/astro/content-loaders.d.ts +40 -0
- package/dist/astro/content-loaders.js +100 -0
- package/dist/astro/content.d.ts +9 -0
- package/dist/astro/content.js +17 -0
- package/dist/astro/custom.d.ts +1 -0
- package/dist/astro/custom.js +3 -0
- package/dist/astro/defaults.d.ts +20 -0
- package/dist/astro/defaults.js +21 -0
- package/dist/astro/env-files.d.ts +1 -0
- package/dist/astro/env-files.js +21 -0
- package/dist/astro/head.d.ts +13 -0
- package/dist/astro/head.js +19 -0
- package/dist/astro/image.d.ts +27 -0
- package/dist/astro/image.js +29 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +111 -0
- package/dist/astro/locale-paths.d.ts +30 -0
- package/dist/astro/locale-paths.js +38 -0
- package/dist/astro/middleware.d.ts +1 -0
- package/dist/astro/middleware.js +83 -0
- package/dist/astro/preview-document.d.ts +1 -0
- package/dist/astro/preview-document.js +18 -0
- package/dist/astro/preview.d.ts +10 -0
- package/dist/astro/preview.js +72 -0
- package/dist/astro/richtext.d.ts +15 -0
- package/dist/astro/richtext.js +78 -0
- package/dist/astro/routes/admin.d.ts +3 -0
- package/dist/astro/routes/admin.js +41 -0
- package/dist/astro/routes/page.astro +17 -0
- package/dist/astro/routes/preview.astro +11 -0
- package/dist/astro/runtime.d.ts +32 -0
- package/dist/astro/runtime.js +46 -0
- package/dist/astro/scope.d.ts +45 -0
- package/dist/astro/scope.js +103 -0
- package/dist/astro/server.d.ts +14 -0
- package/dist/astro/server.js +45 -0
- package/dist/astro/shell.d.ts +67 -0
- package/dist/astro/shell.js +63 -0
- package/dist/astro/style.d.ts +1 -0
- package/dist/astro/style.js +11 -0
- package/dist/astro/views-check.d.ts +3 -0
- package/dist/astro/views-check.js +19 -0
- package/dist/astro/zone.d.ts +39 -0
- package/dist/astro/zone.js +33 -0
- package/dist/bind.d.ts +19 -0
- package/dist/bind.js +34 -0
- package/dist/bridge/bridge.d.ts +9 -0
- package/dist/bridge/bridge.js +107 -0
- package/dist/bridge/entry.d.ts +1 -0
- package/dist/bridge/entry.js +5 -0
- package/dist/bridge/preview-pill.d.ts +18 -0
- package/dist/bridge/preview-pill.js +115 -0
- package/dist/cli/import.d.ts +3 -0
- package/dist/cli/import.js +10 -1
- package/dist/cli/load-collections.js +4 -3
- package/dist/cli/load-config.d.ts +4 -0
- package/dist/cli/load-config.js +26 -0
- package/dist/cli/main.d.ts +5 -1
- package/dist/cli/main.js +24 -6
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +8 -1
- package/dist/cli/rebuild.d.ts +23 -0
- package/dist/cli/rebuild.js +22 -0
- package/dist/cli/space-io.d.ts +15 -1
- package/dist/cli/space-io.js +14 -0
- package/dist/client.js +5 -0
- package/dist/config.d.ts +28 -20
- package/dist/config.js +71 -75
- package/dist/content/content-api.d.ts +1 -1
- package/dist/content/node.d.ts +2 -3
- package/dist/content/node.js +2 -1
- package/dist/crash-message.d.ts +1 -0
- package/dist/crash-message.js +8 -0
- package/dist/css.d.ts +4 -0
- package/dist/css.js +5 -2
- package/dist/custom/CustomSection.d.ts +1 -1
- package/dist/custom/CustomSection.js +4 -8
- package/dist/custom/css.js +2 -2
- package/dist/custom/index.d.ts +6 -10
- package/dist/custom/index.js +1 -2
- package/dist/custom/layout.d.ts +5 -0
- package/dist/custom/layout.js +14 -0
- package/dist/entry-page.d.ts +3 -18
- package/dist/env.js +1 -1
- package/dist/global-id.js +2 -2
- package/dist/global.d.ts +5 -2
- package/dist/global.js +8 -5
- package/dist/image/next-config.d.ts +3 -0
- package/dist/image/next-config.js +12 -7
- package/dist/image/remote-pattern.d.ts +1 -0
- package/dist/image/remote-pattern.js +5 -0
- package/dist/index.d.ts +15 -17
- package/dist/index.js +10 -19
- package/dist/marks.d.ts +7 -0
- package/dist/marks.js +7 -0
- package/dist/next/admin-route.d.ts +7 -0
- package/dist/next/admin-route.js +28 -0
- package/dist/next/bridge.d.ts +1 -0
- package/dist/next/bridge.js +26 -0
- package/dist/next/canvas-boundary.d.ts +1 -1
- package/dist/next/canvas-slot.d.ts +1 -0
- package/dist/next/canvas-slot.js +4 -3
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/next/page-renderer.d.ts +5 -1
- package/dist/next/page-renderer.js +19 -33
- package/dist/next/preview-pill-define.d.ts +1 -0
- package/dist/next/preview-pill-define.js +10 -0
- package/dist/next/preview-pill.d.ts +0 -3
- package/dist/next/preview-pill.js +7 -50
- package/dist/next/queries.d.ts +3 -28
- package/dist/next/queries.js +3 -90
- package/dist/next/reads.d.ts +2 -0
- package/dist/next/reads.js +18 -0
- package/dist/page-context.d.ts +89 -0
- package/dist/page-context.js +8 -0
- package/dist/pairing.d.ts +2 -22
- package/dist/project.d.ts +69 -0
- package/dist/project.js +210 -0
- package/dist/published.d.ts +96 -0
- package/dist/published.js +125 -0
- package/dist/react.d.ts +61 -0
- package/dist/react.js +17 -0
- package/dist/read-shapes.d.ts +1 -1
- package/dist/reads.d.ts +80 -0
- package/dist/reads.js +142 -0
- package/dist/refs.d.ts +1 -1
- package/dist/registration.d.ts +8 -0
- package/dist/registration.js +17 -0
- package/dist/render.d.ts +5 -83
- package/dist/render.js +5 -10
- package/dist/resolve.d.ts +1 -1
- package/dist/richtext/index.d.ts +6 -2
- package/dist/richtext/index.js +3 -2
- package/dist/richtext-href.d.ts +4 -0
- package/dist/richtext-href.js +17 -0
- package/dist/richtext-render.js +1 -14
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/section-crash.d.ts +2 -2
- package/dist/section-crash.js +2 -7
- package/dist/section-wrapper.d.ts +3 -10
- package/dist/section-wrapper.js +3 -2
- package/dist/site.d.ts +38 -14
- package/dist/site.js +159 -286
- package/dist/smd-key.d.ts +9 -0
- package/dist/smd-key.js +1 -0
- package/dist/sql-space.d.ts +4 -0
- package/dist/sql-space.js +10 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +117 -0
- package/dist/store.d.ts +1 -1
- package/dist/supabase-published-store.d.ts +35 -0
- package/dist/supabase-published-store.js +215 -0
- package/dist/supabase-store.d.ts +1 -1
- package/dist/toolset.d.ts +2 -4
- package/dist/toolset.js +6 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view-props.d.ts +40 -0
- package/dist/view-props.js +1 -0
- package/dist/views.d.ts +36 -0
- package/dist/views.js +52 -0
- package/dist/zones.d.ts +7 -7
- package/dist/zones.js +7 -6
- package/migrations/20261007120000_smoodly_v8.sql +154 -0
- package/migrations/20261007200000_smoodly_v9.sql +30 -0
- package/package.json +46 -19
- package/dist/admin/next/bridge.d.ts +0 -2
- package/dist/admin/next/bridge.js +0 -129
- package/dist/admin/next/create-admin.d.ts +0 -19
- package/dist/admin/next/create-admin.js +0 -44
- package/dist/admin/next/index.d.ts +0 -8
- package/dist/admin/next/index.js +0 -8
- package/dist/admin/next/op-handler.d.ts +0 -18
- package/dist/admin/next/op-handler.js +0 -31
package/dist/site.js
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { elementSchemas } from "./richtext-walk.js";
|
|
5
|
-
import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
|
|
6
|
-
import { SMD_KEY } from "./section-wrapper.js";
|
|
7
|
-
import { resolveStyleValues } from "./styles.js";
|
|
8
|
-
import { resolveCustom } from "./custom/index.js";
|
|
9
|
-
import { addressMap, codeAddresses, lastPart } from "./addresses.js";
|
|
10
|
-
import { fullPath, servedAnchor } from "./anchors.js";
|
|
1
|
+
import { schemaOf } from "./entry-store.js";
|
|
2
|
+
import { entrySegment } from "./paths.js";
|
|
3
|
+
import { lastPart } from "./addresses.js";
|
|
11
4
|
import { withDefaults } from "./field-defaults.js";
|
|
12
5
|
import { modelKey } from "./model-key.js";
|
|
13
6
|
import { SMOODLY_VERSION } from "./version.js";
|
|
14
7
|
import { toEntryItem, toPageItem } from "./read-shapes.js";
|
|
15
|
-
import {
|
|
8
|
+
import { pageSeo } from "./seo.js";
|
|
9
|
+
import { checkEntryQuery, createAssembler, pageFieldsOf, siteShape } from "./assemble.js";
|
|
10
|
+
import { collectionOrder } from "./collections.js";
|
|
11
|
+
import { createProjector } from "./project.js";
|
|
16
12
|
/** The `page` prop for a loaded page. `href` is the app's seam from a
|
|
17
13
|
* locale-relative path to a site URL; the default keeps CMS paths as URLs. */
|
|
18
|
-
/** Field types whose stored value is an object: not filterable (memory and SQL would disagree). */
|
|
19
|
-
const OBJECT_FIELD_TYPES = new Set(["image", "video", "file", "richtext", "object"]);
|
|
20
14
|
export function pageContextOf(hit, href = (p) => p) {
|
|
21
15
|
const withUrl = (l) => ({ ...l, url: href(l.path, hit.locale) });
|
|
22
16
|
return {
|
|
@@ -32,18 +26,9 @@ export function pageContextOf(hit, href = (p) => p) {
|
|
|
32
26
|
};
|
|
33
27
|
}
|
|
34
28
|
export function createSmoodlySite(options) {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const custom = resolveCustom(options.custom);
|
|
39
|
-
const elements = elementSchemas(options.elements);
|
|
40
|
-
// Code-owned addresses from the site's own registry (spec 2026-09-29 nested paths §3): a code-owned root's path.
|
|
41
|
-
const codeOwned = codeAddresses({ locales, pages: options.pages, collections: options.collections });
|
|
42
|
-
const addresses = addressMap(codeOwned);
|
|
43
|
-
const pathOpts = { locales, homePageSlug, addresses };
|
|
44
|
-
/** The same addresses as the path index sees them: an entry's stored part joined to its collection's (spec 2026-09-29 anchored paths §2). */
|
|
45
|
-
const anchors = { addresses, homePageSlug, defaultLocale: locales.default };
|
|
46
|
-
const mounted = options.collections.filter((c) => c.path !== undefined);
|
|
29
|
+
const shape = siteShape(options);
|
|
30
|
+
const { locales, pageSchemas, homePageSlug, styles, custom, codeOwned } = shape;
|
|
31
|
+
const assembler = createAssembler(options, shape);
|
|
47
32
|
// Every cached read keys on it (spec 2026-09-30 tester findings §3): an upgrade or a model change starts a fresh cache.
|
|
48
33
|
const cacheKey = `${SMOODLY_VERSION}:${modelKey({
|
|
49
34
|
// Whole schemas: a collection's order, tree or title field changes a read as its fields do (functions drop out in the hash).
|
|
@@ -51,245 +36,95 @@ export function createSmoodlySite(options) {
|
|
|
51
36
|
globals: (options.globals ?? []).map((g) => ({ name: g.name, fields: g.fields, section: g.section })),
|
|
52
37
|
sections: options.sections.map((x) => ("schema" in x ? x.schema : x)),
|
|
53
38
|
elements: (options.elements ?? []).map((x) => ("schema" in x ? x.schema : x)),
|
|
54
|
-
pages:
|
|
39
|
+
pages: pageSchemas,
|
|
55
40
|
locales, homePageSlug, styles, custom,
|
|
56
41
|
})}`;
|
|
57
|
-
/** The resolver's fetcher, bound to a locale: the merged fields of that
|
|
58
|
-
* locale — published ones (a versioned collection's snapshot) on the
|
|
59
|
-
* public path, the live rows in draft mode. */
|
|
60
|
-
const fetcher = (draft, locale) => (collection, ids) => options.stores().entries.getMany(collection, ids, locale, draft ? undefined : { status: "published" });
|
|
61
|
-
/** The resolver's path lookup, bound to a locale: each EntryRef's address (spec 2026-09-29 content API contract §2). */
|
|
62
|
-
const pathLookup = (locale) => (collection, ids) => options.stores().entries.pathsIn(collection, ids, locale);
|
|
63
|
-
/** The media fetcher (spec 2026-09-08 §9) — absent when the site runs without an asset store. */
|
|
64
|
-
const assetFetcher = () => {
|
|
65
|
-
const assets = options.stores().assets;
|
|
66
|
-
return assets ? (ids) => assets.getMany(ids) : undefined;
|
|
67
|
-
};
|
|
68
|
-
const sectionSettings = (name) => {
|
|
69
|
-
const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
|
|
70
|
-
return (s && ("schema" in s ? s.schema : s).settings) ?? {};
|
|
71
|
-
};
|
|
72
|
-
/** Visible to the public in a locale: present there, and a folder or a
|
|
73
|
-
* published page. Draft mode sees every present node. Presence is the
|
|
74
|
-
* locale row — nothing falls back (spec 2026-09-06 §3). */
|
|
75
|
-
const visible = (node, locale, draft) => {
|
|
76
|
-
const row = node.locales[locale];
|
|
77
|
-
return row !== undefined && (draft || node.template === null || row.status === "published");
|
|
78
|
-
};
|
|
79
|
-
const hasPage = (node, locale, draft) => {
|
|
80
|
-
const row = node.locales[locale];
|
|
81
|
-
return node.template !== null && row !== undefined && (draft || row.status === "published");
|
|
82
|
-
};
|
|
83
|
-
const pageFields = (template) => {
|
|
84
|
-
if (template === null)
|
|
85
|
-
return {};
|
|
86
|
-
const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
|
|
87
|
-
return p?.schema.fields ?? {};
|
|
88
|
-
};
|
|
89
|
-
/** The version whose fields a link shows: the draft's in draft mode, else the published one. */
|
|
90
|
-
const versionFor = (node, locale, draft) => {
|
|
91
|
-
const row = node.locales[locale];
|
|
92
|
-
if (!row || node.template === null)
|
|
93
|
-
return null;
|
|
94
|
-
return draft ? row.draftVersionId : row.publishedVersionId;
|
|
95
|
-
};
|
|
96
|
-
/** Raw page fields (no asset resolution) of many nodes in ONE store read
|
|
97
|
-
* (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
|
|
98
|
-
async function linkFields(nodes, locale, draft) {
|
|
99
|
-
const { pages } = options.stores();
|
|
100
|
-
const wanted = new Map(); // node id → version id
|
|
101
|
-
for (const n of nodes) {
|
|
102
|
-
const v = versionFor(n, locale, draft);
|
|
103
|
-
if (v)
|
|
104
|
-
wanted.set(n.id, v);
|
|
105
|
-
}
|
|
106
|
-
const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
|
|
107
|
-
const out = new Map();
|
|
108
|
-
for (const n of nodes) {
|
|
109
|
-
const v = wanted.get(n.id);
|
|
110
|
-
out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
|
|
111
|
-
}
|
|
112
|
-
return out;
|
|
113
|
-
}
|
|
114
|
-
/** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
|
|
115
|
-
const link = (node, byId, locale, draft, fields) => ({
|
|
116
|
-
title: node.locales[locale].title,
|
|
117
|
-
path: pagePath(node, byId, locale, pathOpts),
|
|
118
|
-
hasPage: hasPage(node, locale, draft),
|
|
119
|
-
fields: fields.get(node.id) ?? {},
|
|
120
|
-
});
|
|
121
|
-
async function loadPage(id, locale, path, draft, version) {
|
|
122
|
-
const { pages } = options.stores();
|
|
123
|
-
const record = await pages.getPage(id);
|
|
124
|
-
if (!record || record.template === null)
|
|
125
|
-
return null;
|
|
126
|
-
const row = record.locales[locale];
|
|
127
|
-
if (!row)
|
|
128
|
-
return null;
|
|
129
|
-
let raw;
|
|
130
|
-
if (version !== undefined) {
|
|
131
|
-
// History: a stored version of THIS page in THIS locale, and only inside draft mode.
|
|
132
|
-
const stored = draft ? await pages.getVersion(version) : null;
|
|
133
|
-
raw = stored && stored.pageId === id && stored.locale === locale ? stored.tree : null;
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
raw = draft ? await pages.getDraftTree(id, locale) : row.status === "published" ? await pages.getPublishedTree(id, locale) : null;
|
|
137
|
-
}
|
|
138
|
-
if (!raw)
|
|
139
|
-
return null;
|
|
140
|
-
const tree = await resolveTree(raw, {
|
|
141
|
-
sections: options.sections,
|
|
142
|
-
collections: options.collections,
|
|
143
|
-
globals: options.globals,
|
|
144
|
-
elements: options.elements,
|
|
145
|
-
fetch: fetcher(draft, locale), paths: pathLookup(locale),
|
|
146
|
-
assets: assetFetcher(),
|
|
147
|
-
page: { fields: pageFields(record.template) },
|
|
148
|
-
custom,
|
|
149
|
-
});
|
|
150
|
-
const all = await pages.listPages();
|
|
151
|
-
const index = new Map(all.map((n) => [n.id, n]));
|
|
152
|
-
const byId = (x) => index.get(x);
|
|
153
|
-
const chain = chainOf(record, byId);
|
|
154
|
-
const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
|
|
155
|
-
const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
|
|
156
|
-
const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
|
|
157
|
-
const children = kids.map((n) => link(n, byId, locale, draft, fields));
|
|
158
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present
|
|
159
|
-
// one. Derived from the tree already listed — no index lookup (spec 2026-09-15 §2).
|
|
160
|
-
const alternates = pageAlternates(record, byId, pathOpts, (l) => draft || record.locales[l]?.status === "published");
|
|
161
|
-
const withSeo = (options.pages ?? []).some((x) => x.schema.kind === "page" && x.schema.name === record.template && x.schema.seo === true);
|
|
162
|
-
return {
|
|
163
|
-
kind: "page",
|
|
164
|
-
locale,
|
|
165
|
-
record,
|
|
166
|
-
template: record.template,
|
|
167
|
-
tree,
|
|
168
|
-
title: row.title,
|
|
169
|
-
path,
|
|
170
|
-
fields: withDefaults(tree.fields, pageFields(record.template)),
|
|
171
|
-
ancestors,
|
|
172
|
-
children,
|
|
173
|
-
locales: alternates,
|
|
174
|
-
...(withSeo ? { seo: pageSeo(tree.meta, row.title) } : {}),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
async function loadEntry(id, locale, path, draft) {
|
|
178
|
-
const { entries } = options.stores();
|
|
179
|
-
for (const schema of mounted) {
|
|
180
|
-
const record = await entries.get(schema.name, id);
|
|
181
|
-
if (!record)
|
|
182
|
-
continue;
|
|
183
|
-
// No row, or a draft row outside draft mode, is a 404 for THIS locale.
|
|
184
|
-
const row = record.locales[locale];
|
|
185
|
-
if (!row)
|
|
186
|
-
return null;
|
|
187
|
-
if (!draft && row.status !== "published")
|
|
188
|
-
return null;
|
|
189
|
-
const raw = draft
|
|
190
|
-
? entryFieldsIn(record, locale)
|
|
191
|
-
: (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
|
|
192
|
-
if (!raw)
|
|
193
|
-
return null;
|
|
194
|
-
const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
195
|
-
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
196
|
-
// One record is in memory (a nested entry's parents are not), so the `paths` index
|
|
197
|
-
// supplies the paths — the same single lookup as before (spec 2026-09-15 §2).
|
|
198
|
-
const known = await entries.pathsOf(schema.name, id);
|
|
199
|
-
const alternates = entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", (l) => known[l] ?? null);
|
|
200
|
-
return {
|
|
201
|
-
kind: "entry", locale, collection: schema.name, entry: { ...record, fields }, path, locales: alternates,
|
|
202
|
-
...(schema.seo ? { seo: entrySeo(fields, schema) } : {}),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
42
|
const localeOf = (o) => o?.locale ?? locales.default;
|
|
208
|
-
|
|
43
|
+
const published = () => options.stores().published;
|
|
44
|
+
const projector = createProjector({
|
|
45
|
+
assembler,
|
|
46
|
+
stores: () => { const s = options.stores(); return { pages: s.pages, entries: s.entries, published: s.published }; },
|
|
47
|
+
collections: options.collections, globals: options.globals ?? [], sections: options.sections, elements: options.elements,
|
|
48
|
+
locales, model: cacheKey,
|
|
49
|
+
});
|
|
50
|
+
// Delivery (spec 2026-10-07 delivery model §3–§4): outside draft mode a read takes the stored document when it is
|
|
51
|
+
// current — built by this model and not marked stale — and otherwise assembles that one document, stores it and serves it.
|
|
52
|
+
const current = (row) => row.model === cacheKey && !row.stale;
|
|
53
|
+
const fresh = (row) => current(row) && row.document != null;
|
|
54
|
+
const itemOf = (doc) => toEntryItem(doc.entry, doc.entry.fields, { path: doc.path, locale: doc.locale, locales: doc.locales, ...(doc.seo ? { seo: doc.seo } : {}) });
|
|
55
|
+
/** A page row's answer: its document when current (null for a folder or a hidden node), else the healed one. */
|
|
56
|
+
const pageOf = (row, id, locale) => row && current(row) ? Promise.resolve(row.document) : projector.healPage(id, locale, { seen: row !== null });
|
|
57
|
+
/** An entry document is served at an address only when its collection has addresses. */
|
|
58
|
+
const served = (doc) => (doc && doc.path !== null ? doc : null);
|
|
59
|
+
const entryOf = async (row, id, locale) => row && fresh(row) ? row.document : projector.healEntry(id, locale, { seen: row !== null });
|
|
60
|
+
/** The published documents of a collection's query. Stale rows are rebuilt in one batched projection and the
|
|
61
|
+
* query read once more (spec §4): a rebuild that finds an entry unpublished removes its row, so it drops out. */
|
|
62
|
+
async function deliveredEntries(collection, o) {
|
|
209
63
|
const schema = schemaOf(options.collections, collection);
|
|
64
|
+
checkEntryQuery(schema, collection, o);
|
|
210
65
|
const locale = localeOf(o);
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// The memory adapter's `===` never matches an object while SQL's `@>` matches a subset, so the two would disagree.
|
|
223
|
-
if (OBJECT_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
224
|
-
throw new Error(`smoodly: getEntries("${collection}"): "${key}" holds an object — filter on a scalar field instead.`);
|
|
66
|
+
const read = () => published().entries(collection, {
|
|
67
|
+
locale, where: o.where, order: o.order ?? collectionOrder(schema), limit: o.limit, ids: o.ids,
|
|
68
|
+
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
69
|
+
});
|
|
70
|
+
let rows = await read();
|
|
71
|
+
const behind = rows.filter((r) => !fresh(r)).map((r) => r.id);
|
|
72
|
+
if (behind.length > 0) {
|
|
73
|
+
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
74
|
+
try {
|
|
75
|
+
await projector.entries(behind, { cap: Infinity });
|
|
76
|
+
rows = await read();
|
|
225
77
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
229
|
-
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
|
|
78
|
+
catch (e) {
|
|
79
|
+
console.error(`smoodly: rebuilding ${behind.length} ${collection} documents in the delivery tables failed; served as read.`, e);
|
|
230
80
|
}
|
|
231
81
|
}
|
|
232
|
-
|
|
233
|
-
throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
|
|
234
|
-
}
|
|
235
|
-
// The store filters, orders and limits over the snapshot shown (spec 2026-09-29 content API contract §5): the
|
|
236
|
-
// locale gate — presence in draft mode, published otherwise — is part of the query, so `limit` counts rows that are really there.
|
|
237
|
-
const rows = await entries.query(collection, {
|
|
238
|
-
locale, draft, where: o.where, order: o.order, limit: o.limit, ids: o.ids,
|
|
239
|
-
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
240
|
-
});
|
|
241
|
-
// Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
|
|
242
|
-
const all = schema.tree ? await entries.list(collection) : rows.map((r) => r.record);
|
|
243
|
-
// References and media resolve for the whole list in one pass (§2, §6).
|
|
244
|
-
const sets = rows.map((r) => ({ values: structuredClone(r.fields), descriptors: schema.fields }));
|
|
245
|
-
await resolveFieldSets(sets, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
246
|
-
const index = new Map(all.map((e) => [e.id, e]));
|
|
247
|
-
const byId = (x) => index.get(x);
|
|
248
|
-
return rows.map(({ record }, i) => {
|
|
249
|
-
const paths = entryPathRows([record], byId, schema.name);
|
|
250
|
-
const pathIn = (l) => {
|
|
251
|
-
const row = paths.find((r) => r.locale === l);
|
|
252
|
-
return row ? fullPath(anchors, row.anchor ?? "", row.path, l) : null;
|
|
253
|
-
};
|
|
254
|
-
const item = toEntryItem(record, sets[i].values, {
|
|
255
|
-
path: pathIn(locale),
|
|
256
|
-
locale,
|
|
257
|
-
locales: entryAlternates(record, locales, (l) => draft || record.locales[l]?.status === "published", pathIn),
|
|
258
|
-
...(schema.seo ? { seo: entrySeo(sets[i].values, schema) } : {}),
|
|
259
|
-
});
|
|
260
|
-
return { record, item };
|
|
261
|
-
});
|
|
82
|
+
return rows.flatMap((r) => (r.document != null ? [r.document] : []));
|
|
262
83
|
}
|
|
263
|
-
const getEntries = async (collection, o = {}) => (await
|
|
84
|
+
const getEntries = async (collection, o = {}) => o.draft ? (await assembler.entries(collection, o)).map((r) => r.item) : (await deliveredEntries(collection, o)).map(itemOf);
|
|
85
|
+
/** Every entry of the collection in the locale, with the record a tree and a bare slug read `parentId` from. */
|
|
86
|
+
const readAll = async (collection, locale, draft) => draft
|
|
87
|
+
? assembler.entries(collection, { locale, draft })
|
|
88
|
+
: (await deliveredEntries(collection, { locale })).map((doc) => ({ record: doc.entry, item: itemOf(doc) }));
|
|
264
89
|
const api = {
|
|
265
90
|
async getPagesTree(o) {
|
|
266
91
|
const locale = localeOf(o);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
92
|
+
if (o?.draft)
|
|
93
|
+
return assembler.tree(locale, { draft: true });
|
|
94
|
+
// One light read. A row behind the model or marked stale is rebuilt before answering (spec §4), and the tree read
|
|
95
|
+
// again; a rebuild that finds the node gone or unpublished removes its row.
|
|
96
|
+
let rows = await published().tree(locale);
|
|
97
|
+
const behind = rows.filter((r) => !current(r)).map((r) => r.id);
|
|
98
|
+
if (behind.length > 0) {
|
|
99
|
+
// One batched projection for every row behind (one tree read in the projector), then one more tree read.
|
|
100
|
+
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
101
|
+
try {
|
|
102
|
+
await projector.pages(behind, { cap: Infinity });
|
|
103
|
+
rows = await published().tree(locale);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error(`smoodly: rebuilding ${behind.length} page documents in the delivery tables failed; served as read.`, e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// A node the tree hides (path null) leaves with its subtree, as does one whose parent has no row.
|
|
110
|
+
const build = (parentId) => rows.filter((r) => r.parentId === parentId && r.path !== null).map((r) => ({
|
|
111
|
+
id: r.id, title: r.title, path: r.path, hasPage: r.hasPage, fields: r.linkFields, children: build(r.id),
|
|
112
|
+
}));
|
|
113
|
+
return build(null);
|
|
286
114
|
},
|
|
287
115
|
async getPage(pathOrId, o) {
|
|
288
116
|
const locale = localeOf(o);
|
|
117
|
+
if (!o?.draft) {
|
|
118
|
+
// The content API stays strict (§7): a redirect row, and a code-owned page with no record, are not pages.
|
|
119
|
+
const hit = pathOrId.startsWith("/")
|
|
120
|
+
? (await delivered(locale, pathOrId, false)).hit
|
|
121
|
+
: await pageOf(await published().page(pathOrId, locale), pathOrId, locale);
|
|
122
|
+
return hit?.kind === "page" ? toPageItem(hit) : null;
|
|
123
|
+
}
|
|
289
124
|
const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
|
|
290
125
|
if (!path)
|
|
291
126
|
return null;
|
|
292
|
-
const hit = await site.resolve(locale, path, { draft:
|
|
127
|
+
const hit = await site.resolve(locale, path, { draft: true });
|
|
293
128
|
return hit?.kind === "page" ? toPageItem(hit) : null;
|
|
294
129
|
},
|
|
295
130
|
getEntries,
|
|
@@ -298,64 +133,78 @@ export function createSmoodlySite(options) {
|
|
|
298
133
|
const locale = localeOf(o);
|
|
299
134
|
const draft = o?.draft ?? false;
|
|
300
135
|
if (idOrPath.startsWith("/")) {
|
|
301
|
-
const hit = await site.resolve(locale, idOrPath, { draft });
|
|
136
|
+
const hit = draft ? await site.resolve(locale, idOrPath, { draft }) : (await delivered(locale, idOrPath, false)).hit;
|
|
302
137
|
return hit?.kind === "entry" && hit.collection === collection ? toEntryItem(hit.entry, hit.entry.fields, hit) : null;
|
|
303
138
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
139
|
+
if (draft) {
|
|
140
|
+
const [byId] = await assembler.entries(collection, { locale, draft, ids: [idOrPath] });
|
|
141
|
+
if (byId)
|
|
142
|
+
return byId.item;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const row = await published().entry(idOrPath, locale);
|
|
146
|
+
if (row && row.collection === collection && fresh(row))
|
|
147
|
+
return itemOf(row.document);
|
|
148
|
+
// A miss is a record no projection wrote yet, or a slug: one `get` tells them apart.
|
|
149
|
+
if (row?.collection === collection || (await options.stores().entries.get(collection, idOrPath))) {
|
|
150
|
+
const doc = await projector.healEntry(idOrPath, locale, { seen: row !== null });
|
|
151
|
+
if (doc)
|
|
152
|
+
return itemOf(doc);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
307
155
|
// A bare slug names a ROOT entry only (sibling-scoped slugs, spec §4):
|
|
308
156
|
// match the localized slug directly (paths.ts owns slug derivation),
|
|
309
157
|
// regardless of whether the collection even has a `path`.
|
|
310
|
-
const all = await
|
|
158
|
+
const all = await readAll(collection, locale, draft);
|
|
311
159
|
return all.find((r) => r.record.parentId === null && entrySegment(r.record, locale) === idOrPath)?.item ?? null;
|
|
312
160
|
},
|
|
313
161
|
async getEntriesTree(collection, o) {
|
|
314
|
-
const rows = await
|
|
162
|
+
const rows = await readAll(collection, localeOf(o), o?.draft ?? false);
|
|
315
163
|
const build = (parentId) => rows.filter((r) => r.record.parentId === parentId).map((r) => ({ ...r.item, children: build(r.record.id) }));
|
|
316
164
|
return build(null);
|
|
317
165
|
},
|
|
318
166
|
async getGlobal(item, o) {
|
|
319
167
|
const locale = localeOf(o);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const row = record.locales[locale];
|
|
326
|
-
if (!row || (!draft && row.status !== "published"))
|
|
327
|
-
return null;
|
|
328
|
-
const raw = draft
|
|
329
|
-
? entryFieldsIn(record, locale)
|
|
330
|
-
: (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
|
|
331
|
-
if (!raw)
|
|
332
|
-
return null;
|
|
333
|
-
const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), paths: pathLookup(locale), assets: assetFetcher() });
|
|
334
|
-
if (!item.section)
|
|
335
|
-
return { locale, fields };
|
|
336
|
-
const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
|
|
337
|
-
const fetch = assetFetcher();
|
|
338
|
-
const own = settings !== null && typeof settings === "object" ? settings : {};
|
|
339
|
-
if (fetch)
|
|
340
|
-
await resolveMedia(own, sectionSettings(item.section), fetch, elements);
|
|
341
|
-
// The layout node id, the css and the values ride the styles under the reserved key (spec 2026-09-13 §1, 2026-09-14 §2, §4):
|
|
342
|
-
// the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
|
|
343
|
-
const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
|
|
344
|
-
return {
|
|
345
|
-
locale,
|
|
346
|
-
fields: rest,
|
|
347
|
-
settings: own,
|
|
348
|
-
styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
|
|
349
|
-
};
|
|
168
|
+
if (o?.draft)
|
|
169
|
+
return assembler.global(item, locale, { draft: true });
|
|
170
|
+
// The item's row by its collection, in one read: a global is a one-record collection.
|
|
171
|
+
const [row] = await published().entries(item.name, { locale, limit: 1 });
|
|
172
|
+
return row && fresh(row) ? row.document : projector.healGlobal(item, locale, { seen: row !== undefined });
|
|
350
173
|
},
|
|
351
174
|
};
|
|
175
|
+
/** The published read of an address; `code`: the code-owned page answers where the index has no row. */
|
|
176
|
+
/** `globals`: the names of global items to bring along (schema v9); `rows` is what the route carried, when it had any. */
|
|
177
|
+
async function delivered(locale, path, code, globals) {
|
|
178
|
+
const route = await published().byAddress(locale, path, globals);
|
|
179
|
+
const rows = route?.globals;
|
|
180
|
+
if (route && fresh(route.row))
|
|
181
|
+
return { hit: route.row.document, rows };
|
|
182
|
+
let healed = null;
|
|
183
|
+
if (route) {
|
|
184
|
+
// Stale: rebuilt by id. Unpublished or moved since the row was written, the address is answered as a miss.
|
|
185
|
+
healed = route.kind === "page" ? await projector.healPage(route.row.id, locale) : served(await projector.healEntry(route.row.id, locale));
|
|
186
|
+
if (healed && healed.path === path)
|
|
187
|
+
return { hit: healed, rows };
|
|
188
|
+
}
|
|
189
|
+
// A miss: the paths index doubles as the existence check — a record to assemble, a redirect row, or nothing.
|
|
190
|
+
// A record wins over the code-owned page at its address, as the renderer has it.
|
|
191
|
+
const hit = await site.locate(locale, path);
|
|
192
|
+
if (!hit)
|
|
193
|
+
return { hit: code ? codePage(locale, path) : null };
|
|
194
|
+
if (hit.redirect)
|
|
195
|
+
return { hit: { kind: "redirect", target: { kind: hit.kind, id: hit.id } } };
|
|
196
|
+
if (route && hit.id === route.row.id)
|
|
197
|
+
return { hit: healed, rows }; // just rebuilt
|
|
198
|
+
// By id: a page the tree hides has a current row the address lookup cannot reach; anything else heals there.
|
|
199
|
+
return { hit: await site.load(hit, locale, path), rows };
|
|
200
|
+
}
|
|
352
201
|
const codePath = (address) => (address === homePageSlug ? "/" : `/${address}`);
|
|
353
202
|
function codePage(locale, path) {
|
|
354
203
|
const spec = codeOwned.find((a) => a.template !== null && a.segments[locale] !== undefined && codePath(a.segments[locale]) === path);
|
|
355
204
|
if (!spec || !locales.supported.includes(locale))
|
|
356
205
|
return null;
|
|
357
206
|
const template = spec.template;
|
|
358
|
-
const withSeo =
|
|
207
|
+
const withSeo = pageSchemas.some((x) => x.kind === "page" && x.name === template && x.seo === true);
|
|
359
208
|
return {
|
|
360
209
|
kind: "page",
|
|
361
210
|
locale,
|
|
@@ -365,7 +214,7 @@ export function createSmoodlySite(options) {
|
|
|
365
214
|
tree: { zones: {} },
|
|
366
215
|
title: spec.title,
|
|
367
216
|
path,
|
|
368
|
-
fields: withDefaults(undefined,
|
|
217
|
+
fields: withDefaults(undefined, pageFieldsOf(shape, template)),
|
|
369
218
|
ancestors: [],
|
|
370
219
|
children: [],
|
|
371
220
|
locales: Object.fromEntries(Object.entries(spec.segments).map(([l, address]) => [l, { slug: lastPart(address), path: codePath(address) }])),
|
|
@@ -380,9 +229,32 @@ export function createSmoodlySite(options) {
|
|
|
380
229
|
custom,
|
|
381
230
|
locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
|
|
382
231
|
targetPaths: (target) => options.stores().pages.targetPaths(target),
|
|
383
|
-
load(target, locale, path, opts = {}) {
|
|
232
|
+
async load(target, locale, path, opts = {}) {
|
|
384
233
|
const draft = opts.draft ?? false;
|
|
385
|
-
|
|
234
|
+
// Draft mode assembles from the editing tables; so does a stored version, which outside draft mode is nothing.
|
|
235
|
+
if (draft || opts.version !== undefined) {
|
|
236
|
+
return target.kind === "page" ? assembler.page(target.id, locale, path, { draft, version: opts.version }) : assembler.entry(target.id, locale, path, { draft });
|
|
237
|
+
}
|
|
238
|
+
return target.kind === "page"
|
|
239
|
+
? pageOf(await published().page(target.id, locale), target.id, locale)
|
|
240
|
+
: served(await entryOf(await published().entry(target.id, locale), target.id, locale));
|
|
241
|
+
},
|
|
242
|
+
lookup: async (locale, path) => (await delivered(locale, path, true)).hit,
|
|
243
|
+
async lookupWithGlobals(locale, path, items) {
|
|
244
|
+
const { hit, rows } = await delivered(locale, path, true, items.map((i) => i.name));
|
|
245
|
+
const globals = new Map();
|
|
246
|
+
if (rows) {
|
|
247
|
+
for (const item of items) {
|
|
248
|
+
const row = rows.find((r) => r.collection === item.name);
|
|
249
|
+
// No row: unpublished here, or never projected — left out rather than healed on every read, because a
|
|
250
|
+
// heal of an unpublished item writes nothing and the next read would heal again; the layout's own read
|
|
251
|
+
// pays the call where a page renders it. A stale row heals once and is written.
|
|
252
|
+
if (!row)
|
|
253
|
+
continue;
|
|
254
|
+
globals.set(item.name, fresh(row) ? row.document : await projector.healGlobal(item, locale, { seen: true }));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { hit, globals };
|
|
386
258
|
},
|
|
387
259
|
async resolve(locale, path, opts) {
|
|
388
260
|
// The content API stays strict (§7): a content address is not a redirect.
|
|
@@ -390,6 +262,7 @@ export function createSmoodlySite(options) {
|
|
|
390
262
|
return target && !target.redirect ? site.load(target, locale, path, opts) : null;
|
|
391
263
|
},
|
|
392
264
|
codePage,
|
|
265
|
+
projector,
|
|
393
266
|
...api,
|
|
394
267
|
};
|
|
395
268
|
return site;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StyleValues } from "./styles.ts";
|
|
2
|
+
export declare const SMD_KEY = "__smd";
|
|
3
|
+
export type SmdInternal = {
|
|
4
|
+
node?: string;
|
|
5
|
+
css?: string;
|
|
6
|
+
values?: StyleValues;
|
|
7
|
+
/** Pre-scoped layout CSS (the custom section, spec 2026-09-14 §6), emitted before the editor's css so the box wins. */
|
|
8
|
+
layoutCss?: string;
|
|
9
|
+
};
|
package/dist/smd-key.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SMD_KEY = "__smd";
|
package/dist/sql-space.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export declare const DEFAULT_SPACE = "'00000000-0000-0000-0000-000000000000'";
|
|
|
6
6
|
export declare const SPACE_COL = "\"space_id\" uuid not null default '00000000-0000-0000-0000-000000000000'";
|
|
7
7
|
export declare const CONTENT_TABLES: readonly ["pages", "page_locales", "page_versions", "entries", "entry_locales", "entry_versions", "refs", "paths", "assets"];
|
|
8
8
|
export type ContentTable = (typeof CONTENT_TABLES)[number];
|
|
9
|
+
/** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
|
|
10
|
+
* space-stamped like content, but not transferred (export skips them, import
|
|
11
|
+
* rebuilds them) and not counted toward the row cap, since they are derived. */
|
|
12
|
+
export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries"];
|
|
9
13
|
export declare function currentSpaceSQL(): string;
|
|
10
14
|
/** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
|
|
11
15
|
* the write key's insert, update and delete, each alone for its action; its
|
package/dist/sql-space.js
CHANGED
|
@@ -19,6 +19,10 @@ export const CONTENT_TABLES = [
|
|
|
19
19
|
"paths",
|
|
20
20
|
"assets",
|
|
21
21
|
];
|
|
22
|
+
/** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
|
|
23
|
+
* space-stamped like content, but not transferred (export skips them, import
|
|
24
|
+
* rebuilds them) and not counted toward the row cap, since they are derived. */
|
|
25
|
+
export const DELIVERY_TABLES = ["published_pages", "published_entries"];
|
|
22
26
|
export function currentSpaceSQL() {
|
|
23
27
|
return `
|
|
24
28
|
-- The space a request acts in: the key's claim in Smoodly Cloud, the
|
|
@@ -55,7 +59,7 @@ const WRITE = `${OWN} and (select "smoodly_key_kind"()) = 'write'`;
|
|
|
55
59
|
* policy, which is also their only select. */
|
|
56
60
|
export function contentPoliciesSQL() {
|
|
57
61
|
const statements = [];
|
|
58
|
-
for (const table of [...CONTENT_TABLES, "members"]) {
|
|
62
|
+
for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
|
|
59
63
|
if (table === "refs" || table === "members") {
|
|
60
64
|
statements.push(`drop policy if exists "smoodly_write" on "${table}";\n` +
|
|
61
65
|
`create policy "smoodly_write" on "${table}" for all\n` +
|
|
@@ -190,7 +194,7 @@ $$;`.trim());
|
|
|
190
194
|
statements.push(`create index if not exists "${table}_space_idx" on "${table}" ("space_id");`);
|
|
191
195
|
}
|
|
192
196
|
// Trigger names sort a_ before b_: same-event triggers fire alphabetically.
|
|
193
|
-
for (const table of [...CONTENT_TABLES, "members"]) {
|
|
197
|
+
for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
|
|
194
198
|
statements.push(`drop trigger if exists "smoodly_a_set_space" on "${table}";\n` +
|
|
195
199
|
`create trigger "smoodly_a_set_space" before insert on "${table}"\n` +
|
|
196
200
|
` for each row execute function "smoodly_set_space"();`);
|
|
@@ -270,9 +274,10 @@ export function mediaPoliciesSQL() {
|
|
|
270
274
|
* public. `smoodly_current_space` is NOT here — the stores read it through
|
|
271
275
|
* rpc(). Last in the schema, after every definition exists. */
|
|
272
276
|
export function grantsSQL() {
|
|
273
|
-
// smoodly_list_entries
|
|
274
|
-
// rights and the read policies decide what a key
|
|
275
|
-
// content API contract §5), so there is nothing to
|
|
277
|
+
// smoodly_list_entries and smoodly_list_published are callable by every
|
|
278
|
+
// role on purpose: invoker rights and the read policies decide what a key
|
|
279
|
+
// sees (spec 2026-09-29 content API contract §5), so there is nothing to
|
|
280
|
+
// revoke; the published_routes view is security_invoker for the same reason.
|
|
276
281
|
return [
|
|
277
282
|
`revoke execute on function "smoodly_reorder"(text, uuid[]) from public, anon;`,
|
|
278
283
|
`revoke execute on function "smoodly_reorder_pages"(uuid[]) from public, anon;`,
|
package/dist/sql.d.ts
CHANGED
|
@@ -10,4 +10,6 @@ export declare function foreignKeyIndexesSQL(): string;
|
|
|
10
10
|
* §5). Invoker rights, so RLS decides what a key sees. Defined after the
|
|
11
11
|
* space layer, whose `smoodly_key_kind` it calls. */
|
|
12
12
|
export declare function listEntriesSQL(): string;
|
|
13
|
+
/** The delivery model (spec 2026-10-07 §1): two derived tables, a list function over the raw snapshot, a routes view. */
|
|
14
|
+
export declare function publishedTablesSQL(): string;
|
|
13
15
|
export declare function coreTablesSQL(): string;
|