smoodly 0.0.45 → 0.0.47
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/health.d.ts +4 -0
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/ops-impl.d.ts +7 -1
- package/dist/admin/ops-impl.js +112 -35
- 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/ContentHealthPage.js +3 -1
- 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 +13 -0
- package/dist/cli/migrate.js +30 -2
- 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/document-stamp.d.ts +7 -0
- package/dist/document-stamp.js +48 -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 +85 -0
- package/dist/project.js +228 -0
- package/dist/published.d.ts +115 -0
- package/dist/published.js +136 -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 +40 -14
- package/dist/site.js +199 -284
- package/dist/smd-key.d.ts +9 -0
- package/dist/smd-key.js +1 -0
- package/dist/sql-space.d.ts +5 -0
- package/dist/sql-space.js +11 -5
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +127 -1
- package/dist/store.d.ts +1 -1
- package/dist/supabase-published-store.d.ts +45 -0
- package/dist/supabase-published-store.js +247 -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/migrations/20261008120000_smoodly_v10.sql +47 -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/sql-space.js
CHANGED
|
@@ -19,6 +19,11 @@ 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
|
+
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current. */
|
|
26
|
+
export const DELIVERY_TABLES = ["published_pages", "published_entries", "published_models"];
|
|
22
27
|
export function currentSpaceSQL() {
|
|
23
28
|
return `
|
|
24
29
|
-- The space a request acts in: the key's claim in Smoodly Cloud, the
|
|
@@ -55,7 +60,7 @@ const WRITE = `${OWN} and (select "smoodly_key_kind"()) = 'write'`;
|
|
|
55
60
|
* policy, which is also their only select. */
|
|
56
61
|
export function contentPoliciesSQL() {
|
|
57
62
|
const statements = [];
|
|
58
|
-
for (const table of [...CONTENT_TABLES, "members"]) {
|
|
63
|
+
for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
|
|
59
64
|
if (table === "refs" || table === "members") {
|
|
60
65
|
statements.push(`drop policy if exists "smoodly_write" on "${table}";\n` +
|
|
61
66
|
`create policy "smoodly_write" on "${table}" for all\n` +
|
|
@@ -190,7 +195,7 @@ $$;`.trim());
|
|
|
190
195
|
statements.push(`create index if not exists "${table}_space_idx" on "${table}" ("space_id");`);
|
|
191
196
|
}
|
|
192
197
|
// Trigger names sort a_ before b_: same-event triggers fire alphabetically.
|
|
193
|
-
for (const table of [...CONTENT_TABLES, "members"]) {
|
|
198
|
+
for (const table of [...CONTENT_TABLES, ...DELIVERY_TABLES, "members"]) {
|
|
194
199
|
statements.push(`drop trigger if exists "smoodly_a_set_space" on "${table}";\n` +
|
|
195
200
|
`create trigger "smoodly_a_set_space" before insert on "${table}"\n` +
|
|
196
201
|
` for each row execute function "smoodly_set_space"();`);
|
|
@@ -270,9 +275,10 @@ export function mediaPoliciesSQL() {
|
|
|
270
275
|
* public. `smoodly_current_space` is NOT here — the stores read it through
|
|
271
276
|
* rpc(). Last in the schema, after every definition exists. */
|
|
272
277
|
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
|
|
278
|
+
// smoodly_list_entries and smoodly_list_published are callable by every
|
|
279
|
+
// role on purpose: invoker rights and the read policies decide what a key
|
|
280
|
+
// sees (spec 2026-09-29 content API contract §5), so there is nothing to
|
|
281
|
+
// revoke; the published_routes view is security_invoker for the same reason.
|
|
276
282
|
return [
|
|
277
283
|
`revoke execute on function "smoodly_reorder"(text, uuid[]) from public, anon;`,
|
|
278
284
|
`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;
|
package/dist/sql.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// locale (spec 2026-09-06 §7); a versioned collection reads its
|
|
10
10
|
// fields from the published snapshot.
|
|
11
11
|
import { SEO_KEY } from "./seo.js";
|
|
12
|
-
import { currentSpaceSQL, grantsSQL, SPACE_COL, spaceLayerSQL } from "./sql-space.js";
|
|
12
|
+
import { currentSpaceSQL, grantsSQL, SPACE_COL, spaceLayerSQL, DEFAULT_SPACE } from "./sql-space.js";
|
|
13
13
|
const snake = (s) => s.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
14
14
|
const q = (s) => `"${s}"`;
|
|
15
15
|
// Casts are fine in views.
|
|
@@ -120,6 +120,131 @@ create or replace function "smoodly_list_entries"(
|
|
|
120
120
|
limit p_limit;
|
|
121
121
|
$$;`.trim();
|
|
122
122
|
}
|
|
123
|
+
/** The delivery model (spec 2026-10-07 §1): two derived tables, a list function over the raw snapshot, a routes view. */
|
|
124
|
+
export function publishedTablesSQL() {
|
|
125
|
+
return `
|
|
126
|
+
-- The delivery model (spec 2026-10-07): Publish writes the finished document of a page, an entry
|
|
127
|
+
-- or a global here; a live read is one lookup. Derived and rebuildable from the editing tables.
|
|
128
|
+
create table if not exists "published_pages" (
|
|
129
|
+
${SPACE_COL},
|
|
130
|
+
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
131
|
+
"locale" text not null,
|
|
132
|
+
"parent_id" uuid,
|
|
133
|
+
"sort" numeric,
|
|
134
|
+
"path" text,
|
|
135
|
+
"title" text not null,
|
|
136
|
+
"has_page" boolean not null,
|
|
137
|
+
"link_fields" jsonb not null default '{}'::jsonb,
|
|
138
|
+
"document" jsonb,
|
|
139
|
+
"tags" text[] not null default '{}',
|
|
140
|
+
"model" text not null,
|
|
141
|
+
"stale" boolean not null default false,
|
|
142
|
+
"published_at" timestamptz not null default now(),
|
|
143
|
+
primary key ("page_id", "locale")
|
|
144
|
+
);
|
|
145
|
+
create unique index if not exists "published_pages_address_idx"
|
|
146
|
+
on "published_pages" ("space_id", "locale", "path") where "path" is not null;
|
|
147
|
+
create index if not exists "published_pages_tree_idx"
|
|
148
|
+
on "published_pages" ("space_id", "locale", "parent_id", "sort");
|
|
149
|
+
|
|
150
|
+
create table if not exists "published_entries" (
|
|
151
|
+
${SPACE_COL},
|
|
152
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
153
|
+
"locale" text not null,
|
|
154
|
+
"collection" text not null,
|
|
155
|
+
"parent_id" uuid,
|
|
156
|
+
"sort" numeric,
|
|
157
|
+
"slug" text,
|
|
158
|
+
"path" text,
|
|
159
|
+
"created_at" timestamptz not null,
|
|
160
|
+
"updated_at" timestamptz not null,
|
|
161
|
+
"fields" jsonb not null,
|
|
162
|
+
"document" jsonb not null,
|
|
163
|
+
"tags" text[] not null default '{}',
|
|
164
|
+
"model" text not null,
|
|
165
|
+
"stale" boolean not null default false,
|
|
166
|
+
"published_at" timestamptz not null default now(),
|
|
167
|
+
primary key ("entry_id", "locale")
|
|
168
|
+
);
|
|
169
|
+
create index if not exists "published_entries_list_idx"
|
|
170
|
+
on "published_entries" ("space_id", "collection", "locale");
|
|
171
|
+
create unique index if not exists "published_entries_address_idx"
|
|
172
|
+
on "published_entries" ("space_id", "locale", "path") where "path" is not null;
|
|
173
|
+
create index if not exists "published_entries_fields_idx"
|
|
174
|
+
on "published_entries" using gin ("fields");
|
|
175
|
+
|
|
176
|
+
-- The document stamp a space recorded as current (DESIGN §3, schema v10): set by \`npx smoodly rebuild\`
|
|
177
|
+
-- and by the first writer into an empty record; only a reader at this stamp rewrites the rows above.
|
|
178
|
+
create table if not exists "published_models" (
|
|
179
|
+
"space_id" uuid primary key default ${DEFAULT_SPACE},
|
|
180
|
+
"model" text not null,
|
|
181
|
+
"recorded_at" timestamptz not null default now()
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
alter table "published_pages" enable row level security;
|
|
185
|
+
alter table "published_entries" enable row level security;
|
|
186
|
+
alter table "published_models" enable row level security;
|
|
187
|
+
|
|
188
|
+
-- smoodly_list_entries without the draft branch, over the delivery table (spec 2026-10-07 §3):
|
|
189
|
+
-- \`where\` and \`order\` read the raw published snapshot, the row answers with its document.
|
|
190
|
+
-- Invoker rights, so RLS decides what a key sees.
|
|
191
|
+
create or replace function "smoodly_list_published"(
|
|
192
|
+
p_collection text, p_locale text, p_where jsonb, p_order jsonb,
|
|
193
|
+
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
194
|
+
) returns table ("id" uuid, "document" jsonb, "model" text, "stale" boolean) language sql stable set search_path = public as $$
|
|
195
|
+
with shown as (
|
|
196
|
+
select p."entry_id" as "id", p."sort", p."created_at", p."updated_at", p."fields", p."document", p."model", p."stale"
|
|
197
|
+
from "published_entries" p
|
|
198
|
+
where p."collection" = p_collection and p."locale" = p_locale
|
|
199
|
+
and (not p_has_parent or p."parent_id" is not distinct from p_parent)
|
|
200
|
+
and (p_ids is null or p."entry_id" = any(p_ids))
|
|
201
|
+
)
|
|
202
|
+
select s."id", s."document", s."model", s."stale" from shown s
|
|
203
|
+
where not exists (
|
|
204
|
+
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
205
|
+
where not (case when w.value ? 'in'
|
|
206
|
+
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
207
|
+
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
208
|
+
order by
|
|
209
|
+
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
210
|
+
case when p_order ->> 'direction' = 'asc' then
|
|
211
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
212
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
213
|
+
case when p_order ->> 'direction' = 'desc' then
|
|
214
|
+
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
215
|
+
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
216
|
+
s."created_at" asc, s."id" asc
|
|
217
|
+
limit p_limit;
|
|
218
|
+
$$;
|
|
219
|
+
|
|
220
|
+
-- One lookup for any address (spec 2026-10-07 §3): a page or an entry, under the caller's own policies.
|
|
221
|
+
create or replace view "published_routes" with (security_invoker = true) as
|
|
222
|
+
select "space_id", "locale", "path", 'page' as "kind", "page_id" as "id", "document", "model", "stale", "tags", null::text as "collection"
|
|
223
|
+
from "published_pages" where "path" is not null and "has_page"
|
|
224
|
+
union all
|
|
225
|
+
select "space_id", "locale", "path", 'entry' as "kind", "entry_id" as "id", "document", "model", "stale", "tags", "collection"
|
|
226
|
+
from "published_entries" where "path" is not null;
|
|
227
|
+
|
|
228
|
+
-- The address and the locale's global items in one call (schema v9; register 2026-10-07: a miss was two
|
|
229
|
+
-- rounds, the page then the layout's globals). The route row first, kind 'page' or 'entry'; then one row per
|
|
230
|
+
-- named global, kind 'global', the collection its name. A miss answers nothing: the globals ride with a page
|
|
231
|
+
-- only. Invoker rights, so RLS decides what a key sees.
|
|
232
|
+
create or replace function "smoodly_lookup_address"(p_locale text, p_path text, p_globals text[])
|
|
233
|
+
returns table ("kind" text, "id" uuid, "collection" text, "document" jsonb, "model" text, "stale" boolean, "tags" text[])
|
|
234
|
+
language sql stable set search_path = public as $$
|
|
235
|
+
with route as (
|
|
236
|
+
select r."kind", r."id", r."collection", r."document", r."model", r."stale", r."tags"
|
|
237
|
+
from "published_routes" r where r."locale" = p_locale and r."path" = p_path limit 1
|
|
238
|
+
)
|
|
239
|
+
select * from route
|
|
240
|
+
union all
|
|
241
|
+
select 'global' as "kind", e."entry_id", e."collection", e."document", e."model", e."stale", e."tags"
|
|
242
|
+
from "published_entries" e
|
|
243
|
+
where e."locale" = p_locale and e."collection" = any(coalesce(p_globals, '{}'::text[]))
|
|
244
|
+
and exists (select 1 from route);
|
|
245
|
+
$$;
|
|
246
|
+
`.trim();
|
|
247
|
+
}
|
|
123
248
|
export function coreTablesSQL() {
|
|
124
249
|
return [
|
|
125
250
|
// The trigger/policy blocks below drop-before-create so the migration is
|
|
@@ -413,6 +538,7 @@ alter table "paths" enable row level security;
|
|
|
413
538
|
alter table "assets" enable row level security;
|
|
414
539
|
alter table "smoodly_meta" enable row level security;
|
|
415
540
|
`.trim(),
|
|
541
|
+
publishedTablesSQL(),
|
|
416
542
|
spaceLayerSQL(),
|
|
417
543
|
listEntriesSQL(),
|
|
418
544
|
grantsSQL(),
|
package/dist/store.d.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
+
import type { PublishedEntryRow, PublishedKey, PublishedPageRow, PublishedQuery, PublishedRoute, PublishedStore, PublishedTreeRow } from "./published.ts";
|
|
4
|
+
export declare class SupabasePublishedStore implements PublishedStore {
|
|
5
|
+
private db;
|
|
6
|
+
private schemas;
|
|
7
|
+
private spaceId;
|
|
8
|
+
/** `collections` encode a `where` on an array field as containment, as SupabaseEntryStore.query does. */
|
|
9
|
+
constructor(db: SupabaseClient, collections?: CollectionSchema<any>[]);
|
|
10
|
+
private fail;
|
|
11
|
+
putPages(rows: PublishedPageRow[]): Promise<void>;
|
|
12
|
+
putEntries(rows: PublishedEntryRow[]): Promise<void>;
|
|
13
|
+
/** The space this client writes in (the service role's is the default): a path delete must not reach past it,
|
|
14
|
+
* since the service role passes every policy. Cached for the life of the client; a failed lookup is not. */
|
|
15
|
+
private space;
|
|
16
|
+
/** The unique (space, locale, path) index holds one row per address: a row of another id at an incoming address
|
|
17
|
+
* (a stale row a capped rename did not reach) is deleted before the upsert. Not atomic with it: rows are derived,
|
|
18
|
+
* and a read heals what a failure between the two leaves. One delete per locale in the chunk. */
|
|
19
|
+
private vacate;
|
|
20
|
+
/** One request per kind and locale filter: the keys without a locale go in one in(). */
|
|
21
|
+
private eachGroup;
|
|
22
|
+
remove(keys: PublishedKey[]): Promise<void>;
|
|
23
|
+
/** The space's recorded stamp (DESIGN §3): one row per space in published_models; the key's space, or the
|
|
24
|
+
* service role's default one. */
|
|
25
|
+
recordedModel(): Promise<string | null>;
|
|
26
|
+
recordModel(model: string): Promise<void>;
|
|
27
|
+
/** Four head counts (no rows travel): each table's rows, and its rows at another stamp. The policies scope a
|
|
28
|
+
* write key to its space; the service role counts the default space's rows by the explicit filter. */
|
|
29
|
+
countBehind(model: string): Promise<{
|
|
30
|
+
behind: number;
|
|
31
|
+
total: number;
|
|
32
|
+
}>;
|
|
33
|
+
markStale(keys: PublishedKey[]): Promise<void>;
|
|
34
|
+
/** A route row is for serving, not for the tree: a page route carries the
|
|
35
|
+
* document, model, stale flag and tags; its tree columns are empty, and an
|
|
36
|
+
* entry route's `fields` and times likewise. */
|
|
37
|
+
byAddress(locale: string, path: string, globals?: readonly string[]): Promise<PublishedRoute | null>;
|
|
38
|
+
private route;
|
|
39
|
+
page(id: string, locale: string): Promise<PublishedPageRow | null>;
|
|
40
|
+
entry(id: string, locale: string): Promise<PublishedEntryRow | null>;
|
|
41
|
+
/** A list row carries the document, model and stale flag; the raw `fields`
|
|
42
|
+
* and the tree columns stay in the table (a list never needs them). */
|
|
43
|
+
entries(collection: string, q: PublishedQuery): Promise<PublishedEntryRow[]>;
|
|
44
|
+
tree(locale: string): Promise<PublishedTreeRow[]>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// The Supabase PublishedStore (spec 2026-10-07 delivery model §1, §3): rows in
|
|
2
|
+
// published_pages and published_entries, one address lookup through the
|
|
3
|
+
// published_routes view, lists through smoodly_list_published. Every read is
|
|
4
|
+
// one request; the space is the key's, through the policies as everywhere.
|
|
5
|
+
import { readCurrentSpace } from "./current-space.js";
|
|
6
|
+
import { ARRAY_FIELD_TYPES } from "./entry-store.js";
|
|
7
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const CHUNK = 100;
|
|
9
|
+
const TREE_COLUMNS = "page_id, parent_id, sort, path, title, has_page, link_fields, model, stale";
|
|
10
|
+
const num = (v) => (v === null ? null : Number(v));
|
|
11
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
12
|
+
const fromPage = (r) => ({
|
|
13
|
+
id: r.page_id, locale: r.locale, parentId: r.parent_id, sort: num(r.sort), path: r.path, title: r.title, hasPage: r.has_page,
|
|
14
|
+
linkFields: r.link_fields, document: r.document, tags: r.tags, model: r.model, stale: r.stale, publishedAt: Date.parse(r.published_at),
|
|
15
|
+
});
|
|
16
|
+
const toPage = (r) => ({
|
|
17
|
+
page_id: r.id, locale: r.locale, parent_id: r.parentId, sort: r.sort, path: r.path, title: r.title, has_page: r.hasPage,
|
|
18
|
+
link_fields: r.linkFields, document: r.document, tags: r.tags, model: r.model, stale: r.stale, published_at: iso(r.publishedAt),
|
|
19
|
+
});
|
|
20
|
+
const fromEntry = (r) => ({
|
|
21
|
+
id: r.entry_id, locale: r.locale, collection: r.collection, parentId: r.parent_id, sort: num(r.sort), slug: r.slug, path: r.path,
|
|
22
|
+
createdAt: Date.parse(r.created_at), updatedAt: Date.parse(r.updated_at), fields: r.fields, document: r.document, tags: r.tags,
|
|
23
|
+
model: r.model, stale: r.stale, publishedAt: Date.parse(r.published_at),
|
|
24
|
+
});
|
|
25
|
+
const toEntry = (r) => ({
|
|
26
|
+
entry_id: r.id, locale: r.locale, collection: r.collection, parent_id: r.parentId, sort: r.sort, slug: r.slug, path: r.path,
|
|
27
|
+
created_at: iso(r.createdAt), updated_at: iso(r.updatedAt), fields: r.fields, document: r.document, tags: r.tags,
|
|
28
|
+
model: r.model, stale: r.stale, published_at: iso(r.publishedAt),
|
|
29
|
+
});
|
|
30
|
+
export class SupabasePublishedStore {
|
|
31
|
+
/** `collections` encode a `where` on an array field as containment, as SupabaseEntryStore.query does. */
|
|
32
|
+
constructor(db, collections = []) {
|
|
33
|
+
this.db = db;
|
|
34
|
+
this.schemas = new Map(collections.map((c) => [c.name, c]));
|
|
35
|
+
}
|
|
36
|
+
fail(message) {
|
|
37
|
+
throw new Error(`smoodly: published store: ${message}`);
|
|
38
|
+
}
|
|
39
|
+
async putPages(rows) {
|
|
40
|
+
for (let i = 0; i < rows.length; i += CHUNK) {
|
|
41
|
+
const chunk = rows.slice(i, i + CHUNK);
|
|
42
|
+
await this.vacate("published_pages", "page_id", chunk);
|
|
43
|
+
const res = await this.db.from("published_pages").upsert(chunk.map(toPage), { onConflict: "page_id,locale" });
|
|
44
|
+
if (res.error)
|
|
45
|
+
this.fail(res.error.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async putEntries(rows) {
|
|
49
|
+
for (let i = 0; i < rows.length; i += CHUNK) {
|
|
50
|
+
const chunk = rows.slice(i, i + CHUNK);
|
|
51
|
+
await this.vacate("published_entries", "entry_id", chunk);
|
|
52
|
+
const res = await this.db.from("published_entries").upsert(chunk.map(toEntry), { onConflict: "entry_id,locale" });
|
|
53
|
+
if (res.error)
|
|
54
|
+
this.fail(res.error.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The space this client writes in (the service role's is the default): a path delete must not reach past it,
|
|
58
|
+
* since the service role passes every policy. Cached for the life of the client; a failed lookup is not. */
|
|
59
|
+
space() {
|
|
60
|
+
if (!this.spaceId) {
|
|
61
|
+
this.spaceId = readCurrentSpace(this.db).catch((e) => {
|
|
62
|
+
this.spaceId = undefined;
|
|
63
|
+
throw e;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return this.spaceId;
|
|
67
|
+
}
|
|
68
|
+
/** The unique (space, locale, path) index holds one row per address: a row of another id at an incoming address
|
|
69
|
+
* (a stale row a capped rename did not reach) is deleted before the upsert. Not atomic with it: rows are derived,
|
|
70
|
+
* and a read heals what a failure between the two leaves. One delete per locale in the chunk. */
|
|
71
|
+
async vacate(table, column, rows) {
|
|
72
|
+
const byLocale = new Map();
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
// An id the table cannot hold (no UUID) vacates nothing: an empty `not in ()` is no filter PostgREST accepts.
|
|
75
|
+
if (r.path === null || !UUID_RE.test(r.id))
|
|
76
|
+
continue;
|
|
77
|
+
if (!byLocale.has(r.locale))
|
|
78
|
+
byLocale.set(r.locale, { paths: [], ids: [] });
|
|
79
|
+
byLocale.get(r.locale).paths.push(r.path);
|
|
80
|
+
byLocale.get(r.locale).ids.push(r.id);
|
|
81
|
+
}
|
|
82
|
+
if (byLocale.size === 0)
|
|
83
|
+
return;
|
|
84
|
+
const space = await this.space();
|
|
85
|
+
for (const [locale, { paths, ids }] of byLocale) {
|
|
86
|
+
const res = await this.db.from(table).delete()
|
|
87
|
+
.eq("space_id", space).eq("locale", locale).in("path", paths)
|
|
88
|
+
.not(column, "in", `(${ids.join(",")})`);
|
|
89
|
+
if (res.error)
|
|
90
|
+
this.fail(res.error.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** One request per kind and locale filter: the keys without a locale go in one in(). */
|
|
94
|
+
async eachGroup(keys, act) {
|
|
95
|
+
const groups = new Map();
|
|
96
|
+
for (const k of keys) {
|
|
97
|
+
if (!UUID_RE.test(k.id))
|
|
98
|
+
continue;
|
|
99
|
+
const g = `${k.kind}:${k.locale ?? ""}`;
|
|
100
|
+
if (!groups.has(g))
|
|
101
|
+
groups.set(g, { kind: k.kind, locale: k.locale, ids: [] });
|
|
102
|
+
groups.get(g).ids.push(k.id);
|
|
103
|
+
}
|
|
104
|
+
for (const { kind, locale, ids } of groups.values()) {
|
|
105
|
+
const [table, column] = kind === "page" ? ["published_pages", "page_id"] : ["published_entries", "entry_id"];
|
|
106
|
+
for (let i = 0; i < ids.length; i += CHUNK)
|
|
107
|
+
await act(table, column, ids.slice(i, i + CHUNK), locale);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async remove(keys) {
|
|
111
|
+
await this.eachGroup(keys, async (table, column, ids, locale) => {
|
|
112
|
+
let q = this.db.from(table).delete().in(column, ids);
|
|
113
|
+
if (locale !== undefined)
|
|
114
|
+
q = q.eq("locale", locale);
|
|
115
|
+
const res = await q;
|
|
116
|
+
if (res.error)
|
|
117
|
+
this.fail(res.error.message);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** The space's recorded stamp (DESIGN §3): one row per space in published_models; the key's space, or the
|
|
121
|
+
* service role's default one. */
|
|
122
|
+
async recordedModel() {
|
|
123
|
+
const res = await this.db.from("published_models").select("model").eq("space_id", await this.space()).maybeSingle();
|
|
124
|
+
if (res.error)
|
|
125
|
+
this.fail(res.error.message);
|
|
126
|
+
return res.data?.model ?? null;
|
|
127
|
+
}
|
|
128
|
+
async recordModel(model) {
|
|
129
|
+
const res = await this.db.from("published_models")
|
|
130
|
+
.upsert({ space_id: await this.space(), model, recorded_at: new Date().toISOString() }, { onConflict: "space_id" });
|
|
131
|
+
if (res.error)
|
|
132
|
+
this.fail(res.error.message);
|
|
133
|
+
}
|
|
134
|
+
/** Four head counts (no rows travel): each table's rows, and its rows at another stamp. The policies scope a
|
|
135
|
+
* write key to its space; the service role counts the default space's rows by the explicit filter. */
|
|
136
|
+
async countBehind(model) {
|
|
137
|
+
const space = await this.space();
|
|
138
|
+
const count = async (table, behind) => {
|
|
139
|
+
let q = this.db.from(table).select("*", { count: "exact", head: true }).eq("space_id", space);
|
|
140
|
+
if (behind)
|
|
141
|
+
q = q.neq("model", model);
|
|
142
|
+
const res = await q;
|
|
143
|
+
if (res.error)
|
|
144
|
+
this.fail(res.error.message);
|
|
145
|
+
return res.count ?? 0;
|
|
146
|
+
};
|
|
147
|
+
const [pages, entries, pagesBehind, entriesBehind] = await Promise.all([
|
|
148
|
+
count("published_pages", false), count("published_entries", false), count("published_pages", true), count("published_entries", true),
|
|
149
|
+
]);
|
|
150
|
+
return { behind: pagesBehind + entriesBehind, total: pages + entries };
|
|
151
|
+
}
|
|
152
|
+
async markStale(keys) {
|
|
153
|
+
await this.eachGroup(keys, async (table, column, ids, locale) => {
|
|
154
|
+
let q = this.db.from(table).update({ stale: true }).in(column, ids);
|
|
155
|
+
if (locale !== undefined)
|
|
156
|
+
q = q.eq("locale", locale);
|
|
157
|
+
const res = await q;
|
|
158
|
+
if (res.error)
|
|
159
|
+
this.fail(res.error.message);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** A route row is for serving, not for the tree: a page route carries the
|
|
163
|
+
* document, model, stale flag and tags; its tree columns are empty, and an
|
|
164
|
+
* entry route's `fields` and times likewise. */
|
|
165
|
+
async byAddress(locale, path, globals) {
|
|
166
|
+
if (globals) {
|
|
167
|
+
// One call (schema v9): the route row, then the locale's rows of the named global items.
|
|
168
|
+
const res = await this.db.rpc("smoodly_lookup_address", { p_locale: locale, p_path: path, p_globals: [...globals] });
|
|
169
|
+
if (res.error)
|
|
170
|
+
this.fail(res.error.message);
|
|
171
|
+
const rows = res.data;
|
|
172
|
+
const first = rows.find((r) => r.kind !== "global");
|
|
173
|
+
if (!first)
|
|
174
|
+
return null;
|
|
175
|
+
const route = this.route({ ...first, kind: first.kind, locale, path });
|
|
176
|
+
route.globals = rows.filter((r) => r.kind === "global").map((r) => ({
|
|
177
|
+
id: r.id, locale, collection: r.collection, parentId: null, sort: null, slug: null, path: null, createdAt: 0, updatedAt: 0,
|
|
178
|
+
fields: {}, document: r.document, tags: r.tags, model: r.model, stale: r.stale, publishedAt: 0,
|
|
179
|
+
}));
|
|
180
|
+
return route;
|
|
181
|
+
}
|
|
182
|
+
const res = await this.db.from("published_routes").select("kind, id, locale, path, document, model, stale, tags, collection")
|
|
183
|
+
.eq("locale", locale).eq("path", path).limit(1).maybeSingle();
|
|
184
|
+
if (res.error)
|
|
185
|
+
this.fail(res.error.message);
|
|
186
|
+
const r = res.data;
|
|
187
|
+
return r ? this.route(r) : null;
|
|
188
|
+
}
|
|
189
|
+
route(r) {
|
|
190
|
+
const served = { id: r.id, locale: r.locale, parentId: null, sort: null, path: r.path, tags: r.tags, model: r.model, stale: r.stale, publishedAt: 0 };
|
|
191
|
+
return r.kind === "page"
|
|
192
|
+
? { kind: "page", row: { ...served, title: "", hasPage: true, linkFields: {}, document: r.document } }
|
|
193
|
+
: { kind: "entry", row: { ...served, collection: r.collection, slug: null, createdAt: 0, updatedAt: 0, fields: {}, document: r.document } };
|
|
194
|
+
}
|
|
195
|
+
async page(id, locale) {
|
|
196
|
+
if (!UUID_RE.test(id))
|
|
197
|
+
return null;
|
|
198
|
+
const res = await this.db.from("published_pages").select("*").eq("page_id", id).eq("locale", locale).maybeSingle();
|
|
199
|
+
if (res.error)
|
|
200
|
+
this.fail(res.error.message);
|
|
201
|
+
return res.data ? fromPage(res.data) : null;
|
|
202
|
+
}
|
|
203
|
+
async entry(id, locale) {
|
|
204
|
+
if (!UUID_RE.test(id))
|
|
205
|
+
return null;
|
|
206
|
+
const res = await this.db.from("published_entries").select("*").eq("entry_id", id).eq("locale", locale).maybeSingle();
|
|
207
|
+
if (res.error)
|
|
208
|
+
this.fail(res.error.message);
|
|
209
|
+
return res.data ? fromEntry(res.data) : null;
|
|
210
|
+
}
|
|
211
|
+
/** A list row carries the document, model and stale flag; the raw `fields`
|
|
212
|
+
* and the tree columns stay in the table (a list never needs them). */
|
|
213
|
+
async entries(collection, q) {
|
|
214
|
+
const schema = this.schemas.get(collection);
|
|
215
|
+
const where = {};
|
|
216
|
+
for (const [key, want] of Object.entries(q.where ?? {})) {
|
|
217
|
+
const inArray = ARRAY_FIELD_TYPES.has(schema?.fields[key]?.type ?? "");
|
|
218
|
+
where[key] = Array.isArray(want) ? { in: want.map(String) } : { contains: inArray ? [want] : want };
|
|
219
|
+
}
|
|
220
|
+
const res = await this.db.rpc("smoodly_list_published", {
|
|
221
|
+
p_collection: collection,
|
|
222
|
+
p_locale: q.locale,
|
|
223
|
+
p_where: where,
|
|
224
|
+
p_order: q.order === undefined ? null : q.order === "manual" ? { by: "manual" } : q.order,
|
|
225
|
+
p_limit: q.limit ?? null,
|
|
226
|
+
p_parent: q.parent ?? null,
|
|
227
|
+
p_has_parent: q.parent !== undefined,
|
|
228
|
+
p_ids: q.ids ? q.ids.filter((id) => UUID_RE.test(id)) : null,
|
|
229
|
+
});
|
|
230
|
+
if (res.error)
|
|
231
|
+
this.fail(res.error.message);
|
|
232
|
+
return res.data.map((r) => ({
|
|
233
|
+
id: r.id, locale: q.locale, collection, parentId: null, sort: null, slug: null, path: null, createdAt: 0, updatedAt: 0,
|
|
234
|
+
fields: {}, document: r.document, tags: [], model: r.model, stale: r.stale, publishedAt: 0,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
async tree(locale) {
|
|
238
|
+
const res = await this.db.from("published_pages").select(TREE_COLUMNS).eq("locale", locale)
|
|
239
|
+
.order("sort", { ascending: true, nullsFirst: false }).order("published_at").order("page_id");
|
|
240
|
+
if (res.error)
|
|
241
|
+
this.fail(res.error.message);
|
|
242
|
+
return res.data.map((r) => ({
|
|
243
|
+
id: r.page_id, parentId: r.parent_id, sort: num(r.sort), path: r.path, title: r.title, hasPage: r.has_page, linkFields: r.link_fields,
|
|
244
|
+
model: r.model, stale: r.stale,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
}
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import { type AddLocaleOptions, type CreatePageInput, type PageRecord, type PageStore, type PageStoreOptions, type PageVersion, type PageScan } from "./store.ts";
|
|
3
|
-
import type { PageTree } from "./
|
|
3
|
+
import type { PageTree } from "./page-context.ts";
|
|
4
4
|
import type { PathRow } from "./path-index.ts";
|
|
5
5
|
import { type DraftToken } from "./draft-token.ts";
|
|
6
6
|
import { type PathTarget } from "./paths.ts";
|
package/dist/toolset.d.ts
CHANGED
|
@@ -15,10 +15,8 @@ export declare function toolset(input: {
|
|
|
15
15
|
nodes?: readonly ToolsetNode[];
|
|
16
16
|
/** Registered Elements embeddable in this toolset, by value (spec 2026-09-08 block nodes §1). */
|
|
17
17
|
elements?: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
contexts?: readonly string[];
|
|
21
|
-
};
|
|
18
|
+
name: string;
|
|
19
|
+
contexts: readonly string[];
|
|
22
20
|
}[];
|
|
23
21
|
}): Toolset;
|
|
24
22
|
/** The resolved toolset the widget reads: which marks and nodes an editor
|
package/dist/toolset.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { schemaOnly } from "./registration.js";
|
|
1
2
|
// Toolsets are plain shared values, not registrations (DESIGN.md §3): a
|
|
2
3
|
// richtext field imports one by value and it travels inside the field
|
|
3
4
|
// descriptor to the admin. The vocabulary is typed and checked at
|
|
@@ -30,12 +31,13 @@ export function toolset(input) {
|
|
|
30
31
|
out.nodes = [...input.nodes];
|
|
31
32
|
}
|
|
32
33
|
if (input.elements) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const schemas = input.elements.map((e) => schemaOnly(e));
|
|
35
|
+
for (const e of schemas) {
|
|
36
|
+
if (!(e.contexts ?? []).includes("richtext")) {
|
|
37
|
+
throw new Error(`smoodly: element "${e.name}" is not registered for the richtext context — add "richtext" to its contexts.`);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
|
-
out.elements =
|
|
40
|
+
out.elements = schemas.map((e) => e.name);
|
|
39
41
|
}
|
|
40
42
|
return out;
|
|
41
43
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.47";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.47";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CollectionSchema } from "./collections.ts";
|
|
2
|
+
import type { BuilderMap, Materialize, Values } from "./schema.ts";
|
|
3
|
+
import type { StyleProps } from "./styles.ts";
|
|
4
|
+
import type { EntryItem } from "./read-shapes.ts";
|
|
5
|
+
import type { PageContext, ZoneContent } from "./page-context.ts";
|
|
6
|
+
/** What the view receives. A page view gets its zones and where it sits
|
|
7
|
+
* in the tree; a section/element view gets field values flat, settings
|
|
8
|
+
* complete (defaults filled) and Smoodly's styles steps as stored (Default
|
|
9
|
+
* stores nothing — spec 2026-09-14 §2). The page branch keys on `kind` — an
|
|
10
|
+
* optional-only check would match section schemas too, since a type
|
|
11
|
+
* lacking `__zones` still extends `{ __zones?: X }`. */
|
|
12
|
+
export type Props<S> = S extends {
|
|
13
|
+
kind: "page";
|
|
14
|
+
__zones?: infer Z extends Record<string, unknown>;
|
|
15
|
+
__fields?: infer F extends BuilderMap;
|
|
16
|
+
} ? {
|
|
17
|
+
zones: Record<keyof Z & string, ZoneContent>;
|
|
18
|
+
page: PageContext<Values<F>>;
|
|
19
|
+
} : S extends {
|
|
20
|
+
__f?: infer F extends BuilderMap;
|
|
21
|
+
__s?: infer St extends BuilderMap;
|
|
22
|
+
} ? Values<F> & {
|
|
23
|
+
settings: Values<St>;
|
|
24
|
+
styles: StyleProps;
|
|
25
|
+
} : never;
|
|
26
|
+
export type EntryPageSchema = {
|
|
27
|
+
kind: "entryPage";
|
|
28
|
+
name: string;
|
|
29
|
+
collection: string;
|
|
30
|
+
};
|
|
31
|
+
/** What an entry page renders: the entry as a read returns it — its path, locale and
|
|
32
|
+
* locale alternates ride on it (spec 2026-09-29 content API contract §3). */
|
|
33
|
+
export type EntryPageProps<F = Record<string, unknown>> = {
|
|
34
|
+
entry: EntryItem<F>;
|
|
35
|
+
};
|
|
36
|
+
/** A collection's fields as a read returns them (refs resolved, media as images): an Astro entry page's
|
|
37
|
+
* `entry: EntryItem<EntryFields<typeof articles>>`, what React's `entryPage(articles, View)` infers. */
|
|
38
|
+
export type EntryFields<C extends CollectionSchema<any>> = {
|
|
39
|
+
[K in keyof NonNullable<C["__entry"]>]: Materialize<NonNullable<C["__entry"]>[K]>;
|
|
40
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
+
/** A paired section or element: a component carrying its schema. */
|
|
4
|
+
type ViewOf = ComponentType<any> & {
|
|
5
|
+
schema: {
|
|
6
|
+
name: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/** A paired page type or entry page. */
|
|
10
|
+
type PageViewOf = ((props: any) => unknown) & {
|
|
11
|
+
schema: {
|
|
12
|
+
kind: string;
|
|
13
|
+
name: string;
|
|
14
|
+
collection?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type ViewsInput<S extends ViewOf = ViewOf, E extends ViewOf = ViewOf, P extends PageViewOf = PageViewOf> = {
|
|
18
|
+
sections: S[];
|
|
19
|
+
elements?: E[];
|
|
20
|
+
/** Page types and entry pages alike: everything that renders a URL. */
|
|
21
|
+
pages: P[];
|
|
22
|
+
/** The project's bound RichText, for the custom section's columns (spec 2026-09-14 §6). */
|
|
23
|
+
RichText?: ComponentType<{
|
|
24
|
+
doc: unknown;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export type Views<S extends ViewOf = ViewOf, E extends ViewOf = ViewOf, P extends PageViewOf = PageViewOf> = {
|
|
28
|
+
sections: S[];
|
|
29
|
+
elements: E[];
|
|
30
|
+
pages: P[];
|
|
31
|
+
RichText?: ComponentType<{
|
|
32
|
+
doc: unknown;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
export declare function defineViews<S extends ViewOf, E extends ViewOf, P extends PageViewOf>(config: ResolvedConfig, views: ViewsInput<S, E, P>): Views<S, E, P>;
|
|
36
|
+
export {};
|