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
@@ -9,16 +9,8 @@
9
9
  // save rewrites the entry's outgoing edges over EVERY locale's live
10
10
  // field set. Reorder rewrites sort 1..N in one pass and NEVER touches
11
11
  // updatedAt — order is a property of the collection, not an edit.
12
- import { collectionDepth, collectionOrder } from "./collections.js";
13
12
  import { collectEntryRefs } from "./refs.js";
14
- import { MemoryRefIndex } from "./ref-index.js";
15
- import { MemoryPathIndex } from "./path-index.js";
16
- import { anchorsFor } from "./anchors.js";
17
- import { groupRedirects } from "./store.js";
18
- import { assertFresh } from "./draft-token.js";
19
- import { globalEntryId } from "./global-id.js";
20
- import { DEFAULT_SPACE_ID } from "./sql-space.js";
21
- import { assertDepth, assertSupportedLocale, chainOf, entryPathRows, slugOf, unsupportedLocale, rewriteTargets, } from "./paths.js";
13
+ import { slugOf, unsupportedLocale } from "./paths.js";
22
14
  /** The field types whose value is an array: a scalar `where` on one is
23
15
  * containment, and any-of on one is refused by the site layer (both
24
16
  * adapters read the text form of the whole array, never its elements). */
@@ -45,13 +37,6 @@ export function schemaOf(collections, name) {
45
37
  }
46
38
  /** A global item is a collection with exactly one entry (spec 2026-09-07). */
47
39
  export const isGlobalSchema = (schema) => schema.kind === "global";
48
- let counter = 0;
49
- const uid = (prefix) => `${prefix}-${++counter}-${Date.now().toString(36)}`;
50
- /** A version's saved time: wall-clock, but strictly increasing, so two saves in one
51
- * millisecond still move the draft token (spec 2026-09-26 concurrency §1) the way
52
- * Postgres's microsecond `now()` does. */
53
- let lastSavedAt = 0;
54
- const savedClock = () => (lastSavedAt = Math.max(Date.now(), lastSavedAt + 1));
55
40
  export const DEFAULT_ENTRY_LOCALES = { default: "en", supported: ["en"] };
56
41
  export const entryErrors = {
57
42
  notFound: (collection, id) => new Error(`smoodly: no entry "${id}" in "${collection}".`),
@@ -146,7 +131,7 @@ export function entryTitleIn(record, locale, schema) {
146
131
  * Sort comparator for one order rule; `seq` breaks ties by creation. A
147
132
  * declared order field is SHARED (collection() refuses a localized one),
148
133
  * so `e.fields[order.by]` reads the node. Nulls sort last in BOTH
149
- * directions (the Supabase adapter uses `nullsFirst: false` to match);
134
+ * directions (the SQLite list compiler sorts nulls last to match);
150
135
  * the creation tiebreak follows `direction` for non-manual orders.
151
136
  * Declared fields compare numerically when both values are numbers,
152
137
  * otherwise as strings via `localeCompare` — the same order Postgres
@@ -172,536 +157,3 @@ export function compareEntries(order, seq) {
172
157
  return d !== 0 ? d * dir : (seq(a) - seq(b)) * dir;
173
158
  };
174
159
  }
175
- /** Comparator for `query`: like compareEntries, but a declared field reads the
176
- * merged snapshot, `updatedAt` the locale row's, and an absent, null or empty-string value
177
- * sorts last in both directions; ties break by creation order (ascending). */
178
- export function compareSnapshots(order, locale, seq) {
179
- return (a, b) => {
180
- if (order === "manual") {
181
- const d = (a.record.sort ?? Infinity) - (b.record.sort ?? Infinity);
182
- return d !== 0 ? d : seq(a.record.id) - seq(b.record.id);
183
- }
184
- const dir = order.direction === "asc" ? 1 : -1;
185
- const value = (r) => order.by === "createdAt" ? r.record.createdAt : order.by === "updatedAt" ? r.record.locales[locale]?.updatedAt : r.fields[order.by];
186
- const empty = (v) => (v === "" ? undefined : v); // a cleared text or date is empty too
187
- const va = empty(value(a)), vb = empty(value(b));
188
- const tie = seq(a.record.id) - seq(b.record.id);
189
- if (order.by === "createdAt")
190
- return tie * dir; // the clock can tie within a millisecond; creation order cannot
191
- if (va == null && vb == null)
192
- return tie;
193
- if (va == null)
194
- return 1;
195
- if (vb == null)
196
- return -1;
197
- const d = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));
198
- return d !== 0 ? d * dir : tie;
199
- };
200
- }
201
- export class MemoryEntryStore {
202
- constructor(collections,
203
- // pass the SAME index to MemoryPageStore so cross-kind questions work
204
- refs = new MemoryRefIndex(), options = {}) {
205
- this.refs = refs;
206
- this.entries = new Map();
207
- this.versions = new Map();
208
- /** Insertion order per version — `savedAt` alone can tie within a millisecond. */
209
- this.versionSeq = new Map();
210
- this.seq = 0;
211
- this.order = new Map();
212
- this.byId = (id) => this.entries.get(id);
213
- this.locales = options.locales ?? DEFAULT_ENTRY_LOCALES;
214
- this.schemas = [...collections, ...(options.globals ?? [])];
215
- // Alone, the store's own index reads its collections' addresses (spec 2026-09-29 anchored paths §2).
216
- this.paths = options.paths ?? new MemoryPathIndex(anchorsFor({ locales: this.locales, collections }));
217
- this.elements = options.elements;
218
- }
219
- schema(collection) {
220
- return schemaOf(this.schemas, collection);
221
- }
222
- row(collection, id) {
223
- const entry = this.entries.get(id);
224
- return entry && entry.collection === collection ? entry : null;
225
- }
226
- must(collection, id) {
227
- const entry = this.row(collection, id);
228
- if (!entry)
229
- throw entryErrors.notFound(collection, id);
230
- return entry;
231
- }
232
- snapshot(entry) {
233
- return structuredClone(entry);
234
- }
235
- subtree(entry) {
236
- const out = [entry];
237
- const walk = (parentId) => {
238
- for (const e of this.entries.values())
239
- if (e.parentId === parentId) {
240
- out.push(e);
241
- walk(e.id);
242
- }
243
- };
244
- walk(entry.id);
245
- return out;
246
- }
247
- /** An edit bumps the node and the edited row; `move` and `reorder` never call this. */
248
- bump(entry, row, by) {
249
- const now = Date.now();
250
- entry.updatedAt = now > entry.updatedAt ? now : entry.updatedAt + 1;
251
- if (row) {
252
- row.updatedAt = entry.updatedAt;
253
- row.updatedBy = by ?? null;
254
- }
255
- }
256
- /** The draft rule (spec 2026-09-13 history §1) for every collection (spec 2026-09-14 §1):
257
- * the unpublished draft row is rewritten in place and a new one starts only when the
258
- * draft IS the published one. */
259
- recordVersion(entry, locale) {
260
- const row = entry.locales[locale];
261
- const fields = entryFieldsIn(entry, locale);
262
- const current = row.draftVersionId ? this.versions.get(row.draftVersionId) : undefined;
263
- if (current && row.draftVersionId !== row.publishedVersionId) {
264
- current.fields = fields;
265
- current.savedAt = savedClock();
266
- row.draftSavedAt = String(current.savedAt);
267
- this.versionSeq.set(current.id, ++this.seq); // newest again
268
- return;
269
- }
270
- const version = { id: uid("version"), entryId: entry.id, locale, fields, savedAt: savedClock() };
271
- this.versions.set(version.id, version);
272
- this.versionSeq.set(version.id, ++this.seq);
273
- row.draftVersionId = version.id;
274
- row.draftSavedAt = String(version.savedAt);
275
- }
276
- /** A published row from before every collection was versioned has no snapshot; give it
277
- * one from what is live BEFORE a write changes the fields, so the site keeps serving it
278
- * and the write lands in a new draft row (spec 2026-09-14 §1). */
279
- ensureLiveSnapshot(entry, locale) {
280
- const row = entry.locales[locale];
281
- if (row.status !== "published" || row.publishedVersionId !== null)
282
- return;
283
- const version = { id: uid("version"), entryId: entry.id, locale, fields: entryFieldsIn(entry, locale), savedAt: savedClock() };
284
- this.versions.set(version.id, version);
285
- this.versionSeq.set(version.id, ++this.seq);
286
- row.draftVersionId = version.id;
287
- row.publishedVersionId = version.id;
288
- row.draftSavedAt = String(version.savedAt);
289
- }
290
- dropVersions(entryId, locale) {
291
- for (const [id, v] of this.versions) {
292
- if (v.entryId === entryId && (locale === undefined || v.locale === locale)) {
293
- this.versions.delete(id);
294
- this.versionSeq.delete(id);
295
- }
296
- }
297
- }
298
- /** Every field set the site can render for this entry: each present
299
- * locale's live view, plus a published snapshot the draft has moved
300
- * past (still rendered by the published site). */
301
- liveFieldSets(entry) {
302
- const sets = [];
303
- for (const [locale, row] of Object.entries(entry.locales)) {
304
- sets.push(entryFieldsIn(entry, locale));
305
- if (row.status === "published" && row.publishedVersionId && row.publishedVersionId !== row.draftVersionId) {
306
- const snapshot = this.versions.get(row.publishedVersionId);
307
- if (snapshot)
308
- sets.push(snapshot.fields);
309
- }
310
- }
311
- return sets;
312
- }
313
- async writePaths(collection, nodes, redirect = true) {
314
- await this.paths.rewrite(rewriteTargets("entry", nodes, redirect), entryPathRows(nodes, this.byId, collection));
315
- }
316
- writeRefs(entry) {
317
- this.refs.rewrite({ sourceKind: "entry", sourceId: entry.id }, entryRefEdges(this.liveFieldSets(entry), this.schema(entry.collection), this.elements));
318
- }
319
- siblings(collection, parentId, except) {
320
- return [...this.entries.values()]
321
- .filter((e) => e.collection === collection && e.parentId === parentId && e.id !== except)
322
- .sort(compareEntries("manual", (e) => this.order.get(e.id)));
323
- }
324
- /** The friendly pre-check (spec 2026-09-06 §11): a string slug must be
325
- * unique among the siblings PRESENT in that locale; null never
326
- * conflicts. The paths primary key remains the invariant. */
327
- assertSiblingSlug(collection, parentId, locale, slug, except) {
328
- if (slug === null)
329
- return;
330
- for (const e of this.siblings(collection, parentId, except)) {
331
- if (e.locales[locale]?.slug === slug)
332
- throw entryErrors.slugTaken(collection);
333
- }
334
- }
335
- /** Same collection, within depth. Returns the parent (null at the root). */
336
- assertParent(collection, parentId, subtreeHeight) {
337
- if (parentId === null)
338
- return null;
339
- const parent = this.entries.get(parentId);
340
- if (!parent)
341
- throw entryErrors.notFound(collection, parentId);
342
- if (parent.collection !== collection)
343
- throw entryErrors.foreignParent();
344
- assertDepth(chainOf(parent, this.byId).length + subtreeHeight, collectionDepth(this.schema(collection)), "the entry");
345
- return parent;
346
- }
347
- /** The parent rule: a locale can only be added where the parent has it. */
348
- assertParentHasLocale(parent, locale) {
349
- if (parent && !parent.locales[locale])
350
- throw entryErrors.parentNotInLocale(locale);
351
- }
352
- async create(collection, fields, options, by) {
353
- const schema = this.schema(collection);
354
- assertSupportedLocale(options.locale, this.locales);
355
- const parentId = options.parentId ?? null;
356
- if (isGlobalSchema(schema)) {
357
- if ([...this.entries.values()].some((e) => e.collection === collection))
358
- throw entryErrors.singleton(collection);
359
- if (slugOf(fields) !== null)
360
- throw entryErrors.noSlug(collection);
361
- }
362
- const parent = this.assertParent(collection, parentId, 1);
363
- this.assertParentHasLocale(parent, options.locale);
364
- const { shared, localized, slug } = splitFields(fields, schema);
365
- this.assertSiblingSlug(collection, parentId, options.locale, slug);
366
- const now = Date.now();
367
- const entry = {
368
- // A global item's id is derived, not minted (spec 2026-09-07 §10):
369
- // the same uuid v5 the Supabase adapter computes, so self-host and
370
- // cloud agree on the row a materialization would create.
371
- id: isGlobalSchema(schema) ? await globalEntryId(DEFAULT_SPACE_ID, collection) : uid("entry"),
372
- collection, parentId, fields: shared,
373
- locales: {
374
- [options.locale]: {
375
- slug, status: "draft", fields: localized, draftVersionId: null, draftSavedAt: null, publishedVersionId: null, updatedAt: now, updatedBy: by ?? null,
376
- },
377
- },
378
- sort: null, createdAt: now, updatedAt: now,
379
- };
380
- this.recordVersion(entry, options.locale);
381
- this.entries.set(entry.id, entry);
382
- this.order.set(entry.id, ++this.seq);
383
- try {
384
- await this.writePaths(collection, [entry]);
385
- }
386
- catch (e) {
387
- this.entries.delete(entry.id);
388
- this.order.delete(entry.id);
389
- this.dropVersions(entry.id);
390
- throw e;
391
- }
392
- this.writeRefs(entry);
393
- return this.snapshot(entry);
394
- }
395
- async get(collection, id) {
396
- const entry = this.row(collection, id);
397
- return entry ? this.snapshot(entry) : null;
398
- }
399
- async list(collection, options) {
400
- const schema = this.schema(collection);
401
- let rows = [...this.entries.values()].filter((e) => e.collection === collection);
402
- if (options?.status || options?.locale !== undefined) {
403
- const locale = options.locale ?? this.locales.default;
404
- rows = rows.filter((e) => {
405
- const row = e.locales[locale];
406
- return row !== undefined && (!options.status || row.status === options.status);
407
- });
408
- }
409
- if (options?.parent !== undefined)
410
- rows = rows.filter((e) => e.parentId === options.parent);
411
- if (options?.where)
412
- rows = rows.filter((e) => matchesWhere(e.fields, options.where));
413
- rows.sort(compareEntries(options?.order ?? collectionOrder(schema), (e) => this.order.get(e.id)));
414
- if (typeof options?.limit === "number")
415
- rows = rows.slice(0, options.limit);
416
- return rows.map((e) => this.snapshot(e));
417
- }
418
- async query(collection, q) {
419
- const schema = this.schema(collection);
420
- const wanted = q.ids ? new Set(q.ids) : null;
421
- const rows = [];
422
- for (const e of this.entries.values()) {
423
- if (e.collection !== collection)
424
- continue;
425
- if (wanted && !wanted.has(e.id))
426
- continue;
427
- if (q.parent !== undefined && e.parentId !== q.parent)
428
- continue;
429
- const row = e.locales[q.locale];
430
- if (!row || (!q.draft && row.status !== "published"))
431
- continue;
432
- const snapshot = !q.draft && row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
433
- const fields = snapshot ? structuredClone(snapshot.fields) : entryFieldsIn(e, q.locale);
434
- if (q.where && !matchesWhere(fields, q.where))
435
- continue;
436
- rows.push({ record: this.snapshot(e), fields });
437
- }
438
- rows.sort(compareSnapshots(q.order ?? collectionOrder(schema), q.locale, (id) => this.order.get(id)));
439
- return typeof q.limit === "number" ? rows.slice(0, q.limit) : rows;
440
- }
441
- async getMany(collection, ids, locale, options) {
442
- this.schema(collection);
443
- const out = {};
444
- for (const id of ids) {
445
- const entry = this.row(collection, id);
446
- const row = entry?.locales[locale];
447
- if (!entry || !row)
448
- continue;
449
- if (options?.status) {
450
- if (row.status !== options.status)
451
- continue;
452
- // A versioned collection serves what was published, not the live row.
453
- const snapshot = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
454
- out[id] = snapshot ? structuredClone(snapshot.fields) : entryFieldsIn(entry, locale);
455
- }
456
- else {
457
- out[id] = entryFieldsIn(entry, locale);
458
- }
459
- }
460
- return out;
461
- }
462
- async update(collection, id, locale, fields, by, expected, options) {
463
- const schema = this.schema(collection);
464
- const entry = this.must(collection, id);
465
- const row = entryLocaleRow(entry, locale);
466
- assertFresh(row, expected);
467
- this.ensureLiveSnapshot(entry, locale);
468
- const { shared, localized, slug } = splitFields(fields, schema);
469
- if (isGlobalSchema(schema) && slug !== null)
470
- throw entryErrors.noSlug(collection);
471
- this.assertSiblingSlug(collection, entry.parentId, locale, slug, id);
472
- const before = { fields: entry.fields, row: { ...row } };
473
- entry.fields = keepLocalizedShared(before.fields, shared, schema);
474
- row.fields = localized;
475
- row.slug = slug;
476
- try {
477
- await this.writePaths(collection, this.subtree(entry), options?.redirect ?? true);
478
- }
479
- catch (e) {
480
- entry.fields = before.fields;
481
- Object.assign(row, before.row);
482
- throw e;
483
- }
484
- this.recordVersion(entry, locale);
485
- this.bump(entry, row, by);
486
- this.writeRefs(entry);
487
- return this.snapshot(entry);
488
- }
489
- async setStatus(collection, id, locale, status, by, expected) {
490
- const entry = this.must(collection, id);
491
- const row = entryLocaleRow(entry, locale);
492
- if (status === "published") {
493
- assertFresh(row, expected);
494
- // Publish points at the current draft; a row always has one after its first write, but never trust that from here.
495
- if (!row.draftVersionId)
496
- this.recordVersion(entry, locale);
497
- row.publishedVersionId = row.draftVersionId;
498
- }
499
- // Unpublish keeps the pointer for a later republish (spec §2).
500
- row.status = status;
501
- this.bump(entry, row, by);
502
- this.writeRefs(entry); // the published snapshot just became, or stopped being, a live set
503
- return this.snapshot(entry);
504
- }
505
- async discardDraft(collection, id, locale, by, expected) {
506
- const schema = this.schema(collection);
507
- const entry = this.must(collection, id);
508
- const row = entryLocaleRow(entry, locale);
509
- assertFresh(row, expected);
510
- const snapshot = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
511
- if (!snapshot)
512
- throw entryErrors.nothingPublished(collection, id, locale);
513
- const { shared, localized } = splitFields(snapshot.fields, schema);
514
- entry.fields = keepLocalizedShared(entry.fields, shared, schema);
515
- row.fields = localized;
516
- const abandoned = row.draftVersionId;
517
- row.draftVersionId = row.publishedVersionId;
518
- row.draftSavedAt = String(snapshot.savedAt);
519
- if (abandoned && abandoned !== row.publishedVersionId) {
520
- this.versions.delete(abandoned);
521
- this.versionSeq.delete(abandoned);
522
- }
523
- this.bump(entry, row, by);
524
- this.writeRefs(entry);
525
- return this.snapshot(entry);
526
- }
527
- async addLocale(collection, id, locale, options, by) {
528
- assertSupportedLocale(locale, this.locales);
529
- const entry = this.must(collection, id);
530
- // The source is read FIRST: "copy from a locale that isn't there" is
531
- // the more specific complaint, even when the target locale also exists.
532
- const source = entryLocaleRow(entry, options.from);
533
- if (entry.locales[locale])
534
- throw entryErrors.alreadyInLocale(id, locale);
535
- const parent = entry.parentId === null ? null : (this.entries.get(entry.parentId) ?? null);
536
- this.assertParentHasLocale(parent, locale);
537
- this.assertSiblingSlug(collection, entry.parentId, locale, source.slug, id);
538
- const now = Date.now();
539
- entry.locales[locale] = {
540
- slug: source.slug, status: "draft", fields: structuredClone(source.fields),
541
- draftVersionId: null, draftSavedAt: null, publishedVersionId: null, updatedAt: now, updatedBy: by ?? null,
542
- };
543
- this.recordVersion(entry, locale);
544
- // Only this node's rows change: a child cannot have a locale its parent lacked.
545
- try {
546
- await this.writePaths(collection, [entry]);
547
- }
548
- catch (e) {
549
- delete entry.locales[locale];
550
- this.dropVersions(id, locale);
551
- throw e;
552
- }
553
- this.writeRefs(entry);
554
- return this.snapshot(entry);
555
- }
556
- async removeLocale(collection, id, locale) {
557
- const schema = this.schema(collection);
558
- if (isGlobalSchema(schema))
559
- throw entryErrors.everyLocale(collection);
560
- const entry = this.must(collection, id);
561
- const row = entryLocaleRow(entry, locale);
562
- if (Object.keys(entry.locales).length === 1)
563
- throw entryErrors.lastLocale(id);
564
- const blockers = [...this.entries.values()]
565
- .filter((e) => e.parentId === id && e.locales[locale] !== undefined)
566
- .map((e) => entryTitleIn(e, locale, schema));
567
- if (blockers.length > 0)
568
- throw entryErrors.localeInUse(locale, blockers);
569
- delete entry.locales[locale];
570
- try {
571
- await this.writePaths(collection, [entry], false);
572
- }
573
- catch (e) {
574
- entry.locales[locale] = row;
575
- throw e;
576
- }
577
- this.dropVersions(id, locale);
578
- this.bump(entry);
579
- this.writeRefs(entry);
580
- return this.snapshot(entry);
581
- }
582
- async scanEntries() {
583
- return [...this.entries.values()].map((entry) => ({
584
- record: this.snapshot(entry),
585
- live: Object.fromEntries(Object.entries(entry.locales).map(([l, row]) => {
586
- if (row.status !== "published")
587
- return [l, null];
588
- // A legacy published row without a snapshot serves its live row (spec 2026-09-14 §1).
589
- const snap = row.publishedVersionId ? this.versions.get(row.publishedVersionId) : undefined;
590
- return [l, snap ? structuredClone(snap.fields) : entryFieldsIn(entry, l)];
591
- })),
592
- }));
593
- }
594
- async listVersions(collection, id, locale) {
595
- this.must(collection, id);
596
- return [...this.versions.values()]
597
- .filter((v) => v.entryId === id && v.locale === locale)
598
- .sort((a, b) => b.savedAt - a.savedAt || this.versionSeq.get(b.id) - this.versionSeq.get(a.id))
599
- .map((v) => structuredClone(v));
600
- }
601
- async restore(collection, id, locale, versionId, by, expected) {
602
- const schema = this.schema(collection);
603
- const entry = this.must(collection, id);
604
- const row = entryLocaleRow(entry, locale);
605
- const version = this.versions.get(versionId);
606
- if (!version || version.entryId !== id || version.locale !== locale)
607
- throw entryErrors.foreignVersion(collection, id, locale, versionId);
608
- assertFresh(row, expected);
609
- if (versionId === row.publishedVersionId)
610
- return this.discardDraft(collection, id, locale, by, expected);
611
- const { shared, localized } = splitFields(version.fields, schema);
612
- entry.fields = keepLocalizedShared(entry.fields, shared, schema);
613
- row.fields = localized; // the slug is the row's own and never versioned
614
- this.recordVersion(entry, locale);
615
- this.bump(entry, row, by);
616
- this.writeRefs(entry);
617
- return this.snapshot(entry);
618
- }
619
- async getVersion(collection, versionId) {
620
- this.schema(collection);
621
- const v = this.versions.get(versionId);
622
- return v ? structuredClone(v) : null;
623
- }
624
- async move(collection, id, to, options) {
625
- if (isGlobalSchema(this.schema(collection)))
626
- throw entryErrors.owned(collection, "it has no tree position");
627
- const entry = this.must(collection, id);
628
- const subtree = this.subtree(entry);
629
- if (to.parentId !== null && (to.parentId === id || subtree.some((e) => e.id === to.parentId)))
630
- throw entryErrors.cycle();
631
- const height = Math.max(...subtree.map((e) => chainOf(e, this.byId).length)) - chainOf(entry, this.byId).length + 1;
632
- const parent = this.assertParent(collection, to.parentId, height);
633
- // The destination must have every locale the entry has, or a chain breaks there.
634
- if (parent)
635
- for (const locale of Object.keys(entry.locales))
636
- if (!parent.locales[locale])
637
- throw entryErrors.moveLocales(locale);
638
- if (to.parentId !== entry.parentId) {
639
- for (const [locale, row] of Object.entries(entry.locales))
640
- this.assertSiblingSlug(collection, to.parentId, locale, row.slug, id);
641
- }
642
- const before = { parentId: entry.parentId, sort: entry.sort };
643
- entry.parentId = to.parentId;
644
- const siblings = this.siblings(collection, to.parentId, id);
645
- const index = Math.min(Math.max(to.index ?? siblings.length, 0), siblings.length);
646
- siblings.splice(index, 0, entry);
647
- const previous = new Map(siblings.map((s) => [s.id, s.sort]));
648
- siblings.forEach((s, i) => (s.sort = i + 1));
649
- try {
650
- await this.writePaths(collection, subtree, options?.redirect ?? true);
651
- }
652
- catch (e) {
653
- Object.assign(entry, before);
654
- for (const s of siblings)
655
- s.sort = previous.get(s.id) ?? null;
656
- throw e;
657
- }
658
- return this.snapshot(entry);
659
- }
660
- async delete(collection, id) {
661
- const entry = this.row(collection, id);
662
- if (!entry)
663
- return;
664
- if (isGlobalSchema(this.schema(collection)))
665
- throw entryErrors.owned(collection, "remove the registration to remove it");
666
- if ([...this.entries.values()].some((e) => e.parentId === id))
667
- throw entryErrors.hasChildren(collection, id);
668
- const usage = await this.referencesTo(collection, id);
669
- if (usage.length > 0) {
670
- throw new Error(`smoodly: "${collection}/${id}" is referenced by ${usage.length} document(s) — remove those references first.`);
671
- }
672
- await this.paths.remove([{ kind: "entry", id }]);
673
- this.entries.delete(id);
674
- this.order.delete(id);
675
- this.dropVersions(id);
676
- this.refs.remove({ sourceKind: "entry", sourceId: id });
677
- }
678
- async reorder(collection, orderedIds) {
679
- if (isGlobalSchema(this.schema(collection)))
680
- throw entryErrors.owned(collection, "it has no order");
681
- orderedIds.forEach((id, i) => {
682
- const entry = this.row(collection, id);
683
- if (entry)
684
- entry.sort = i + 1; // deliberately NOT touching updatedAt
685
- });
686
- }
687
- async referencesTo(collection, id) {
688
- return this.refs.to(collection, id);
689
- }
690
- async incoming(targetId) {
691
- return this.refs.incoming(targetId);
692
- }
693
- resolvePath(locale, path) {
694
- return this.paths.resolve(locale, path);
695
- }
696
- pathsIn(collection, ids, locale) {
697
- this.schema(collection);
698
- return this.paths.livePathsIn("entry", ids, locale);
699
- }
700
- pathsOf(collection, id) {
701
- return this.row(collection, id) ? this.paths.pathsOf({ kind: "entry", id }) : Promise.resolve({});
702
- }
703
- async redirectsOf(collection, id) {
704
- this.schema(collection);
705
- return groupRedirects(await this.paths.redirectsOf({ kind: "entry", id }));
706
- }
707
- }
package/dist/env.d.ts CHANGED
@@ -1,20 +1,27 @@
1
- import { readCurrentSpace } from "./current-space.ts";
2
1
  export type SmoodlyEnv = {
3
- /** The content project. */
4
- url: string;
5
- /** Server-only: the service-role key (self-host) or the environment's write key (cloud). */
2
+ /** Server-only: the space's secret key (sk_…), minted by Smoodly's API. */
6
3
  secretKey: string;
7
- /** What a browser may hold for the content project: anon/publishable key, or the cloud's per-environment key. It reads no content (sign-in and presence only). "" on a live site that runs without the admin. */
4
+ /** The space's publishable key (pk_…), what a browser may hold: it reads published content only. "" when unset. */
8
5
  publishableKey: string;
9
- /** Where editors sign in. Defaults to `url`. */
10
- authUrl: string;
11
- /** That project's publishable key. Defaults to `publishableKey`. */
12
- authKey: string;
6
+ /** Smoodly's API, without a trailing slash: the site reads there and the admin posts its operations there
7
+ * (spec 2026-10-14 §1). */
8
+ apiUrl: string;
9
+ /** The space the site's keys belong to (SMOODLY_SPACE_ID). */
10
+ spaceId: string;
11
+ };
12
+ /** Who asks: a site reading over the API, or its admin. */
13
+ export type SmoodlyEnvNeed = "site" | "admin";
14
+ /** Smoodly's hosted API; SMOODLY_API_URL is set only for another address (the repo's loop, a staging service). */
15
+ export declare const DEFAULT_API_URL = "https://api.smoodly.io";
16
+ /** The live site and its admin read over the API with its address, the space and the secret key (spec 2026-10-16 the
17
+ * control plane §5). A key in the wrong variable or of no known form is named here, not by a failed read later. */
18
+ export declare function resolveSmoodlyEnv(env: Record<string, string | undefined>, need?: SmoodlyEnvNeed): SmoodlyEnv;
19
+ /** What the API client is built from (createSmoodlyApi): the site's env, the secret key as the key. */
20
+ export declare function apiEnv(env?: Record<string, string | undefined>): {
21
+ apiUrl: string;
22
+ spaceId: string;
23
+ key: string;
13
24
  };
14
- /** The live site reads with the URL and the secret key; the admin and presence also need the
15
- * publishable key, so their callers pass "admin" (spec 2026-09-30 tester findings §4). A key
16
- * in the wrong variable or of no known form is named here, not by a failed read later. */
17
- export declare function resolveSmoodlyEnv(env: Record<string, string | undefined>, need?: "site" | "admin"): SmoodlyEnv;
18
25
  /** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
19
26
  * image links are absolute. A value without a scheme is named here, not by a bare "Invalid URL" on
20
27
  * every page; an unset one falls back to localhost and warns once in production, where it would
@@ -22,13 +29,13 @@ export declare function resolveSmoodlyEnv(env: Record<string, string | undefined
22
29
  export declare function siteUrl(env?: Record<string, string | undefined>): URL;
23
30
  /** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
24
31
  * so `next build` without env still succeeds. */
25
- export declare const smoodlyEnv: (need?: "site" | "admin") => SmoodlyEnv;
26
- /** What the browser needs to join the admin's presence channel: the content project's URL, publishable key and space, public values only — never for content reads (spec 2026-09-29 content API contract §8). */
32
+ export declare const smoodlyEnv: (need?: SmoodlyEnvNeed) => SmoodlyEnv;
33
+ /** What the browser needs to open the admin's presence socket: Smoodly's API and the space, public values only
34
+ * (spec 2026-10-15 the space object §6). The socket opens with a one-time ticket the service signs from the editor's
35
+ * pass (spec 2026-10-16 sign-in and the dashboard §3). */
27
36
  export type PresenceConfig = {
28
- url: string;
29
- key: string;
37
+ apiUrl: string;
30
38
  spaceId: string;
31
39
  };
32
- /** The presence config, with the space the database says the secret key acts in —
33
- * asked, never read out of the key, so it cannot disagree with where rows land. */
34
- export declare function presenceConfig(env: SmoodlyEnv, db?: Parameters<typeof readCurrentSpace>[0]): Promise<PresenceConfig>;
40
+ /** The presence config from the env: no lookup, the space is the site's own. */
41
+ export declare function presenceConfig(env: SmoodlyEnv): PresenceConfig;