smoodly 0.0.24 → 0.0.26
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 +214 -40
- package/bin/smoodly.js +6 -0
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/editor/EditorView.js +100 -22
- package/dist/admin/editor/InsertPicker.d.ts +14 -0
- package/dist/admin/editor/InsertPicker.js +37 -7
- package/dist/admin/editor/InspectorFields.js +1 -1
- package/dist/admin/editor/Outline.js +2 -2
- package/dist/admin/editor/PageSettings.js +1 -1
- package/dist/admin/editor/SeoSettings.js +1 -1
- package/dist/admin/editor/autosave.d.ts +4 -1
- package/dist/admin/editor/autosave.js +24 -1
- package/dist/admin/editor/global-edit.d.ts +5 -1
- package/dist/admin/editor/global-edit.js +15 -5
- package/dist/admin/editor/locale-url.d.ts +7 -4
- package/dist/admin/editor/locale-url.js +14 -7
- package/dist/admin/editor/useGlobalEdit.d.ts +1 -1
- package/dist/admin/editor/useGlobalEdit.js +1 -1
- package/dist/admin/editor/usePageLocale.js +9 -1
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +6 -3
- package/dist/admin/forms/DatePicker.d.ts +9 -0
- package/dist/admin/forms/DatePicker.js +133 -0
- package/dist/admin/forms/FieldWidget.js +11 -5
- package/dist/admin/forms/ImagePreview.d.ts +9 -0
- package/dist/admin/forms/ImagePreview.js +11 -0
- package/dist/admin/forms/MediaWidget.d.ts +8 -2
- package/dist/admin/forms/MediaWidget.js +13 -13
- package/dist/admin/forms/RichtextElementCard.js +1 -1
- package/dist/admin/forms/RichtextImageCard.js +14 -9
- package/dist/admin/forms/date-picker.d.ts +44 -0
- package/dist/admin/forms/date-picker.js +117 -0
- package/dist/admin/forms/elements-context.d.ts +3 -0
- package/dist/admin/forms/elements-context.js +7 -2
- package/dist/admin/forms/time-zone-context.d.ts +7 -0
- package/dist/admin/forms/time-zone-context.js +15 -0
- package/dist/admin/forms/useRichtextEditor.js +10 -1
- package/dist/admin/next/bridge.d.ts +2 -1
- package/dist/admin/next/bridge.js +35 -5
- package/dist/admin/next/create-admin.d.ts +4 -0
- package/dist/admin/next/create-admin.js +3 -1
- package/dist/admin/next/index.d.ts +1 -0
- package/dist/admin/ops-impl.js +74 -24
- package/dist/admin/ops.d.ts +31 -12
- package/dist/admin/serialize.d.ts +2 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.d.ts +3 -1
- package/dist/admin/shell/AdminApp.js +42 -2
- package/dist/admin/shell/EntriesList.js +17 -22
- package/dist/admin/shell/EntryForm.js +91 -13
- package/dist/admin/shell/GlobalForm.js +68 -9
- package/dist/admin/shell/GlobalList.js +10 -20
- package/dist/admin/shell/IconRail.js +14 -5
- package/dist/admin/shell/InviteModal.js +9 -5
- package/dist/admin/shell/ListView.js +2 -2
- package/dist/admin/shell/MediaScreen.js +11 -1
- package/dist/admin/shell/MembersPage.js +33 -15
- package/dist/admin/shell/PageDialogs.d.ts +26 -0
- package/dist/admin/shell/PageDialogs.js +38 -0
- package/dist/admin/shell/PagesList.js +101 -74
- package/dist/admin/shell/RouteLink.js +1 -1
- package/dist/admin/shell/SchemaNotice.d.ts +4 -0
- package/dist/admin/shell/SchemaNotice.js +4 -0
- package/dist/admin/shell/SettingsView.js +11 -26
- package/dist/admin/shell/live-url.d.ts +4 -3
- package/dist/admin/shell/live-url.js +12 -3
- package/dist/admin/shell/locale-memory.d.ts +8 -0
- package/dist/admin/shell/locale-memory.js +23 -0
- package/dist/admin/shell/members-list.d.ts +2 -0
- package/dist/admin/shell/members-list.js +5 -0
- package/dist/admin/shell/page-dialogs.d.ts +9 -0
- package/dist/admin/shell/page-dialogs.js +19 -0
- package/dist/admin/shell/presence-context.d.ts +9 -0
- package/dist/admin/shell/presence-context.js +35 -0
- package/dist/admin/shell/presence-realtime.d.ts +3 -0
- package/dist/admin/shell/presence-realtime.js +22 -0
- package/dist/admin/shell/presence.d.ts +51 -0
- package/dist/admin/shell/presence.js +90 -0
- package/dist/admin/shell/router.d.ts +10 -7
- package/dist/admin/shell/router.js +18 -9
- package/dist/admin/shell/settings.d.ts +0 -6
- package/dist/admin/shell/settings.js +4 -5
- package/dist/admin/shell/stale.d.ts +28 -0
- package/dist/admin/shell/stale.js +17 -0
- package/dist/admin/tree-ops.js +3 -0
- package/dist/admin/ui/ActionMenu.js +26 -6
- package/dist/admin/ui/Combobox.d.ts +5 -0
- package/dist/admin/ui/Combobox.js +1 -1
- package/dist/admin/ui/ConfirmDialog.d.ts +8 -0
- package/dist/admin/ui/ConfirmDialog.js +30 -0
- package/dist/admin/ui/Dialog.d.ts +13 -0
- package/dist/admin/ui/Dialog.js +54 -0
- package/dist/admin/ui/HistoryModal.d.ts +2 -1
- package/dist/admin/ui/HistoryModal.js +7 -2
- package/dist/admin/ui/LockIcon.d.ts +4 -0
- package/dist/admin/ui/LockIcon.js +12 -0
- package/dist/admin/ui/PresenceLine.d.ts +6 -0
- package/dist/admin/ui/PresenceLine.js +11 -0
- package/dist/admin/ui/ResizeHandle.js +4 -4
- package/dist/admin/ui/StaleBanner.d.ts +5 -0
- package/dist/admin/ui/StaleBanner.js +9 -0
- package/dist/admin/ui/confirm.d.ts +35 -0
- package/dist/admin/ui/confirm.js +57 -0
- package/dist/admin/ui/primitives.d.ts +19 -4
- package/dist/admin/ui/primitives.js +12 -5
- package/dist/admin/ui/resize.d.ts +7 -4
- package/dist/admin/ui/resize.js +7 -4
- package/dist/admin/ui/theme.d.ts +1 -1
- package/dist/admin/ui/theme.js +373 -204
- package/dist/admin/validate.d.ts +4 -0
- package/dist/admin/validate.js +28 -0
- package/dist/cli/load-collections.d.ts +2 -0
- package/dist/cli/load-collections.js +26 -0
- package/dist/cli/main.d.ts +13 -0
- package/dist/cli/main.js +62 -0
- package/dist/cli/migrate.d.ts +23 -0
- package/dist/cli/migrate.js +64 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +9 -0
- package/dist/draft-token.d.ts +31 -0
- package/dist/draft-token.js +41 -0
- package/dist/entry-store.d.ts +11 -8
- package/dist/entry-store.js +24 -10
- package/dist/env.d.ts +9 -0
- package/dist/env.js +19 -0
- package/dist/fields.d.ts +2 -0
- package/dist/fields.js +2 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/marks.d.ts +3 -0
- package/dist/marks.js +3 -0
- package/dist/migrations.d.ts +37 -0
- package/dist/migrations.js +73 -0
- package/dist/schema-version.d.ts +17 -0
- package/dist/schema-version.js +32 -0
- package/dist/sql-space.js +17 -0
- package/dist/sql.js +21 -0
- package/dist/store.d.ts +14 -8
- package/dist/store.js +30 -8
- package/dist/supabase-entry-store.d.ts +15 -5
- package/dist/supabase-entry-store.js +87 -22
- package/dist/supabase-store.d.ts +18 -5
- package/dist/supabase-store.js +83 -20
- package/dist/time-zone.d.ts +6 -0
- package/dist/time-zone.js +27 -0
- package/migrations/20260926120000_smoodly_v1.sql +535 -0
- package/migrations/20260926130000_smoodly_v2.sql +26 -0
- package/package.json +13 -3
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type { Crop, Hotspot, AssetKind, ImageValue, VideoValue, FileValue, Asset
|
|
|
27
27
|
export { z } from "./zones.ts";
|
|
28
28
|
export { defineConfig } from "./config.ts";
|
|
29
29
|
export { collectionSQL, coreTablesSQL } from "./sql.ts";
|
|
30
|
+
export { SCHEMA_VERSION, SCHEMA_VERSION_KEY, schemaVersionSQL, coreFileVersion, schemaBehind, schemaBehindMessage } from "./schema-version.ts";
|
|
31
|
+
export { draftToken, sameToken, isDraftToken, assertFresh, StaleDraftError } from "./draft-token.ts";
|
|
32
|
+
export type { DraftToken, DraftPointerRow } from "./draft-token.ts";
|
|
30
33
|
export { MemoryPageStore } from "./store.ts";
|
|
31
34
|
export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
|
|
32
35
|
export type { AssetStore, UploadTicket, FinishInput, AssetQuery, AssetPage, BeginUploadInput, CreateAssetInput } from "./asset-store.ts";
|
|
@@ -150,10 +153,10 @@ export declare const smoodly: {
|
|
|
150
153
|
Section: typeof Section;
|
|
151
154
|
marks: typeof marks;
|
|
152
155
|
};
|
|
153
|
-
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
156
|
+
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
154
157
|
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
155
|
-
export { resolveSmoodlyEnv, smoodlyEnv } from "./env.ts";
|
|
156
|
-
export type { SmoodlyEnv } from "./env.ts";
|
|
158
|
+
export { resolveSmoodlyEnv, smoodlyEnv, publicContentConfig, spaceIdOfKey } from "./env.ts";
|
|
159
|
+
export type { SmoodlyEnv, ContentConfig } from "./env.ts";
|
|
157
160
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
|
158
161
|
export { createSmoodlySite, pageContextOf } from "./site.ts";
|
|
159
162
|
export type { SiteOptions, SitePage, SitePageSchema, SiteEntry, SiteHit, SmoodlySite, LoadOptions, PageTreeItem, SiteEntryItem, EntryTreeItem, EntryQuery, } from "./site.ts";
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT
|
|
|
21
21
|
export { z } from "./zones.js";
|
|
22
22
|
export { defineConfig } from "./config.js";
|
|
23
23
|
export { collectionSQL, coreTablesSQL } from "./sql.js";
|
|
24
|
+
export { SCHEMA_VERSION, SCHEMA_VERSION_KEY, schemaVersionSQL, coreFileVersion, schemaBehind, schemaBehindMessage } from "./schema-version.js";
|
|
25
|
+
export { draftToken, sameToken, isDraftToken, assertFresh, StaleDraftError } from "./draft-token.js";
|
|
24
26
|
export { MemoryPageStore } from "./store.js";
|
|
25
27
|
export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.js";
|
|
26
28
|
export { imageDimensions } from "./image-dimensions.js";
|
|
@@ -61,7 +63,7 @@ export const smoodly = {
|
|
|
61
63
|
Section,
|
|
62
64
|
marks,
|
|
63
65
|
};
|
|
64
|
-
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
65
|
-
export { resolveSmoodlyEnv, smoodlyEnv } from "./env.js";
|
|
66
|
+
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
67
|
+
export { resolveSmoodlyEnv, smoodlyEnv, publicContentConfig, spaceIdOfKey } from "./env.js";
|
|
66
68
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
67
69
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
package/dist/marks.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export declare const NODE_ATTR = "data-smd-node";
|
|
|
4
4
|
/** An empty zone's placeholder box, from `<Zone>` in editor mode. */
|
|
5
5
|
export declare const ZONE_ATTR = "data-smd-zone";
|
|
6
6
|
export declare const EMPTY_ATTR = "data-smd-empty";
|
|
7
|
+
/** A section that renders no text and no media, stamped by the bridge in
|
|
8
|
+
* editor mode so the canvas can give it a height to see and select. */
|
|
9
|
+
export declare const BLANK_ATTR = "data-smd-blank";
|
|
7
10
|
/** The element that renders a field's value verbatim. */
|
|
8
11
|
export declare const FIELD_ATTR = "data-smd-field";
|
|
9
12
|
/** A list item's or object field's root: scopes the field names inside it. */
|
package/dist/marks.js
CHANGED
|
@@ -3,6 +3,9 @@ export const NODE_ATTR = "data-smd-node";
|
|
|
3
3
|
/** An empty zone's placeholder box, from `<Zone>` in editor mode. */
|
|
4
4
|
export const ZONE_ATTR = "data-smd-zone";
|
|
5
5
|
export const EMPTY_ATTR = "data-smd-empty";
|
|
6
|
+
/** A section that renders no text and no media, stamped by the bridge in
|
|
7
|
+
* editor mode so the canvas can give it a height to see and select. */
|
|
8
|
+
export const BLANK_ATTR = "data-smd-blank";
|
|
6
9
|
/** The element that renders a field's value verbatim. */
|
|
7
10
|
export const FIELD_ATTR = "data-smd-field";
|
|
8
11
|
/** A list item's or object field's root: scopes the field names inside it. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type MigrationFile = {
|
|
2
|
+
name: string;
|
|
3
|
+
text: string;
|
|
4
|
+
};
|
|
5
|
+
export type CollectionSQL = {
|
|
6
|
+
name: string;
|
|
7
|
+
sql: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const MARKER_PREFIX = "-- smoodly:collection ";
|
|
10
|
+
/** The first 16 hex chars of sha256 over the exact emitted SQL. */
|
|
11
|
+
export declare const collectionHash: (sql: string) => string;
|
|
12
|
+
export declare const collectionMarker: (c: CollectionSQL) => string;
|
|
13
|
+
/** name → hash, the LAST marker per collection across the files in name order. */
|
|
14
|
+
export declare function parseMarkers(files: MigrationFile[]): Map<string, string>;
|
|
15
|
+
/** UTC yyyyMMddHHmmss — the Supabase CLI's file-name prefix. */
|
|
16
|
+
export declare const stamp: (at: Date) => string;
|
|
17
|
+
/** A views file must sort after every core file it may depend on and
|
|
18
|
+
* after any earlier views file it supersedes: the later of `now` and the
|
|
19
|
+
* newest own stamp plus a minute. */
|
|
20
|
+
export declare function viewsStamp(names: string[], now: Date): Date;
|
|
21
|
+
/** One file with a block per new or changed collection; null when none changed. */
|
|
22
|
+
export declare function viewsFile(collections: CollectionSQL[], existing: Map<string, string>, at: Date): MigrationFile | null;
|
|
23
|
+
export type CorePending = {
|
|
24
|
+
copy: MigrationFile[];
|
|
25
|
+
modified: string[];
|
|
26
|
+
};
|
|
27
|
+
/** Package files the app lacks (copy) and same-named files whose text differs (modified — never overwritten). */
|
|
28
|
+
export declare function pendingCoreFiles(packageFiles: MigrationFile[], appFiles: MigrationFile[]): CorePending;
|
|
29
|
+
export type MigratePlan = CorePending & {
|
|
30
|
+
views: MigrationFile | null;
|
|
31
|
+
};
|
|
32
|
+
export declare function planMigrate(input: {
|
|
33
|
+
packageFiles: MigrationFile[];
|
|
34
|
+
appFiles: MigrationFile[];
|
|
35
|
+
collections: CollectionSQL[];
|
|
36
|
+
}, now: Date): MigratePlan;
|
|
37
|
+
export declare const planIsEmpty: (plan: MigratePlan) => boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// packages/smoodly/src/migrations.ts
|
|
2
|
+
// What `npx smoodly migrate` decides, as pure functions over file lists
|
|
3
|
+
// (spec 2026-09-26 §2–§3): which package files the app lacks, which
|
|
4
|
+
// collections need a fresh view block, and the one views file to write.
|
|
5
|
+
// Node-only (crypto): exposed at `smoodly/migrations`, never from the
|
|
6
|
+
// root index, which the client bundle reaches through the config.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
export const MARKER_PREFIX = "-- smoodly:collection ";
|
|
9
|
+
const MARKER_RE = /^-- smoodly:collection (\S+) sha256:([0-9a-f]+)\s*$/gm;
|
|
10
|
+
const OWN_FILE_RE = /^(\d{14})_smoodly_(v\d+|views)\.sql$/;
|
|
11
|
+
/** The first 16 hex chars of sha256 over the exact emitted SQL. */
|
|
12
|
+
export const collectionHash = (sql) => createHash("sha256").update(sql).digest("hex").slice(0, 16);
|
|
13
|
+
export const collectionMarker = (c) => `${MARKER_PREFIX}${c.name} sha256:${collectionHash(c.sql)}`;
|
|
14
|
+
/** name → hash, the LAST marker per collection across the files in name order. */
|
|
15
|
+
export function parseMarkers(files) {
|
|
16
|
+
const out = new Map();
|
|
17
|
+
for (const file of [...files].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
18
|
+
for (const m of file.text.matchAll(MARKER_RE))
|
|
19
|
+
out.set(m[1], m[2]);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** UTC yyyyMMddHHmmss — the Supabase CLI's file-name prefix. */
|
|
24
|
+
export const stamp = (at) => at.toISOString().replace(/\D/g, "").slice(0, 14);
|
|
25
|
+
const stampDate = (s) => new Date(Date.UTC(+s.slice(0, 4), +s.slice(4, 6) - 1, +s.slice(6, 8), +s.slice(8, 10), +s.slice(10, 12), +s.slice(12, 14)));
|
|
26
|
+
/** A views file must sort after every core file it may depend on and
|
|
27
|
+
* after any earlier views file it supersedes: the later of `now` and the
|
|
28
|
+
* newest own stamp plus a minute. */
|
|
29
|
+
export function viewsStamp(names, now) {
|
|
30
|
+
let at = now;
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
const m = OWN_FILE_RE.exec(name);
|
|
33
|
+
if (!m)
|
|
34
|
+
continue;
|
|
35
|
+
const after = new Date(stampDate(m[1]).getTime() + 60000);
|
|
36
|
+
if (after > at)
|
|
37
|
+
at = after;
|
|
38
|
+
}
|
|
39
|
+
return at;
|
|
40
|
+
}
|
|
41
|
+
/** One file with a block per new or changed collection; null when none changed. */
|
|
42
|
+
export function viewsFile(collections, existing, at) {
|
|
43
|
+
const changed = collections.filter((c) => existing.get(c.name) !== collectionHash(c.sql));
|
|
44
|
+
if (changed.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const text = "-- Smoodly: read views and order indexes for this app's collections, written by `npx smoodly migrate`\n" +
|
|
47
|
+
`-- (${at.toISOString()}). One block per collection; the marker's hash is the block's SQL, compared on the next run.\n` +
|
|
48
|
+
"-- Never edit a written file: change the collection and run migrate again.\n\n" +
|
|
49
|
+
changed.map((c) => `${collectionMarker(c)}\n${c.sql.trim()}`).join("\n\n") +
|
|
50
|
+
"\n";
|
|
51
|
+
return { name: `${stamp(at)}_smoodly_views.sql`, text };
|
|
52
|
+
}
|
|
53
|
+
/** Package files the app lacks (copy) and same-named files whose text differs (modified — never overwritten). */
|
|
54
|
+
export function pendingCoreFiles(packageFiles, appFiles) {
|
|
55
|
+
const app = new Map(appFiles.map((f) => [f.name, f.text]));
|
|
56
|
+
const copy = [];
|
|
57
|
+
const modified = [];
|
|
58
|
+
for (const file of [...packageFiles].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
59
|
+
const have = app.get(file.name);
|
|
60
|
+
if (have === undefined)
|
|
61
|
+
copy.push(file);
|
|
62
|
+
else if (have !== file.text)
|
|
63
|
+
modified.push(file.name);
|
|
64
|
+
}
|
|
65
|
+
return { copy, modified };
|
|
66
|
+
}
|
|
67
|
+
export function planMigrate(input, now) {
|
|
68
|
+
const core = pendingCoreFiles(input.packageFiles, input.appFiles);
|
|
69
|
+
const names = [...input.packageFiles, ...input.appFiles].map((f) => f.name);
|
|
70
|
+
const views = viewsFile(input.collections, parseMarkers(input.appFiles), viewsStamp(names, now));
|
|
71
|
+
return { ...core, views };
|
|
72
|
+
}
|
|
73
|
+
export const planIsEmpty = (plan) => plan.copy.length === 0 && plan.modified.length === 0 && plan.views === null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
2
|
+
* that adds a table, column, function or policy the package reads; a
|
|
3
|
+
* unit test holds it equal to the highest file in `migrations/`. */
|
|
4
|
+
export declare const SCHEMA_VERSION = 2;
|
|
5
|
+
export declare const SCHEMA_VERSION_KEY = "schema_version";
|
|
6
|
+
export declare const CORE_FILE_RE: RegExp;
|
|
7
|
+
export declare const VIEWS_FILE_RE: RegExp;
|
|
8
|
+
/** The version a core file leaves behind, from its name; null for anything else. */
|
|
9
|
+
export declare function coreFileVersion(name: string): number | null;
|
|
10
|
+
/** The last statement of every core file. */
|
|
11
|
+
export declare function schemaVersionSQL(version: number): string;
|
|
12
|
+
/** Behind blocks the admin and warns on the site (§5). Null = no
|
|
13
|
+
* `smoodly_meta` table, a database older than version 1. Ahead is the
|
|
14
|
+
* normal state of a cloud app between our migration and its package
|
|
15
|
+
* bump, and is fine. */
|
|
16
|
+
export declare const schemaBehind: (database: number | null, required?: number) => boolean;
|
|
17
|
+
export declare function schemaBehindMessage(database: number | null, required?: number): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// packages/smoodly/src/schema-version.ts
|
|
2
|
+
// The schema version this package needs (spec 2026-09-26 §1) and the two
|
|
3
|
+
// things every side agrees on: the `smoodly_meta` row that records a
|
|
4
|
+
// database's version, and the names of the files the package ships. Pure
|
|
5
|
+
// and framework-free — the SQL emitters, the CLI, the stores, the admin
|
|
6
|
+
// op and the shell all read it from here.
|
|
7
|
+
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
8
|
+
* that adds a table, column, function or policy the package reads; a
|
|
9
|
+
* unit test holds it equal to the highest file in `migrations/`. */
|
|
10
|
+
export const SCHEMA_VERSION = 2;
|
|
11
|
+
export const SCHEMA_VERSION_KEY = "schema_version";
|
|
12
|
+
export const CORE_FILE_RE = /^(\d{14})_smoodly_v(\d+)\.sql$/;
|
|
13
|
+
export const VIEWS_FILE_RE = /^(\d{14})_smoodly_views\.sql$/;
|
|
14
|
+
/** The version a core file leaves behind, from its name; null for anything else. */
|
|
15
|
+
export function coreFileVersion(name) {
|
|
16
|
+
const m = CORE_FILE_RE.exec(name);
|
|
17
|
+
return m ? Number(m[2]) : null;
|
|
18
|
+
}
|
|
19
|
+
/** The last statement of every core file. */
|
|
20
|
+
export function schemaVersionSQL(version) {
|
|
21
|
+
return (`insert into "smoodly_meta" ("key", "value") values ('${SCHEMA_VERSION_KEY}', '${version}')\n` +
|
|
22
|
+
` on conflict ("key") do update set "value" = excluded."value";`);
|
|
23
|
+
}
|
|
24
|
+
/** Behind blocks the admin and warns on the site (§5). Null = no
|
|
25
|
+
* `smoodly_meta` table, a database older than version 1. Ahead is the
|
|
26
|
+
* normal state of a cloud app between our migration and its package
|
|
27
|
+
* bump, and is fine. */
|
|
28
|
+
export const schemaBehind = (database, required = SCHEMA_VERSION) => database === null || database < required;
|
|
29
|
+
export function schemaBehindMessage(database, required = SCHEMA_VERSION) {
|
|
30
|
+
return (`smoodly: the database schema is at version ${database ?? "none"}; this Smoodly needs version ${required}. ` +
|
|
31
|
+
"Run `npx smoodly migrate`, then `supabase db push` (hosted) or `supabase db reset` (local).");
|
|
32
|
+
}
|
package/dist/sql-space.js
CHANGED
|
@@ -193,6 +193,23 @@ $$;`.trim());
|
|
|
193
193
|
statements.push(`drop policy if exists "smoodly_read" on "spaces";\n` +
|
|
194
194
|
`create policy "smoodly_read" on "spaces" for select\n` +
|
|
195
195
|
` using (${write});`);
|
|
196
|
+
// The package reads its schema version with the write key (the
|
|
197
|
+
// service role bypasses anyway); nothing else sees the table. No
|
|
198
|
+
// space_id: the schema is one per database (spec 2026-09-26 §1).
|
|
199
|
+
statements.push(`drop policy if exists "smoodly_read" on "smoodly_meta";\n` +
|
|
200
|
+
`create policy "smoodly_read" on "smoodly_meta" for select\n` +
|
|
201
|
+
` using ((select "smoodly_key_kind"()) = 'write' and not (select "smoodly_space_key_revoked"()));`);
|
|
202
|
+
// The editing-presence channel (spec 2026-09-26 concurrency §6): one
|
|
203
|
+
// private Realtime topic per space, `space:<id>`. A caller receives on
|
|
204
|
+
// and sends to its own space's topic only — the anon key resolves to the
|
|
205
|
+
// default space, a cloud read key to its claim. Identical everywhere.
|
|
206
|
+
const topic = `(select realtime.topic()) = 'space:' || (select public."smoodly_current_space"())::text`;
|
|
207
|
+
statements.push(`drop policy if exists "smoodly_presence_read" on "realtime"."messages";\n` +
|
|
208
|
+
`create policy "smoodly_presence_read" on "realtime"."messages" for select\n` +
|
|
209
|
+
` using (${topic});`);
|
|
210
|
+
statements.push(`drop policy if exists "smoodly_presence_write" on "realtime"."messages";\n` +
|
|
211
|
+
`create policy "smoodly_presence_write" on "realtime"."messages" for insert\n` +
|
|
212
|
+
` with check (${topic});`);
|
|
196
213
|
statements.push(storageSQL());
|
|
197
214
|
return statements.join("\n\n");
|
|
198
215
|
}
|
package/dist/sql.js
CHANGED
|
@@ -193,6 +193,17 @@ create table if not exists "entry_versions" (
|
|
|
193
193
|
create index if not exists "entry_versions_entry_locale_idx"
|
|
194
194
|
on "entry_versions" ("entry_id", "locale", "saved_at");
|
|
195
195
|
|
|
196
|
+
-- ── Draft pointers as foreign keys (schema v2, spec 2026-09-26 concurrency §1):
|
|
197
|
+
-- PostgREST embeds the draft version's saved_at through them, so every
|
|
198
|
+
-- locale row read carries the token. Set null on delete: discard moves
|
|
199
|
+
-- the pointer before it deletes the abandoned row, so nothing dangles.
|
|
200
|
+
alter table "page_locales" drop constraint if exists "page_locales_draft_version_fkey";
|
|
201
|
+
alter table "page_locales" add constraint "page_locales_draft_version_fkey"
|
|
202
|
+
foreign key ("draft_version_id") references "page_versions" ("id") on delete set null;
|
|
203
|
+
alter table "entry_locales" drop constraint if exists "entry_locales_draft_version_fkey";
|
|
204
|
+
alter table "entry_locales" add constraint "entry_locales_draft_version_fkey"
|
|
205
|
+
foreign key ("draft_version_id") references "entry_versions" ("id") on delete set null;
|
|
206
|
+
|
|
196
207
|
-- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
|
|
197
208
|
-- statement. Deliberately does not touch updated_at — a drag must not
|
|
198
209
|
-- make the whole collection look freshly edited. Scoped to the current
|
|
@@ -286,6 +297,15 @@ $$;
|
|
|
286
297
|
-- to anything but a write key. The generated per-collection views
|
|
287
298
|
-- (collectionSQL) run with security_invoker = true, so they are subject
|
|
288
299
|
-- to this same smoodly_read policy rather than their owner's privileges.
|
|
300
|
+
-- smoodly_meta: which version of this schema the database holds (spec
|
|
301
|
+
-- 2026-09-26 §1). One row per database, not per space; every core
|
|
302
|
+
-- migration file upserts the version row as its last statement, and the
|
|
303
|
+
-- package compares it with SCHEMA_VERSION (behind blocks the admin).
|
|
304
|
+
create table if not exists "smoodly_meta" (
|
|
305
|
+
"key" text primary key,
|
|
306
|
+
"value" text not null
|
|
307
|
+
);
|
|
308
|
+
|
|
289
309
|
alter table "pages" enable row level security;
|
|
290
310
|
alter table "page_locales" enable row level security;
|
|
291
311
|
alter table "page_versions" enable row level security;
|
|
@@ -295,6 +315,7 @@ alter table "entry_versions" enable row level security;
|
|
|
295
315
|
alter table "refs" enable row level security;
|
|
296
316
|
alter table "paths" enable row level security;
|
|
297
317
|
alter table "assets" enable row level security;
|
|
318
|
+
alter table "smoodly_meta" enable row level security;
|
|
298
319
|
`.trim(),
|
|
299
320
|
spaceLayerSQL(),
|
|
300
321
|
].join("\n\n");
|
package/dist/store.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type RefEdge, type SectionLike } from "./refs.ts";
|
|
|
4
4
|
import type { Toolset } from "./toolset.ts";
|
|
5
5
|
import type { MemoryRefIndex } from "./ref-index.ts";
|
|
6
6
|
import { type PathIndex } from "./path-index.ts";
|
|
7
|
+
import { type DraftToken } from "./draft-token.ts";
|
|
7
8
|
import { type LocaleSet, type PagePathOptions, type PathTarget } from "./paths.ts";
|
|
8
9
|
export type PageLocale = {
|
|
9
10
|
/** The node's OWN segment in this locale; the URL is derived (paths.ts). */
|
|
@@ -11,6 +12,8 @@ export type PageLocale = {
|
|
|
11
12
|
title: string;
|
|
12
13
|
status: "draft" | "published";
|
|
13
14
|
draftVersionId: string | null;
|
|
15
|
+
/** The draft version's saved time as the store's own text — the token's second half (spec 2026-09-26 concurrency §1). Null with no pointer. */
|
|
16
|
+
draftSavedAt: string | null;
|
|
14
17
|
publishedVersionId: string | null;
|
|
15
18
|
updatedAt: number;
|
|
16
19
|
/** The editor's email behind the last write that touched this row; null for the seed and scripts (spec 2026-09-13 history §2). */
|
|
@@ -54,6 +57,8 @@ export interface PageStore {
|
|
|
54
57
|
getPage(id: string): Promise<PageRecord | null>;
|
|
55
58
|
listPages(): Promise<PageRecord[]>;
|
|
56
59
|
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
60
|
+
/** The database's schema version from `smoodly_meta`; null when the table is missing (older than v1). Memory: always current (spec 2026-09-26 §5). */
|
|
61
|
+
schemaVersion(): Promise<number | null>;
|
|
57
62
|
/** locale → path, for the locales the page exists in. */
|
|
58
63
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
59
64
|
/** Refused when the page already has the locale or its parent lacks it (the parent rule). */
|
|
@@ -79,18 +84,18 @@ export interface PageStore {
|
|
|
79
84
|
}): Promise<PageRecord>;
|
|
80
85
|
/** A folder becomes a page: an empty draft version per present locale. */
|
|
81
86
|
setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
|
|
82
|
-
saveDraft(id: string, locale: string, tree: PageTree, by?: string): Promise<PageVersion>;
|
|
83
|
-
publish(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
87
|
+
saveDraft(id: string, locale: string, tree: PageTree, by?: string, expected?: DraftToken | null): Promise<PageVersion>;
|
|
88
|
+
publish(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
84
89
|
/** The draft goes back to what is live: the locale's draft pointer set to its
|
|
85
90
|
* published pointer and the abandoned draft row deleted (spec 2026-09-13 history §1).
|
|
86
91
|
* Refused when nothing is published in the locale (spec 2026-09-13 publish flow §4). */
|
|
87
|
-
discardDraft(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
92
|
+
discardDraft(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
88
93
|
/** Takes ONE locale off the site: status draft, both pointers kept so a later
|
|
89
94
|
* publish republishes the current draft (spec §5). */
|
|
90
95
|
unpublish(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
91
96
|
/** The version's tree becomes the draft through the draft rule (spec 2026-09-13 history §6); the
|
|
92
97
|
* published version restores as a discard. Refused when the version is not this page's in this locale. */
|
|
93
|
-
restore(id: string, locale: string, versionId: string, by?: string): Promise<PageRecord>;
|
|
98
|
+
restore(id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
94
99
|
/** One stored version by id, whoever it belongs to — callers check `pageId` and `locale`. */
|
|
95
100
|
getVersion(versionId: string): Promise<PageVersion | null>;
|
|
96
101
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
@@ -194,6 +199,7 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
194
199
|
getPage(id: string): Promise<PageRecord | null>;
|
|
195
200
|
listPages(): Promise<PageRecord[]>;
|
|
196
201
|
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
202
|
+
schemaVersion(): Promise<number>;
|
|
197
203
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
198
204
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
199
205
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
|
@@ -207,11 +213,11 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
207
213
|
index?: number;
|
|
208
214
|
}): Promise<PageRecord>;
|
|
209
215
|
setTemplate(id: string, template: string, by?: string): Promise<PageRecord>;
|
|
210
|
-
saveDraft(id: string, locale: string, tree: PageTree, by?: string): Promise<PageVersion>;
|
|
211
|
-
publish(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
212
|
-
discardDraft(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
216
|
+
saveDraft(id: string, locale: string, tree: PageTree, by?: string, expected?: DraftToken | null): Promise<PageVersion>;
|
|
217
|
+
publish(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
218
|
+
discardDraft(id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
213
219
|
unpublish(id: string, locale: string, by?: string): Promise<PageRecord>;
|
|
214
|
-
restore(id: string, locale: string, versionId: string, by?: string): Promise<PageRecord>;
|
|
220
|
+
restore(id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<PageRecord>;
|
|
215
221
|
getVersion(versionId: string): Promise<PageVersion | null>;
|
|
216
222
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
217
223
|
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
package/dist/store.js
CHANGED
|
@@ -13,6 +13,8 @@ import { ASSET_COLLECTION } from "./media.js";
|
|
|
13
13
|
import { collectTreeRefs } from "./refs.js";
|
|
14
14
|
import { customToolset, withCustomSchema } from "./custom/index.js";
|
|
15
15
|
import { MemoryPathIndex } from "./path-index.js";
|
|
16
|
+
import { assertFresh } from "./draft-token.js";
|
|
17
|
+
import { SCHEMA_VERSION } from "./schema-version.js";
|
|
16
18
|
import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, } from "./paths.js";
|
|
17
19
|
export const DEFAULT_LOCALES = { default: "en", supported: ["en"] };
|
|
18
20
|
/** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
|
|
@@ -90,6 +92,11 @@ export async function assertPublishable(label, tree, sections, entries, locale)
|
|
|
90
92
|
}
|
|
91
93
|
let counter = 0;
|
|
92
94
|
const uid = () => `id-${++counter}-${Date.now().toString(36)}`;
|
|
95
|
+
/** A version's saved time: wall-clock, but strictly increasing, so two saves in one
|
|
96
|
+
* millisecond still move the draft token (spec 2026-09-26 concurrency §1) the way
|
|
97
|
+
* Postgres's microsecond `now()` does. */
|
|
98
|
+
let lastSavedAt = 0;
|
|
99
|
+
const savedClock = () => (lastSavedAt = Math.max(Date.now(), lastSavedAt + 1));
|
|
93
100
|
/** The row's audit pair: every write that touches a locale row sets both (spec 2026-09-13 history §2). */
|
|
94
101
|
function touchRow(row, by) {
|
|
95
102
|
row.updatedAt = Date.now();
|
|
@@ -178,7 +185,7 @@ export class MemoryPageStore {
|
|
|
178
185
|
refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections, elements));
|
|
179
186
|
}
|
|
180
187
|
newVersion(pageId, locale, tree) {
|
|
181
|
-
const version = { id: uid(), pageId, locale, tree: structuredClone(tree), savedAt:
|
|
188
|
+
const version = { id: uid(), pageId, locale, tree: structuredClone(tree), savedAt: savedClock() };
|
|
182
189
|
this.versions.set(version.id, version);
|
|
183
190
|
return version;
|
|
184
191
|
}
|
|
@@ -190,12 +197,14 @@ export class MemoryPageStore {
|
|
|
190
197
|
let version;
|
|
191
198
|
if (current && row.draftVersionId !== row.publishedVersionId) {
|
|
192
199
|
current.tree = structuredClone(tree);
|
|
193
|
-
current.savedAt =
|
|
200
|
+
current.savedAt = savedClock();
|
|
201
|
+
row.draftSavedAt = String(current.savedAt);
|
|
194
202
|
version = current;
|
|
195
203
|
}
|
|
196
204
|
else {
|
|
197
205
|
version = this.newVersion(page.id, locale, tree);
|
|
198
206
|
row.draftVersionId = version.id;
|
|
207
|
+
row.draftSavedAt = String(version.savedAt);
|
|
199
208
|
}
|
|
200
209
|
touchRow(row, by);
|
|
201
210
|
return version;
|
|
@@ -234,6 +243,7 @@ export class MemoryPageStore {
|
|
|
234
243
|
title: input.title ?? input.slug,
|
|
235
244
|
status: "draft",
|
|
236
245
|
draftVersionId: version?.id ?? null,
|
|
246
|
+
draftSavedAt: version ? String(version.savedAt) : null,
|
|
237
247
|
publishedVersionId: null,
|
|
238
248
|
updatedAt: Date.now(),
|
|
239
249
|
updatedBy: by ?? null,
|
|
@@ -266,6 +276,9 @@ export class MemoryPageStore {
|
|
|
266
276
|
resolvePath(locale, path) {
|
|
267
277
|
return this.paths.resolve(locale, path);
|
|
268
278
|
}
|
|
279
|
+
schemaVersion() {
|
|
280
|
+
return Promise.resolve(SCHEMA_VERSION);
|
|
281
|
+
}
|
|
269
282
|
pathsOf(id) {
|
|
270
283
|
return this.paths.pathsOf({ kind: "page", id });
|
|
271
284
|
}
|
|
@@ -291,6 +304,7 @@ export class MemoryPageStore {
|
|
|
291
304
|
title: options.title ?? source.title,
|
|
292
305
|
status: "draft",
|
|
293
306
|
draftVersionId: version?.id ?? null,
|
|
307
|
+
draftSavedAt: version ? String(version.savedAt) : null,
|
|
294
308
|
publishedVersionId: null,
|
|
295
309
|
updatedAt: Date.now(),
|
|
296
310
|
updatedBy: by ?? null,
|
|
@@ -411,24 +425,28 @@ export class MemoryPageStore {
|
|
|
411
425
|
throw pageErrors.hasTemplate(id);
|
|
412
426
|
page.template = template;
|
|
413
427
|
for (const [locale, row] of Object.entries(page.locales)) {
|
|
414
|
-
|
|
428
|
+
const version = this.newVersion(id, locale, { zones: {} });
|
|
429
|
+
row.draftVersionId = version.id;
|
|
430
|
+
row.draftSavedAt = String(version.savedAt);
|
|
415
431
|
touchRow(row, by);
|
|
416
432
|
}
|
|
417
433
|
return this.snapshot(page);
|
|
418
434
|
}
|
|
419
|
-
async saveDraft(id, locale, tree, by) {
|
|
435
|
+
async saveDraft(id, locale, tree, by, expected) {
|
|
420
436
|
const page = this.must(id);
|
|
421
437
|
if (page.template === null)
|
|
422
438
|
throw pageErrors.folder(id);
|
|
439
|
+
assertFresh(localeRow(page, locale), expected);
|
|
423
440
|
const version = this.writeDraftVersion(page, locale, tree, by);
|
|
424
441
|
this.writeRefs(page);
|
|
425
442
|
return { ...version, tree: structuredClone(version.tree) };
|
|
426
443
|
}
|
|
427
|
-
async publish(id, locale, by) {
|
|
444
|
+
async publish(id, locale, by, expected) {
|
|
428
445
|
const page = this.must(id);
|
|
429
446
|
if (page.template === null)
|
|
430
447
|
throw pageErrors.folder(id);
|
|
431
448
|
const row = localeRow(page, locale);
|
|
449
|
+
assertFresh(row, expected);
|
|
432
450
|
if (!row.draftVersionId)
|
|
433
451
|
throw pageErrors.folder(id);
|
|
434
452
|
const { sections, entries } = this.options;
|
|
@@ -441,13 +459,15 @@ export class MemoryPageStore {
|
|
|
441
459
|
this.writeRefs(page);
|
|
442
460
|
return this.snapshot(page);
|
|
443
461
|
}
|
|
444
|
-
async discardDraft(id, locale, by) {
|
|
462
|
+
async discardDraft(id, locale, by, expected) {
|
|
445
463
|
const page = this.must(id);
|
|
446
464
|
const row = localeRow(page, locale);
|
|
465
|
+
assertFresh(row, expected);
|
|
447
466
|
if (!row.publishedVersionId)
|
|
448
467
|
throw pageErrors.nothingPublished(id, locale);
|
|
449
468
|
const abandoned = row.draftVersionId;
|
|
450
469
|
row.draftVersionId = row.publishedVersionId;
|
|
470
|
+
row.draftSavedAt = String(this.versions.get(row.publishedVersionId).savedAt);
|
|
451
471
|
if (abandoned && abandoned !== row.publishedVersionId)
|
|
452
472
|
this.versions.delete(abandoned); // never shown as a stray draft
|
|
453
473
|
touchRow(row, by);
|
|
@@ -462,7 +482,7 @@ export class MemoryPageStore {
|
|
|
462
482
|
this.writeRefs(page);
|
|
463
483
|
return this.snapshot(page);
|
|
464
484
|
}
|
|
465
|
-
async restore(id, locale, versionId, by) {
|
|
485
|
+
async restore(id, locale, versionId, by, expected) {
|
|
466
486
|
const page = this.must(id);
|
|
467
487
|
if (page.template === null)
|
|
468
488
|
throw pageErrors.folder(id);
|
|
@@ -470,8 +490,10 @@ export class MemoryPageStore {
|
|
|
470
490
|
const version = this.versions.get(versionId);
|
|
471
491
|
if (!version || version.pageId !== id || version.locale !== locale)
|
|
472
492
|
throw pageErrors.foreignVersion(id, locale, versionId);
|
|
493
|
+
// The memory store is single-threaded: the read-side check IS the atomic check.
|
|
494
|
+
assertFresh(row, expected);
|
|
473
495
|
if (versionId === row.publishedVersionId)
|
|
474
|
-
return this.discardDraft(id, locale, by);
|
|
496
|
+
return this.discardDraft(id, locale, by, expected);
|
|
475
497
|
this.writeDraftVersion(page, locale, version.tree, by);
|
|
476
498
|
this.writeRefs(page);
|
|
477
499
|
return this.snapshot(page);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
3
|
import type { AddEntryLocaleOptions, CreateEntryOptions, EntryRecord, EntryStore, EntryStoreOptions, EntryUsage, EntryVersion, ListOptions } from "./entry-store.ts";
|
|
4
|
+
import { type DraftToken } from "./draft-token.ts";
|
|
4
5
|
export declare class SupabaseEntryStore implements EntryStore {
|
|
5
6
|
private db;
|
|
6
7
|
private paths;
|
|
@@ -39,9 +40,18 @@ export declare class SupabaseEntryStore implements EntryStore {
|
|
|
39
40
|
private insertLocale;
|
|
40
41
|
/** A touch writes the audit pair — `updated_at` and `updated_by` (spec 2026-09-13 history §2). */
|
|
41
42
|
private updateLocale;
|
|
43
|
+
/** The row's stale error, built from a fresh read: what a reload would get, and who wrote last. */
|
|
44
|
+
private stale;
|
|
45
|
+
/** `updateLocale` with the pointer as a filter (spec 2026-09-26 concurrency §3): the row is
|
|
46
|
+
* written only while `draft_version_id` still equals `guard` (null = still no pointer).
|
|
47
|
+
* `undefined` guard = unconditional. Resolves false when no row matched. */
|
|
48
|
+
private guardedUpdateLocale;
|
|
42
49
|
/** The row goes first (it is what makes the entry exist in the locale), then its versions. */
|
|
43
50
|
private deleteLocale;
|
|
44
|
-
/** The draft rule for every collection (spec 2026-09-14 §1), see MemoryEntryStore.recordVersion
|
|
51
|
+
/** The draft rule for every collection (spec 2026-09-14 §1), see MemoryEntryStore.recordVersion —
|
|
52
|
+
* guarded (spec 2026-09-26 concurrency §3) when the caller verified `row` against the token:
|
|
53
|
+
* the rewrite filters on saved_at, the first draft on the pointer. Returns the version row it
|
|
54
|
+
* created, or null when it rewrote in place, so a failed later step can undo it. */
|
|
45
55
|
private recordVersion;
|
|
46
56
|
/** See MemoryEntryStore.ensureLiveSnapshot: the snapshot of what is live, both pointers on it. */
|
|
47
57
|
private ensureLiveSnapshot;
|
|
@@ -51,13 +61,13 @@ export declare class SupabaseEntryStore implements EntryStore {
|
|
|
51
61
|
getMany(collection: string, ids: string[], locale: string, options?: {
|
|
52
62
|
status?: "published";
|
|
53
63
|
}): Promise<Record<string, Record<string, unknown>>>;
|
|
54
|
-
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string): Promise<EntryRecord>;
|
|
55
|
-
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string): Promise<EntryRecord>;
|
|
56
|
-
discardDraft(collection: string, id: string, locale: string, by?: string): Promise<EntryRecord>;
|
|
64
|
+
update(collection: string, id: string, locale: string, fields: Record<string, unknown>, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
65
|
+
setStatus(collection: string, id: string, locale: string, status: "draft" | "published", by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
66
|
+
discardDraft(collection: string, id: string, locale: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
57
67
|
addLocale(collection: string, id: string, locale: string, options: AddEntryLocaleOptions, by?: string): Promise<EntryRecord>;
|
|
58
68
|
removeLocale(collection: string, id: string, locale: string): Promise<EntryRecord>;
|
|
59
69
|
listVersions(collection: string, id: string, locale: string): Promise<EntryVersion[]>;
|
|
60
|
-
restore(collection: string, id: string, locale: string, versionId: string, by?: string): Promise<EntryRecord>;
|
|
70
|
+
restore(collection: string, id: string, locale: string, versionId: string, by?: string, expected?: DraftToken | null): Promise<EntryRecord>;
|
|
61
71
|
getVersion(collection: string, versionId: string): Promise<EntryVersion | null>;
|
|
62
72
|
move(collection: string, id: string, to: {
|
|
63
73
|
parentId: string | null;
|