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.
Files changed (285) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth-config.d.ts +6 -5
  3. package/dist/admin/auth.d.ts +3 -11
  4. package/dist/admin/auth.js +1 -30
  5. package/dist/admin/browser/fetch-op.js +3 -2
  6. package/dist/admin/browser/main.js +6 -2
  7. package/dist/admin/client-ops.d.ts +5 -4
  8. package/dist/admin/client-ops.js +14 -11
  9. package/dist/admin/delivery-mode.d.ts +1 -1
  10. package/dist/admin/delivery-mode.js +2 -2
  11. package/dist/admin/drift.d.ts +0 -2
  12. package/dist/admin/drift.js +1 -1
  13. package/dist/admin/editor/EditorView.d.ts +0 -3
  14. package/dist/admin/editor/EditorView.js +11 -16
  15. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  16. package/dist/admin/editor/FloatingToolbar.js +1 -1
  17. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  18. package/dist/admin/editor/SeoSettings.js +2 -2
  19. package/dist/admin/editor/canvas-address.d.ts +8 -0
  20. package/dist/admin/editor/canvas-address.js +18 -2
  21. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  22. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  23. package/dist/admin/editor/inline-dom.d.ts +0 -4
  24. package/dist/admin/editor/inline-dom.js +1 -1
  25. package/dist/admin/editor/preview-grant.d.ts +6 -0
  26. package/dist/admin/editor/preview-grant.js +25 -0
  27. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  28. package/dist/admin/files.generated.js +1 -1
  29. package/dist/admin/fixed-nodes.d.ts +0 -6
  30. package/dist/admin/fixed-nodes.js +1 -81
  31. package/dist/admin/forms/CropEditor.d.ts +4 -2
  32. package/dist/admin/forms/CropEditor.js +58 -5
  33. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  34. package/dist/admin/forms/ImagePreview.js +9 -3
  35. package/dist/admin/forms/MediaGrid.js +2 -1
  36. package/dist/admin/forms/MediaPicker.js +9 -6
  37. package/dist/admin/forms/MediaWidget.js +11 -9
  38. package/dist/admin/forms/assets-context.d.ts +7 -3
  39. package/dist/admin/forms/assets-context.js +5 -3
  40. package/dist/admin/forms/crop-editor.d.ts +24 -2
  41. package/dist/admin/forms/crop-editor.js +36 -2
  42. package/dist/admin/forms/date-picker.d.ts +0 -1
  43. package/dist/admin/forms/date-picker.js +1 -1
  44. package/dist/admin/forms/tabs.d.ts +0 -1
  45. package/dist/admin/forms/tabs.js +1 -1
  46. package/dist/admin/forms/upload.d.ts +18 -11
  47. package/dist/admin/forms/upload.js +62 -24
  48. package/dist/admin/global-items.d.ts +0 -3
  49. package/dist/admin/global-items.js +0 -54
  50. package/dist/admin/handler.d.ts +22 -28
  51. package/dist/admin/handler.js +146 -47
  52. package/dist/admin/health.d.ts +0 -4
  53. package/dist/admin/index.d.ts +5 -8
  54. package/dist/admin/index.js +3 -6
  55. package/dist/admin/ops.d.ts +36 -22
  56. package/dist/admin/page-data.d.ts +14 -6
  57. package/dist/admin/page-data.js +1 -1
  58. package/dist/admin/session-flow.d.ts +43 -0
  59. package/dist/admin/session-flow.js +94 -0
  60. package/dist/admin/shell/AdminApp.d.ts +8 -3
  61. package/dist/admin/shell/AdminApp.js +37 -58
  62. package/dist/admin/shell/ContentHealthPage.js +1 -3
  63. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  64. package/dist/admin/shell/GlobalFields.js +1 -1
  65. package/dist/admin/shell/IconRail.js +5 -4
  66. package/dist/admin/shell/MediaScreen.js +9 -6
  67. package/dist/admin/shell/MembersPage.d.ts +1 -2
  68. package/dist/admin/shell/MembersPage.js +19 -90
  69. package/dist/admin/shell/SettingsView.d.ts +1 -2
  70. package/dist/admin/shell/SettingsView.js +2 -2
  71. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  72. package/dist/admin/shell/SignedOutView.js +9 -0
  73. package/dist/admin/shell/console-links.d.ts +5 -0
  74. package/dist/admin/shell/console-links.js +11 -0
  75. package/dist/admin/shell/members-list.d.ts +1 -14
  76. package/dist/admin/shell/members-list.js +2 -23
  77. package/dist/admin/shell/pages-tree.d.ts +0 -2
  78. package/dist/admin/shell/pages-tree.js +1 -1
  79. package/dist/admin/shell/presence-socket.d.ts +10 -0
  80. package/dist/admin/shell/presence-socket.js +191 -0
  81. package/dist/admin/shell/presence.d.ts +6 -18
  82. package/dist/admin/shell/presence.js +4 -11
  83. package/dist/admin/shell/session.d.ts +20 -12
  84. package/dist/admin/shell/session.js +113 -47
  85. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  86. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  87. package/dist/admin/ui/primitives.d.ts +4 -14
  88. package/dist/admin/ui/primitives.js +5 -8
  89. package/dist/admin/ui/theme.js +13 -2
  90. package/dist/admin/validate.js +10 -1
  91. package/dist/api-client.d.ts +26 -0
  92. package/dist/api-client.js +107 -0
  93. package/dist/assemble.d.ts +4 -41
  94. package/dist/assemble.js +4 -302
  95. package/dist/asset-store.d.ts +28 -30
  96. package/dist/asset-store.js +26 -121
  97. package/dist/astro/components/Head.astro +4 -1
  98. package/dist/astro/components/Image.astro +12 -17
  99. package/dist/astro/components/Video.astro +5 -2
  100. package/dist/astro/head.d.ts +3 -0
  101. package/dist/astro/head.js +24 -0
  102. package/dist/astro/index.js +54 -25
  103. package/dist/astro/locale-paths.d.ts +5 -2
  104. package/dist/astro/locale-paths.js +10 -2
  105. package/dist/astro/routes/admin.js +9 -7
  106. package/dist/astro/runtime.d.ts +2 -1
  107. package/dist/astro/runtime.js +4 -3
  108. package/dist/astro/server.d.ts +7 -11
  109. package/dist/astro/server.js +22 -28
  110. package/dist/astro/shell.d.ts +2 -2
  111. package/dist/astro/static-media-node.d.ts +4 -0
  112. package/dist/astro/static-media-node.js +25 -0
  113. package/dist/astro/static-media.d.ts +17 -0
  114. package/dist/astro/static-media.js +55 -0
  115. package/dist/astro/static.d.ts +5 -10
  116. package/dist/astro/static.js +13 -29
  117. package/dist/canvas-marker.d.ts +14 -0
  118. package/dist/canvas-marker.js +36 -0
  119. package/dist/cli/api.d.ts +14 -0
  120. package/dist/cli/api.js +48 -0
  121. package/dist/cli/app-env.d.ts +2 -0
  122. package/dist/cli/app-env.js +20 -0
  123. package/dist/cli/export.d.ts +16 -2
  124. package/dist/cli/export.js +52 -35
  125. package/dist/cli/import.d.ts +7 -11
  126. package/dist/cli/import.js +50 -54
  127. package/dist/cli/load-config.d.ts +0 -1
  128. package/dist/cli/load-config.js +1 -1
  129. package/dist/cli/main.d.ts +4 -12
  130. package/dist/cli/main.js +26 -72
  131. package/dist/cli/push.d.ts +6 -0
  132. package/dist/cli/push.js +31 -0
  133. package/dist/collections.d.ts +0 -1
  134. package/dist/collections.js +1 -1
  135. package/dist/config.d.ts +20 -5
  136. package/dist/config.js +18 -1
  137. package/dist/content/content-api.d.ts +22 -23
  138. package/dist/content/content-api.js +46 -56
  139. package/dist/content/mime.d.ts +1 -0
  140. package/dist/content/mime.js +6 -0
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/deploy-store.js +1 -0
  143. package/dist/entry-store.d.ts +3 -89
  144. package/dist/entry-store.js +2 -550
  145. package/dist/env.d.ts +27 -20
  146. package/dist/env.js +38 -50
  147. package/dist/fields.d.ts +11 -3
  148. package/dist/fields.js +33 -2
  149. package/dist/global-id.d.ts +2 -0
  150. package/dist/global-id.js +4 -2
  151. package/dist/image/SmoodlyImage.d.ts +9 -5
  152. package/dist/image/SmoodlyImage.js +29 -15
  153. package/dist/image/SmoodlyVideo.d.ts +1 -1
  154. package/dist/image/SmoodlyVideo.js +3 -2
  155. package/dist/image/index.d.ts +2 -3
  156. package/dist/image/index.js +0 -2
  157. package/dist/image/markup.d.ts +53 -0
  158. package/dist/image/markup.js +111 -0
  159. package/dist/index.d.ts +12 -22
  160. package/dist/index.js +7 -20
  161. package/dist/media-cut.d.ts +44 -0
  162. package/dist/media-cut.js +70 -0
  163. package/dist/media.d.ts +57 -3
  164. package/dist/media.js +125 -15
  165. package/dist/member-store.d.ts +3 -22
  166. package/dist/member-store.js +1 -53
  167. package/dist/next/admin-route.d.ts +1 -1
  168. package/dist/next/admin-route.js +5 -3
  169. package/dist/next/meta.js +2 -3
  170. package/dist/next/page-renderer.d.ts +2 -2
  171. package/dist/next/page-renderer.js +28 -15
  172. package/dist/next/queries.d.ts +2 -2
  173. package/dist/path-index.d.ts +0 -24
  174. package/dist/path-index.js +0 -98
  175. package/dist/push.d.ts +30 -0
  176. package/dist/push.js +52 -0
  177. package/dist/read-counter.d.ts +2 -0
  178. package/dist/read-counter.js +6 -0
  179. package/dist/reads.d.ts +9 -8
  180. package/dist/reads.js +7 -3
  181. package/dist/registry-json.d.ts +11 -0
  182. package/dist/registry-json.js +81 -0
  183. package/dist/render.d.ts +0 -1
  184. package/dist/render.js +1 -1
  185. package/dist/resolve.d.ts +12 -6
  186. package/dist/resolve.js +11 -8
  187. package/dist/revalidate.d.ts +8 -1
  188. package/dist/revalidate.js +8 -1
  189. package/dist/richtext/index.d.ts +1 -1
  190. package/dist/section-wrapper.d.ts +1 -1
  191. package/dist/section-wrapper.js +0 -9
  192. package/dist/signed-message.d.ts +26 -0
  193. package/dist/signed-message.js +103 -0
  194. package/dist/site.d.ts +59 -18
  195. package/dist/site.js +21 -252
  196. package/dist/space-transfer.d.ts +5 -2
  197. package/dist/space-transfer.js +17 -5
  198. package/dist/store.d.ts +2 -80
  199. package/dist/store.js +3 -489
  200. package/dist/toolset.d.ts +2 -1
  201. package/dist/toolset.js +1 -1
  202. package/dist/version.d.ts +1 -1
  203. package/dist/version.js +1 -1
  204. package/package.json +4 -15
  205. package/dist/admin/ops-impl.d.ts +0 -51
  206. package/dist/admin/ops-impl.js +0 -1400
  207. package/dist/admin/shell/InviteModal.d.ts +0 -6
  208. package/dist/admin/shell/InviteModal.js +0 -28
  209. package/dist/admin/shell/LoginView.d.ts +0 -5
  210. package/dist/admin/shell/LoginView.js +0 -80
  211. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  212. package/dist/admin/shell/SchemaNotice.js +0 -4
  213. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  214. package/dist/admin/shell/SetPasswordView.js +0 -41
  215. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  216. package/dist/admin/shell/presence-realtime.js +0 -32
  217. package/dist/admin/supabase-auth.d.ts +0 -7
  218. package/dist/admin/supabase-auth.js +0 -30
  219. package/dist/astro/deploy-deps.d.ts +0 -10
  220. package/dist/astro/deploy-deps.js +0 -8
  221. package/dist/astro/image.d.ts +0 -27
  222. package/dist/astro/image.js +0 -29
  223. package/dist/bind.d.ts +0 -19
  224. package/dist/bind.js +0 -34
  225. package/dist/cli/add-admin.d.ts +0 -11
  226. package/dist/cli/add-admin.js +0 -25
  227. package/dist/cli/load-collections.d.ts +0 -2
  228. package/dist/cli/load-collections.js +0 -27
  229. package/dist/cli/migrate.d.ts +0 -43
  230. package/dist/cli/migrate.js +0 -138
  231. package/dist/cli/rebuild.d.ts +0 -23
  232. package/dist/cli/rebuild.js +0 -22
  233. package/dist/cli/space-io.d.ts +0 -41
  234. package/dist/cli/space-io.js +0 -84
  235. package/dist/client.d.ts +0 -11
  236. package/dist/client.js +0 -54
  237. package/dist/current-space.d.ts +0 -4
  238. package/dist/current-space.js +0 -9
  239. package/dist/document-stamp.d.ts +0 -7
  240. package/dist/document-stamp.js +0 -48
  241. package/dist/image/loaders.d.ts +0 -15
  242. package/dist/image/loaders.js +0 -5
  243. package/dist/image/next-config.d.ts +0 -18
  244. package/dist/image/next-config.js +0 -32
  245. package/dist/image/remote-pattern.d.ts +0 -1
  246. package/dist/image/remote-pattern.js +0 -5
  247. package/dist/migrations.d.ts +0 -37
  248. package/dist/migrations.js +0 -73
  249. package/dist/project.d.ts +0 -88
  250. package/dist/project.js +0 -231
  251. package/dist/published.d.ts +0 -142
  252. package/dist/published.js +0 -164
  253. package/dist/ref-index.d.ts +0 -19
  254. package/dist/ref-index.js +0 -0
  255. package/dist/schema-read.d.ts +0 -23
  256. package/dist/schema-read.js +0 -50
  257. package/dist/schema-version.d.ts +0 -19
  258. package/dist/schema-version.js +0 -34
  259. package/dist/sql-space.d.ts +0 -44
  260. package/dist/sql-space.js +0 -289
  261. package/dist/sql.d.ts +0 -15
  262. package/dist/sql.js +0 -556
  263. package/dist/supabase-asset-store.d.ts +0 -21
  264. package/dist/supabase-asset-store.js +0 -170
  265. package/dist/supabase-entry-store.d.ts +0 -92
  266. package/dist/supabase-entry-store.js +0 -824
  267. package/dist/supabase-member-store.d.ts +0 -21
  268. package/dist/supabase-member-store.js +0 -96
  269. package/dist/supabase-path-index.d.ts +0 -30
  270. package/dist/supabase-path-index.js +0 -138
  271. package/dist/supabase-published-store.d.ts +0 -56
  272. package/dist/supabase-published-store.js +0 -314
  273. package/dist/supabase-store.d.ts +0 -103
  274. package/dist/supabase-store.js +0 -678
  275. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  276. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  277. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  278. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  279. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  280. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  281. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  282. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  283. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  284. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  285. 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
- }