smoodly 0.0.50 → 0.0.52
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 +14 -13
- package/dist/admin/auth-config.d.ts +6 -5
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/browser/fetch-op.js +3 -2
- package/dist/admin/browser/main.js +6 -2
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +14 -11
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +5 -8
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +36 -22
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +43 -0
- package/dist/admin/session-flow.js +94 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +5 -4
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +20 -12
- package/dist/admin/shell/session.js +113 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +5 -8
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +4 -302
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +54 -25
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +2 -1
- package/dist/astro/runtime.js +4 -3
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +2 -2
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +22 -23
- package/dist/content/content-api.js +46 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/deploy-store.d.ts +19 -0
- package/dist/deploy-store.js +1 -0
- package/dist/entry-store.d.ts +3 -89
- package/dist/entry-store.js +2 -550
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +12 -22
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +57 -3
- package/dist/media.js +125 -15
- package/dist/member-store.d.ts +3 -22
- package/dist/member-store.js +1 -53
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +28 -15
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +9 -8
- package/dist/reads.js +7 -3
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +5 -2
- package/dist/space-transfer.js +17 -5
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1400
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
package/dist/sql.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { CollectionSchema } from "./collections.ts";
|
|
2
|
-
export declare function collectionSQL(collection: CollectionSchema<any>): string;
|
|
3
|
-
/** Foreign keys the planner needs an index for (spec 2026-09-29 pre-install
|
|
4
|
-
* fixes §3): a restrict or set-null check scans the referencing table
|
|
5
|
-
* without one, and the versions' read policies probe published_version_id
|
|
6
|
-
* on every public read. The composite indexes that exist lead with space_id. */
|
|
7
|
-
export declare function foreignKeyIndexesSQL(): string;
|
|
8
|
-
/** The shown snapshot of one collection's entries in a locale, filtered,
|
|
9
|
-
* ordered and limited in one query (spec 2026-09-29 content API contract
|
|
10
|
-
* §5). Invoker rights, so RLS decides what a key sees. Defined after the
|
|
11
|
-
* space layer, whose `smoodly_key_kind` it calls. */
|
|
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;
|
|
15
|
-
export declare function coreTablesSQL(): string;
|
package/dist/sql.js
DELETED
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
// Schema → SQL generation (Postgres/Supabase) for the stable schema
|
|
2
|
-
// (DESIGN.md §3): all collection content lives in the one `entries`
|
|
3
|
-
// table, applied once and never migrated. Per collection we emit only
|
|
4
|
-
// a disposable guardrail — a published-only read view, created with
|
|
5
|
-
// security_invoker so it runs under the caller's own RLS policies
|
|
6
|
-
// rather than the view owner's privileges (SQL-queryable eject story;
|
|
7
|
-
// sibling slug uniqueness is the `paths` primary key plus the stores'
|
|
8
|
-
// per-locale pre-check, not an index). A view joins the locale rows, one row per published
|
|
9
|
-
// locale (spec 2026-09-06 §7); a versioned collection reads its
|
|
10
|
-
// fields from the published snapshot.
|
|
11
|
-
import { SEO_KEY } from "./seo.js";
|
|
12
|
-
import { currentSpaceSQL, grantsSQL, SPACE_COL, spaceLayerSQL, DEFAULT_SPACE } from "./sql-space.js";
|
|
13
|
-
const snake = (s) => s.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
|
|
14
|
-
const q = (s) => `"${s}"`;
|
|
15
|
-
// Casts are fine in views.
|
|
16
|
-
function fieldExpr(key, d, source) {
|
|
17
|
-
switch (d.type) {
|
|
18
|
-
case "text":
|
|
19
|
-
case "link":
|
|
20
|
-
case "slug":
|
|
21
|
-
case "select":
|
|
22
|
-
case "ref": // stored as the target entry's id string
|
|
23
|
-
return `${source}->>'${key}'`;
|
|
24
|
-
case "number":
|
|
25
|
-
return `(${source}->>'${key}')::numeric`;
|
|
26
|
-
case "date":
|
|
27
|
-
return `(${source}->>'${key}')::timestamptz`;
|
|
28
|
-
default:
|
|
29
|
-
return `${source}->'${key}'`; // image, video, file, object, list, richtext, refList
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export function collectionSQL(collection) {
|
|
33
|
-
const name = collection.name;
|
|
34
|
-
// Every collection is versioned (spec 2026-09-14 §1): a field's value is the published
|
|
35
|
-
// snapshot's, falling back to the live row (the node's shared fields under the locale
|
|
36
|
-
// row's localized ones) for a published row from before versioning that nobody has
|
|
37
|
-
// written since — the store gives it a snapshot on its first write.
|
|
38
|
-
const source = `coalesce(v."fields", e."fields" || l."fields")`;
|
|
39
|
-
const columns = ['e."id"', 'l."locale"', 'l."slug"', 'e."sort"', 'e."created_at"', 'l."updated_at"'];
|
|
40
|
-
for (const [key, d] of Object.entries(collection.fields)) {
|
|
41
|
-
if (key === "slug")
|
|
42
|
-
continue; // the segment is the row's column, never a field
|
|
43
|
-
if (key === SEO_KEY)
|
|
44
|
-
continue; // the panel's values are Smoodly-internal, not a view column
|
|
45
|
-
columns.push(`${fieldExpr(key, d, source)} as ${q(snake(key))}`);
|
|
46
|
-
}
|
|
47
|
-
const statements = [
|
|
48
|
-
[
|
|
49
|
-
`create or replace view ${q(name)} with (security_invoker = true) as`,
|
|
50
|
-
`select`,
|
|
51
|
-
columns.map((c) => " " + c).join(",\n"),
|
|
52
|
-
`from "entries" e`,
|
|
53
|
-
`join "entry_locales" l on l."entry_id" = e."id"`,
|
|
54
|
-
`left join "entry_versions" v on v."id" = l."published_version_id"`,
|
|
55
|
-
`where e."collection" = '${name}' and l."status" = 'published';`,
|
|
56
|
-
].join("\n"),
|
|
57
|
-
];
|
|
58
|
-
// Ordering reads the shown snapshot (smoodly_list_entries), so no index on the live row serves it (spec 2026-09-29 content API contract §5).
|
|
59
|
-
return statements.join("\n\n");
|
|
60
|
-
}
|
|
61
|
-
/** Foreign keys the planner needs an index for (spec 2026-09-29 pre-install
|
|
62
|
-
* fixes §3): a restrict or set-null check scans the referencing table
|
|
63
|
-
* without one, and the versions' read policies probe published_version_id
|
|
64
|
-
* on every public read. The composite indexes that exist lead with space_id. */
|
|
65
|
-
export function foreignKeyIndexesSQL() {
|
|
66
|
-
return [
|
|
67
|
-
`create index if not exists "page_locales_draft_version_idx" on "page_locales" ("draft_version_id");`,
|
|
68
|
-
`create index if not exists "entry_locales_draft_version_idx" on "entry_locales" ("draft_version_id");`,
|
|
69
|
-
`create index if not exists "page_locales_published_version_idx" on "page_locales" ("published_version_id");`,
|
|
70
|
-
`create index if not exists "entry_locales_published_version_idx" on "entry_locales" ("published_version_id");`,
|
|
71
|
-
`create index if not exists "pages_parent_id_idx" on "pages" ("parent_id");`,
|
|
72
|
-
`create index if not exists "entries_parent_id_idx" on "entries" ("parent_id");`,
|
|
73
|
-
`create index if not exists "assets_poster_idx" on "assets" ("poster_id");`,
|
|
74
|
-
].join("\n");
|
|
75
|
-
}
|
|
76
|
-
/** The shown snapshot of one collection's entries in a locale, filtered,
|
|
77
|
-
* ordered and limited in one query (spec 2026-09-29 content API contract
|
|
78
|
-
* §5). Invoker rights, so RLS decides what a key sees. Defined after the
|
|
79
|
-
* space layer, whose `smoodly_key_kind` it calls. */
|
|
80
|
-
export function listEntriesSQL() {
|
|
81
|
-
return `
|
|
82
|
-
-- The shown snapshot of one collection's entries in a locale, filtered,
|
|
83
|
-
-- ordered and limited in one query (spec 2026-09-29 content API contract
|
|
84
|
-
-- §5): the published snapshot, or the live merge in preview. Invoker
|
|
85
|
-
-- rights, so RLS decides what a key sees; a preview read needs the
|
|
86
|
-
-- service role or a write key, anything else reads published.
|
|
87
|
-
create or replace function "smoodly_list_entries"(
|
|
88
|
-
p_collection text, p_locale text, p_draft boolean, p_where jsonb, p_order jsonb,
|
|
89
|
-
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
90
|
-
) returns table ("id" uuid, "fields" jsonb) language sql stable set search_path = public as $$
|
|
91
|
-
with mode as (
|
|
92
|
-
select p_draft and (current_user = 'service_role' or "smoodly_key_kind"() = 'write') as draft
|
|
93
|
-
), shown as (
|
|
94
|
-
select e."id", e."sort", e."created_at", l."updated_at",
|
|
95
|
-
case when (select draft from mode) then e."fields" || l."fields" || jsonb_build_object('slug', l."slug")
|
|
96
|
-
else coalesce(v."fields", e."fields" || l."fields" || jsonb_build_object('slug', l."slug")) end as "fields"
|
|
97
|
-
from "entries" e
|
|
98
|
-
join "entry_locales" l on l."entry_id" = e."id" and l."locale" = p_locale
|
|
99
|
-
left join "entry_versions" v on v."id" = l."published_version_id"
|
|
100
|
-
where e."collection" = p_collection
|
|
101
|
-
and ((select draft from mode) or l."status" = 'published')
|
|
102
|
-
and (not p_has_parent or e."parent_id" is not distinct from p_parent)
|
|
103
|
-
and (p_ids is null or e."id" = any(p_ids))
|
|
104
|
-
)
|
|
105
|
-
select s."id", s."fields" from shown s
|
|
106
|
-
where not exists (
|
|
107
|
-
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
108
|
-
where not (case when w.value ? 'in'
|
|
109
|
-
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
110
|
-
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
111
|
-
order by
|
|
112
|
-
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
113
|
-
case when p_order ->> 'direction' = 'asc' then
|
|
114
|
-
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
115
|
-
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
116
|
-
case when p_order ->> 'direction' = 'desc' then
|
|
117
|
-
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
118
|
-
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
119
|
-
s."created_at" asc, s."id" asc
|
|
120
|
-
limit p_limit;
|
|
121
|
-
$$;`.trim();
|
|
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
|
-
-- The last Deploy press of a space (spec 2026-10-09 static mode §7, schema v11): who pressed and when;
|
|
185
|
-
-- the admin compares it with the build marker the live site serves.
|
|
186
|
-
create table if not exists "space_deploys" (
|
|
187
|
-
"space_id" uuid primary key default ${DEFAULT_SPACE},
|
|
188
|
-
"requested_at" timestamptz,
|
|
189
|
-
"requested_by" text,
|
|
190
|
-
"removed_at" timestamptz
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
alter table "published_pages" enable row level security;
|
|
194
|
-
alter table "published_entries" enable row level security;
|
|
195
|
-
alter table "published_models" enable row level security;
|
|
196
|
-
alter table "space_deploys" enable row level security;
|
|
197
|
-
|
|
198
|
-
-- smoodly_list_entries without the draft branch, over the delivery table (spec 2026-10-07 §3):
|
|
199
|
-
-- \`where\` and \`order\` read the raw published snapshot, the row answers with its document.
|
|
200
|
-
-- Invoker rights, so RLS decides what a key sees.
|
|
201
|
-
create or replace function "smoodly_list_published"(
|
|
202
|
-
p_collection text, p_locale text, p_where jsonb, p_order jsonb,
|
|
203
|
-
p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
|
|
204
|
-
) returns table ("id" uuid, "document" jsonb, "model" text, "stale" boolean) language sql stable set search_path = public as $$
|
|
205
|
-
with shown as (
|
|
206
|
-
select p."entry_id" as "id", p."sort", p."created_at", p."updated_at", p."fields", p."document", p."model", p."stale"
|
|
207
|
-
from "published_entries" p
|
|
208
|
-
where p."collection" = p_collection and p."locale" = p_locale
|
|
209
|
-
and (not p_has_parent or p."parent_id" is not distinct from p_parent)
|
|
210
|
-
and (p_ids is null or p."entry_id" = any(p_ids))
|
|
211
|
-
)
|
|
212
|
-
select s."id", s."document", s."model", s."stale" from shown s
|
|
213
|
-
where not exists (
|
|
214
|
-
select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
|
|
215
|
-
where not (case when w.value ? 'in'
|
|
216
|
-
then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
|
|
217
|
-
else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
|
|
218
|
-
order by
|
|
219
|
-
case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
|
|
220
|
-
case when p_order ->> 'direction' = 'asc' then
|
|
221
|
-
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
222
|
-
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
|
|
223
|
-
case when p_order ->> 'direction' = 'desc' then
|
|
224
|
-
case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
|
|
225
|
-
when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
|
|
226
|
-
s."created_at" asc, s."id" asc
|
|
227
|
-
limit p_limit;
|
|
228
|
-
$$;
|
|
229
|
-
|
|
230
|
-
-- One lookup for any address (spec 2026-10-07 §3): a page or an entry, under the caller's own policies.
|
|
231
|
-
create or replace view "published_routes" with (security_invoker = true) as
|
|
232
|
-
select "space_id", "locale", "path", 'page' as "kind", "page_id" as "id", "document", "model", "stale", "tags", null::text as "collection"
|
|
233
|
-
from "published_pages" where "path" is not null and "has_page"
|
|
234
|
-
union all
|
|
235
|
-
select "space_id", "locale", "path", 'entry' as "kind", "entry_id" as "id", "document", "model", "stale", "tags", "collection"
|
|
236
|
-
from "published_entries" where "path" is not null;
|
|
237
|
-
|
|
238
|
-
-- The address and the locale's global items in one call (schema v9; register 2026-10-07: a miss was two
|
|
239
|
-
-- rounds, the page then the layout's globals). The route row first, kind 'page' or 'entry'; then one row per
|
|
240
|
-
-- named global, kind 'global', the collection its name. A miss answers nothing: the globals ride with a page
|
|
241
|
-
-- only. Invoker rights, so RLS decides what a key sees.
|
|
242
|
-
create or replace function "smoodly_lookup_address"(p_locale text, p_path text, p_globals text[])
|
|
243
|
-
returns table ("kind" text, "id" uuid, "collection" text, "document" jsonb, "model" text, "stale" boolean, "tags" text[])
|
|
244
|
-
language sql stable set search_path = public as $$
|
|
245
|
-
with route as (
|
|
246
|
-
select r."kind", r."id", r."collection", r."document", r."model", r."stale", r."tags"
|
|
247
|
-
from "published_routes" r where r."locale" = p_locale and r."path" = p_path limit 1
|
|
248
|
-
)
|
|
249
|
-
select * from route
|
|
250
|
-
union all
|
|
251
|
-
select 'global' as "kind", e."entry_id", e."collection", e."document", e."model", e."stale", e."tags"
|
|
252
|
-
from "published_entries" e
|
|
253
|
-
where e."locale" = p_locale and e."collection" = any(coalesce(p_globals, '{}'::text[]))
|
|
254
|
-
and exists (select 1 from route);
|
|
255
|
-
$$;
|
|
256
|
-
`.trim();
|
|
257
|
-
}
|
|
258
|
-
export function coreTablesSQL() {
|
|
259
|
-
return [
|
|
260
|
-
// The trigger/policy blocks below drop-before-create so the migration is
|
|
261
|
-
// re-appliable; on a fresh database every drop would otherwise NOTICE
|
|
262
|
-
// "does not exist, skipping" in the installer's output.
|
|
263
|
-
`set client_min_messages = warning;`,
|
|
264
|
-
currentSpaceSQL(),
|
|
265
|
-
`
|
|
266
|
-
create table if not exists "pages" (
|
|
267
|
-
"id" uuid primary key default gen_random_uuid(),
|
|
268
|
-
${SPACE_COL},
|
|
269
|
-
"parent_id" uuid references "pages" ("id") on delete restrict,
|
|
270
|
-
"template" text,
|
|
271
|
-
"sort" numeric,
|
|
272
|
-
"fixed" text,
|
|
273
|
-
"created_at" timestamptz not null default now(),
|
|
274
|
-
"updated_at" timestamptz not null default now()
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
-- The tree is the URL (spec 2026-09-05 §1): a node stores its parent and
|
|
278
|
-
-- its position; template null = folder. Everything a locale owns — the
|
|
279
|
-
-- node's own segment, title, publish state and version pointers — lives
|
|
280
|
-
-- in page_locales, one row per locale the page exists in (spec 2026-09-06
|
|
281
|
-
-- §2). There is no sibling-slug index: URL uniqueness per locale is the
|
|
282
|
-
-- primary key of "paths", which the stores write inside the same operation.
|
|
283
|
-
create index if not exists "pages_parent_idx"
|
|
284
|
-
on "pages" ("space_id", "parent_id", "sort");
|
|
285
|
-
|
|
286
|
-
-- The registration that owns a code-owned record (spec 2026-09-29 nested
|
|
287
|
-
-- paths §3): "collection:<name>" or "page:<name>"; null = an editor's
|
|
288
|
-
-- page. Its path is its address from the config, not its slug chain.
|
|
289
|
-
create unique index if not exists "pages_fixed_idx"
|
|
290
|
-
on "pages" ("space_id", "fixed") where "fixed" is not null;
|
|
291
|
-
|
|
292
|
-
create table if not exists "page_locales" (
|
|
293
|
-
${SPACE_COL},
|
|
294
|
-
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
295
|
-
"locale" text not null,
|
|
296
|
-
"slug" text not null,
|
|
297
|
-
"title" text not null,
|
|
298
|
-
"status" text not null default 'draft',
|
|
299
|
-
"draft_version_id" uuid,
|
|
300
|
-
"published_version_id" uuid,
|
|
301
|
-
"updated_at" timestamptz not null default now(),
|
|
302
|
-
"updated_by" text,
|
|
303
|
-
primary key ("page_id", "locale")
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
-- Sibling reorder for pages: same shape as smoodly_reorder for entries.
|
|
307
|
-
create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void language sql set search_path = public as $$
|
|
308
|
-
update "pages" pg set "sort" = u.ord
|
|
309
|
-
from unnest(p_ids) with ordinality as u(id, ord)
|
|
310
|
-
where pg."id" = u.id
|
|
311
|
-
and pg."space_id" = "smoodly_current_space"();
|
|
312
|
-
$$;
|
|
313
|
-
|
|
314
|
-
-- A version belongs to ONE locale of one page: each locale has its own
|
|
315
|
-
-- draft, its own publish and its own history. One row per publish plus
|
|
316
|
-
-- the live draft (spec 2026-09-13 history §1): the unpublished draft row
|
|
317
|
-
-- is rewritten in place and saved_at is when its content last changed.
|
|
318
|
-
create table if not exists "page_versions" (
|
|
319
|
-
"id" uuid primary key default gen_random_uuid(),
|
|
320
|
-
${SPACE_COL},
|
|
321
|
-
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
322
|
-
"locale" text not null,
|
|
323
|
-
"tree" jsonb not null,
|
|
324
|
-
"saved_at" timestamptz not null default now()
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
create index if not exists "page_versions_page_locale_idx"
|
|
328
|
-
on "page_versions" ("page_id", "locale", "saved_at");
|
|
329
|
-
|
|
330
|
-
create table if not exists "entries" (
|
|
331
|
-
"id" uuid primary key default gen_random_uuid(),
|
|
332
|
-
${SPACE_COL},
|
|
333
|
-
"collection" text not null,
|
|
334
|
-
"parent_id" uuid references "entries" ("id") on delete restrict,
|
|
335
|
-
"fields" jsonb not null default '{}'::jsonb,
|
|
336
|
-
"sort" numeric,
|
|
337
|
-
"created_at" timestamptz not null default now(),
|
|
338
|
-
"updated_at" timestamptz not null default now()
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
-- The node holds identity, tree position and the SHARED fields — every
|
|
342
|
-
-- field without .localized() (spec 2026-09-06 §7): a Person's photo is
|
|
343
|
-
-- the same in every language. Everything a locale owns — its segment,
|
|
344
|
-
-- publish state, the .localized() fields and the version pointers —
|
|
345
|
-
-- lives in entry_locales, one row per locale the entry exists in. There
|
|
346
|
-
-- is no sibling-slug index any more: URL uniqueness per locale is the
|
|
347
|
-
-- primary key of "paths", which the stores write inside the same
|
|
348
|
-
-- operation, and both adapters pre-check siblings per locale in code.
|
|
349
|
-
create index if not exists "entries_collection_sort_idx"
|
|
350
|
-
on "entries" ("space_id", "collection", "sort");
|
|
351
|
-
|
|
352
|
-
create index if not exists "entries_parent_idx"
|
|
353
|
-
on "entries" ("space_id", "collection", "parent_id");
|
|
354
|
-
|
|
355
|
-
create table if not exists "entry_locales" (
|
|
356
|
-
${SPACE_COL},
|
|
357
|
-
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
358
|
-
"locale" text not null,
|
|
359
|
-
"slug" text,
|
|
360
|
-
"status" text not null default 'draft',
|
|
361
|
-
"fields" jsonb not null default '{}'::jsonb,
|
|
362
|
-
"draft_version_id" uuid,
|
|
363
|
-
"published_version_id" uuid,
|
|
364
|
-
"updated_at" timestamptz not null default now(),
|
|
365
|
-
"updated_by" text,
|
|
366
|
-
primary key ("entry_id", "locale")
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
create index if not exists "entry_locales_status_idx"
|
|
370
|
-
on "entry_locales" ("space_id", "locale", "status");
|
|
371
|
-
|
|
372
|
-
-- A version belongs to ONE locale of one entry and stores the MERGED
|
|
373
|
-
-- fields (shared + localized + slug) as they were at that save, so
|
|
374
|
-
-- restoring an old Finnish version restores the shared fields too.
|
|
375
|
-
-- Rows for every collection and every global item (spec 2026-09-14 §1).
|
|
376
|
-
-- One row per publish plus the live draft (spec 2026-09-13 history §1).
|
|
377
|
-
create table if not exists "entry_versions" (
|
|
378
|
-
"id" uuid primary key default gen_random_uuid(),
|
|
379
|
-
${SPACE_COL},
|
|
380
|
-
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
381
|
-
"locale" text not null,
|
|
382
|
-
"fields" jsonb not null,
|
|
383
|
-
"saved_at" timestamptz not null default now()
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
create index if not exists "entry_versions_entry_locale_idx"
|
|
387
|
-
on "entry_versions" ("entry_id", "locale", "saved_at");
|
|
388
|
-
|
|
389
|
-
-- ── Draft pointers as foreign keys (schema v2, spec 2026-09-26 concurrency §1):
|
|
390
|
-
-- PostgREST embeds the draft version's saved_at through them, so every
|
|
391
|
-
-- locale row read carries the token. Set null on delete: discard moves
|
|
392
|
-
-- the pointer before it deletes the abandoned row, so nothing dangles.
|
|
393
|
-
alter table "page_locales" drop constraint if exists "page_locales_draft_version_fkey";
|
|
394
|
-
alter table "page_locales" add constraint "page_locales_draft_version_fkey"
|
|
395
|
-
foreign key ("draft_version_id") references "page_versions" ("id") on delete set null;
|
|
396
|
-
alter table "entry_locales" drop constraint if exists "entry_locales_draft_version_fkey";
|
|
397
|
-
alter table "entry_locales" add constraint "entry_locales_draft_version_fkey"
|
|
398
|
-
foreign key ("draft_version_id") references "entry_versions" ("id") on delete set null;
|
|
399
|
-
|
|
400
|
-
-- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
|
|
401
|
-
-- statement. Deliberately does not touch updated_at — a drag must not
|
|
402
|
-
-- make the whole collection look freshly edited. Scoped to the current
|
|
403
|
-
-- space by the same function the policies use.
|
|
404
|
-
create or replace function "smoodly_reorder"(
|
|
405
|
-
p_collection text,
|
|
406
|
-
p_ids uuid[]
|
|
407
|
-
) returns void language sql set search_path = public as $$
|
|
408
|
-
update "entries" e set "sort" = u.ord
|
|
409
|
-
from unnest(p_ids) with ordinality as u(id, ord)
|
|
410
|
-
where e."id" = u.id
|
|
411
|
-
and e."collection" = p_collection
|
|
412
|
-
and e."space_id" = "smoodly_current_space"();
|
|
413
|
-
$$;
|
|
414
|
-
|
|
415
|
-
create table if not exists "refs" (
|
|
416
|
-
"source_kind" text not null,
|
|
417
|
-
"source_id" uuid not null,
|
|
418
|
-
${SPACE_COL},
|
|
419
|
-
"source_field" text not null,
|
|
420
|
-
"target_collection" text not null,
|
|
421
|
-
"target_id" uuid not null,
|
|
422
|
-
primary key ("source_kind", "source_id", "source_field", "target_id")
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
-- The URL index (spec 2026-09-05 §4): the refs counterpart for paths.
|
|
426
|
-
-- Derived — the stores rewrite a target's rows on every create, rename,
|
|
427
|
-
-- move and delete — so one primary-key hit resolves any URL in any locale
|
|
428
|
-
-- and no page or entry can share a URL with another in any locale.
|
|
429
|
-
-- Rows exist for drafts too (the editor needs them); the read policy
|
|
430
|
-
-- admits a row only when its target is published.
|
|
431
|
-
-- A row with "redirect" set is an old address of its target (spec
|
|
432
|
-
-- 2026-09-26 redirects §2): the same primary key, so a redirect can never
|
|
433
|
-
-- sit where a live URL is and a live row taking the address replaces it.
|
|
434
|
-
-- "anchor" names the code address a row hangs under (spec 2026-09-29
|
|
435
|
-
-- anchored paths): "path" is only the part editors made, and the address
|
|
436
|
-
-- itself lives in the config; '' is the editors' own tree.
|
|
437
|
-
create table if not exists "paths" (
|
|
438
|
-
${SPACE_COL},
|
|
439
|
-
"locale" text not null,
|
|
440
|
-
"anchor" text not null default '',
|
|
441
|
-
"path" text not null,
|
|
442
|
-
"kind" text not null check ("kind" in ('page', 'entry')),
|
|
443
|
-
"target_id" uuid not null,
|
|
444
|
-
"redirect" boolean not null default false,
|
|
445
|
-
primary key ("space_id", "locale", "anchor", "path")
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
create index if not exists "paths_target_idx"
|
|
449
|
-
on "paths" ("space_id", "kind", "target_id");
|
|
450
|
-
|
|
451
|
-
-- Media (spec 2026-09-08): one row per upload, bytes in the "media"
|
|
452
|
-
-- bucket at "path". Immediate — never draft-gated, never versioned —
|
|
453
|
-
-- so there is no locale row and no version table. status 'pending' is
|
|
454
|
-
-- a row whose signed upload has not been confirmed; list never shows it.
|
|
455
|
-
create table if not exists "assets" (
|
|
456
|
-
"id" uuid primary key default gen_random_uuid(),
|
|
457
|
-
${SPACE_COL},
|
|
458
|
-
"kind" text not null check ("kind" in ('image', 'video', 'file')),
|
|
459
|
-
"status" text not null default 'pending' check ("status" in ('pending', 'ready')),
|
|
460
|
-
"path" text not null,
|
|
461
|
-
"mime" text not null,
|
|
462
|
-
"size" bigint not null,
|
|
463
|
-
"width" integer,
|
|
464
|
-
"height" integer,
|
|
465
|
-
"duration" numeric,
|
|
466
|
-
"poster_id" uuid references "assets" ("id") on delete set null,
|
|
467
|
-
"filename" text not null,
|
|
468
|
-
"title" text,
|
|
469
|
-
"alt" text,
|
|
470
|
-
"caption" text,
|
|
471
|
-
"credit" text,
|
|
472
|
-
"created_by" text,
|
|
473
|
-
"created_at" timestamptz not null default now(),
|
|
474
|
-
"updated_at" timestamptz not null default now()
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
create index if not exists "assets_space_created_idx"
|
|
478
|
-
on "assets" ("space_id", "status", "created_at" desc);
|
|
479
|
-
|
|
480
|
-
-- Replace the targets' rows with the given rows in ONE call: a SQL
|
|
481
|
-
-- function runs in the caller's transaction, so a primary-key collision
|
|
482
|
-
-- rolls the whole rewrite back. Inserted rows get their space from the
|
|
483
|
-
-- a_set_space trigger (the claim, or the default space).
|
|
484
|
-
-- A target may name redirect_locales: its old live rows there stay as redirects.
|
|
485
|
-
-- A row carries "keys": every (anchor, path) that yields its full URL, from
|
|
486
|
-
-- the config's addresses (spec 2026-09-29 anchored paths §2). A redirect at
|
|
487
|
-
-- any of them goes (live wins, a redirect by hand has no anchor), and an old
|
|
488
|
-
-- row at one is the same address moving between anchors, never a redirect.
|
|
489
|
-
create or replace function "smoodly_rewrite_paths"(p_targets jsonb, p_rows jsonb) returns void language sql set search_path = public as $$
|
|
490
|
-
delete from "paths" p
|
|
491
|
-
using jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
|
|
492
|
-
where p."redirect" and p."locale" = r.locale
|
|
493
|
-
and exists (select 1 from jsonb_to_recordset(r.keys) as k(anchor text, path text) where k.anchor = p."anchor" and k.path = p."path")
|
|
494
|
-
and p."space_id" = "smoodly_current_space"();
|
|
495
|
-
update "paths" p set "redirect" = true
|
|
496
|
-
from jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
497
|
-
where not p."redirect" and p."kind" = t.kind and p."target_id" = t.id
|
|
498
|
-
and p."locale" = any (coalesce(t.redirect_locales, '{}'::text[]))
|
|
499
|
-
and not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid, keys jsonb)
|
|
500
|
-
cross join lateral jsonb_to_recordset(r.keys) as k(anchor text, path text)
|
|
501
|
-
where r.locale = p."locale" and k.anchor = p."anchor" and k.path = p."path")
|
|
502
|
-
and p."space_id" = "smoodly_current_space"();
|
|
503
|
-
delete from "paths" p
|
|
504
|
-
using jsonb_to_recordset(p_targets) as t(kind text, id uuid, redirect_locales text[])
|
|
505
|
-
where p."kind" = t.kind and p."target_id" = t.id
|
|
506
|
-
and (not p."redirect" or not exists (select 1 from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid)
|
|
507
|
-
where r.kind = p."kind" and r.id = p."target_id" and r.locale = p."locale"))
|
|
508
|
-
and p."space_id" = "smoodly_current_space"();
|
|
509
|
-
insert into "paths" ("locale", "anchor", "path", "kind", "target_id")
|
|
510
|
-
select r.locale, r.anchor, r.path, r.kind, r.id
|
|
511
|
-
from jsonb_to_recordset(p_rows) as r(locale text, anchor text, path text, kind text, id uuid);
|
|
512
|
-
$$;
|
|
513
|
-
|
|
514
|
-
-- One call reads every candidate (anchor, path) of a request (spec
|
|
515
|
-
-- 2026-09-29 anchored paths §2); the caller picks by precedence. Invoker
|
|
516
|
-
-- rights: the paths read policy applies, as to a plain select.
|
|
517
|
-
create or replace function "smoodly_resolve_path"(p_locale text, p_keys jsonb) returns setof "paths" language sql stable set search_path = public as $$
|
|
518
|
-
select p.* from "paths" p
|
|
519
|
-
join jsonb_to_recordset(p_keys) as k(anchor text, path text) on p."anchor" = k.anchor and p."path" = k.path
|
|
520
|
-
where p."locale" = p_locale;
|
|
521
|
-
$$;`.trim(),
|
|
522
|
-
foreignKeyIndexesSQL(),
|
|
523
|
-
`
|
|
524
|
-
-- RLS on every core table. The service role bypasses it (self-host
|
|
525
|
-
-- adapters untouched). The policy set that follows (space layer) admits
|
|
526
|
-
-- a write key to its own space and no other key to any content (spec
|
|
527
|
-
-- 2026-09-29 content API contract §8): the publishable key serves
|
|
528
|
-
-- sign-in and presence, and the generated per-collection views
|
|
529
|
-
-- (collectionSQL) run with security_invoker = true, so they are subject
|
|
530
|
-
-- to this same smoodly_read policy rather than their owner's privileges.
|
|
531
|
-
-- smoodly_meta: which version of this schema the database holds (spec
|
|
532
|
-
-- 2026-09-26 §1). One row per database, not per space; every core
|
|
533
|
-
-- migration file upserts the version row as its last statement, and the
|
|
534
|
-
-- package compares it with SCHEMA_VERSION (behind blocks the admin).
|
|
535
|
-
create table if not exists "smoodly_meta" (
|
|
536
|
-
"key" text primary key,
|
|
537
|
-
"value" text not null
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
alter table "pages" enable row level security;
|
|
541
|
-
alter table "page_locales" enable row level security;
|
|
542
|
-
alter table "page_versions" enable row level security;
|
|
543
|
-
alter table "entries" enable row level security;
|
|
544
|
-
alter table "entry_locales" enable row level security;
|
|
545
|
-
alter table "entry_versions" enable row level security;
|
|
546
|
-
alter table "refs" enable row level security;
|
|
547
|
-
alter table "paths" enable row level security;
|
|
548
|
-
alter table "assets" enable row level security;
|
|
549
|
-
alter table "smoodly_meta" enable row level security;
|
|
550
|
-
`.trim(),
|
|
551
|
-
publishedTablesSQL(),
|
|
552
|
-
spaceLayerSQL(),
|
|
553
|
-
listEntriesSQL(),
|
|
554
|
-
grantsSQL(),
|
|
555
|
-
].join("\n\n");
|
|
556
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { type AssetPage, type AssetQuery, type AssetStore, type BeginUploadInput, type CreateAssetInput, type FinishInput, type UploadTicket } from "./asset-store.ts";
|
|
3
|
-
import { type AssetMeta, type AssetRecord } from "./media.ts";
|
|
4
|
-
export declare class SupabaseAssetStore implements AssetStore {
|
|
5
|
-
private db;
|
|
6
|
-
constructor(db: SupabaseClient);
|
|
7
|
-
private fail;
|
|
8
|
-
private bucket;
|
|
9
|
-
private toRecord;
|
|
10
|
-
/** The space this client writes into — the claim, or the default. */
|
|
11
|
-
private space;
|
|
12
|
-
private insert;
|
|
13
|
-
beginUpload(input: BeginUploadInput): Promise<UploadTicket>;
|
|
14
|
-
finishUpload(id: string, dims: FinishInput): Promise<AssetRecord>;
|
|
15
|
-
create(input: CreateAssetInput): Promise<AssetRecord>;
|
|
16
|
-
get(id: string): Promise<AssetRecord | null>;
|
|
17
|
-
getMany(ids: string[]): Promise<Record<string, AssetRecord>>;
|
|
18
|
-
list(query?: AssetQuery): Promise<AssetPage>;
|
|
19
|
-
update(id: string, meta: AssetMeta): Promise<AssetRecord>;
|
|
20
|
-
delete(id: string): Promise<void>;
|
|
21
|
-
}
|