smoodly 0.0.51 → 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 +22 -11
- 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 +7 -7
- package/dist/reads.js +3 -0
- 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 -1403
- 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/entry-store.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
4
|
+
/** The space's publishable key (pk_…), what a browser may hold: it reads published content only. "" when unset. */
|
|
8
5
|
publishableKey: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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?:
|
|
26
|
-
/** What the browser needs to
|
|
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
|
-
|
|
29
|
-
key: string;
|
|
37
|
+
apiUrl: string;
|
|
30
38
|
spaceId: string;
|
|
31
39
|
};
|
|
32
|
-
/** The presence config
|
|
33
|
-
|
|
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;
|