@marktiderman/genesis-sites 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,377 @@
1
+ import { F as FontChoice, a as SiteDoc, A as AnySectionDefinition, R as ReleasePage, o as ReleaseSnapshot, m as Release, j as PageMeta, P as PageDoc, d as PageKind, n as ReleaseMeta, c as SiteDocInput } from './registry-CdgVymsu.js';
2
+ import { CanonicalBrand } from '@marktiderman/genesis-design-system';
3
+
4
+ /**
5
+ * URL and path rules shared by the schemas, the renderer and the database.
6
+ *
7
+ * Every link a site renders comes from stored props, so every link is
8
+ * untrusted. A link is safe when it is a site path (`/about`), a fragment
9
+ * (`#pricing`), or an absolute `https:`, `http:`, `mailto:` or `tel:` URL.
10
+ * Everything else (`javascript:`, `data:`, `vbscript:`, protocol-relative
11
+ * `//host`, a backslash trick, a control character) is refused, both when a
12
+ * document is parsed and again when it is rendered.
13
+ */
14
+ /** The longest URL a site stores. */
15
+ declare const MAX_URL_LENGTH = 2048;
16
+ /** Is this a link a site may render: a site path, a fragment, or an http(s), mailto or tel URL? */
17
+ declare function isSafeHref(value: unknown): value is string;
18
+ /** Is this a URL an image may load from: a site path or an `https:` URL? */
19
+ declare function isSafeImageSrc(value: unknown): value is string;
20
+ /** Is this an external (absolute) link, opened with `rel="noopener noreferrer"`? */
21
+ declare function isExternalHref(href: string): boolean;
22
+ /**
23
+ * A page's path: `/`, or lowercase segments of letters, digits and hyphens
24
+ * (`/about`, `/classes/strength`). No trailing slash, no dots, at most 8
25
+ * segments and 200 characters. The database checks the same pattern.
26
+ */
27
+ declare const PAGE_PATH: RegExp;
28
+ declare const MAX_PAGE_PATH_LENGTH = 200;
29
+ /**
30
+ * Paths a site never owns, because it shares its hostname with the host app:
31
+ * the app's API and server functions, its auth callbacks, and this package's
32
+ * own assets. A host passes its own list (or a predicate) to
33
+ * `handleSiteRequest` and `validatePagePath`; the database's default is the
34
+ * same list (`genesis_sites.reserved_paths()`).
35
+ */
36
+ declare const DEFAULT_RESERVED_PATHS: readonly string[];
37
+ /** A reserved-path rule: a list of path prefixes, or a predicate. */
38
+ type ReservedPaths = readonly string[] | ((path: string) => boolean);
39
+ /** Is `path` reserved: equal to a reserved prefix, or under it (`/api`, `/api/x`, not `/apiary`)? */
40
+ declare function isReservedPath(path: string, reserved?: ReservedPaths): boolean;
41
+ /** Problems with a page path, empty when it is fine. */
42
+ declare function pagePathProblems(path: unknown, reserved?: ReservedPaths): string[];
43
+ /** Throws when the path can't be a page's. */
44
+ declare function validatePagePath(path: string, reserved?: ReservedPaths): string;
45
+ /**
46
+ * The page path a request path asks for: decoded, lowercase, without a
47
+ * trailing slash. Returns null for a path that can't name a page (so the
48
+ * caller falls through to the host app).
49
+ */
50
+ declare function normalizeRequestPath(pathname: string): string | null;
51
+
52
+ /**
53
+ * A site's theme, from its brand: one `<style>` of CSS variables per site.
54
+ *
55
+ * The brand comes from the host (`BrandSource`) at render time, never copied
56
+ * into the site. A publish checks the brand's colours and records its hash;
57
+ * a page rendered with a different brand (edited since) is served with a
58
+ * short cache and a warning until the site is published again, which checks
59
+ * the new colours and purges the old pages. It
60
+ * may be a full `CanonicalBrand` or the minimal shape a brand screen collects
61
+ * (`SiteBrand`: a name, a primary colour for light and dark, a heading and a
62
+ * body font from a fixed list, a logo).
63
+ *
64
+ * The variables are design-system `themeFromBrand(brand).variables` (the
65
+ * brand's scales, `--color-*`) plus the genesis-ui roles the sections are
66
+ * styled with (`--primary`, `--background`, `--font-display`, ...), so the
67
+ * one shared stylesheet serves every brand. Foregrounds are picked for
68
+ * contrast: the text on a primary button is whichever of the brand's light
69
+ * or dark text reads better, and the focus ring falls back to the text
70
+ * colour when the primary is too faint on the page.
71
+ */
72
+
73
+ /** The minimal brand: what a brand screen collects. */
74
+ interface SiteBrand {
75
+ name: string;
76
+ colors: {
77
+ /** The primary colour in light mode, as #rrggbb. */
78
+ primary: string;
79
+ /** The primary in dark mode. Default: the light primary, lightened. */
80
+ primaryDark?: string;
81
+ };
82
+ fonts?: {
83
+ heading?: FontChoice;
84
+ body?: FontChoice;
85
+ };
86
+ /** The logo, shown in the header and footer and named in JSON-LD. */
87
+ logo?: {
88
+ src: string;
89
+ alt?: string;
90
+ };
91
+ }
92
+ /** What a `BrandSource` may return: the minimal brand, or a full design-system brand (with an optional logo). */
93
+ type BrandInput = SiteBrand | (CanonicalBrand & {
94
+ logo?: {
95
+ src: string;
96
+ alt?: string;
97
+ };
98
+ displayName?: string;
99
+ });
100
+ /** The host's brand, read at render time. */
101
+ interface BrandSource {
102
+ getBrand(siteId: string): Promise<BrandInput | null> | BrandInput | null;
103
+ }
104
+ interface FontInfo {
105
+ label: string;
106
+ /** The CSS font stack. */
107
+ stack: string;
108
+ /** The Google Fonts `family=` value, when it isn't a system font. */
109
+ google?: string;
110
+ }
111
+ /** The fonts a brand or site may choose. */
112
+ declare const FONT_CHOICES: Readonly<Record<FontChoice, FontInfo>>;
113
+ /** `#abc` / `#aabbcc` → [r, g, b] (0–255). Throws on anything else. */
114
+ declare function parseHex(hex: string): [number, number, number];
115
+ /** WCAG relative luminance. */
116
+ declare function luminance(hex: string): number;
117
+ /** WCAG contrast ratio (1–21) of two colours. */
118
+ declare function contrastRatio(a: string, b: string): number;
119
+ /** Mix a colour toward white (`amount` > 0) or black (< 0), 0–1. */
120
+ declare function mix(hex: string, amount: number): string;
121
+ /** `#4f46e5` → `243 75% 59%`: the HSL triplet genesis-ui's variables hold. */
122
+ declare function hexToHslTriplet(hex: string): string;
123
+ declare function isSiteBrand(brand: BrandInput): brand is SiteBrand;
124
+ /** The minimal brand as a design-system brand (Genesis's neutrals and status colours, the brand's primary and fonts). */
125
+ declare function toCanonicalBrand(brand: BrandInput): CanonicalBrand;
126
+ /** The brand's display name: the minimal brand's `name`, a canonical brand's `displayName` or `name`. */
127
+ declare function brandDisplayName(brand: BrandInput): string;
128
+ interface ThemePair {
129
+ /** What the pair is, for an accessibility issue: "body text on the page". */
130
+ name: string;
131
+ mode: "light" | "dark";
132
+ foreground: string;
133
+ background: string;
134
+ /** The ratio it must reach: 4.5 for text, 3 for the focus ring. */
135
+ minimum: number;
136
+ }
137
+ interface SiteTheme {
138
+ light: Record<string, string>;
139
+ dark: Record<string, string>;
140
+ colorScheme: "light" | "dark" | "auto";
141
+ /** The fonts the site uses, to load. */
142
+ fonts: FontInfo[];
143
+ /** The colour pairs the sections draw, for the contrast check. */
144
+ pairs: ThemePair[];
145
+ /** Hash of the brand and overrides it was made from (recorded on a release). */
146
+ brandHash: string;
147
+ }
148
+ /** FNV-1a, 32 bits twice: a short, stable hash for cache keys and release records (not for security). */
149
+ declare function stableHash(value: unknown): string;
150
+ /**
151
+ * The site's theme: variables for light and dark, the fonts to load, the
152
+ * pairs the contrast check reads, and a hash of what it was made from.
153
+ *
154
+ * @stability Beta
155
+ */
156
+ declare function siteTheme(brand: BrandInput, theme?: Partial<SiteDoc["theme"]>): SiteTheme;
157
+ /** The site's `<style>` body: the light variables on `:root`, the dark ones by scheme. */
158
+ declare function themeCss(theme: SiteTheme): string;
159
+
160
+ interface SiteIssue {
161
+ severity: "error" | "warning";
162
+ code: string;
163
+ message: string;
164
+ pageId?: string;
165
+ path?: string;
166
+ sectionId?: string;
167
+ }
168
+ declare const isPublishable: (issues: readonly SiteIssue[]) => boolean;
169
+ type Lookup = (type: string) => AnySectionDefinition | undefined;
170
+ /** Issues on one page: its sections, limits and the h1. */
171
+ declare function checkPage(page: ReleasePage, lookup?: Lookup, reserved?: ReservedPaths): SiteIssue[];
172
+ /** Issues with the site's own parts: header, footer, redirects. */
173
+ declare function checkSiteDoc(site: SiteDoc, lookup?: Lookup): SiteIssue[];
174
+ /** Colour pairs below their minimum (1.4.3 text, 1.4.11 focus ring). */
175
+ declare function checkContrast(theme: SiteTheme): SiteIssue[];
176
+ interface CheckReleaseOptions {
177
+ /** The brand's theme: adds the contrast check. */
178
+ theme?: SiteTheme;
179
+ lookup?: Lookup;
180
+ reserved?: ReservedPaths;
181
+ }
182
+ /**
183
+ * Everything a publish checks, for a whole release.
184
+ *
185
+ * @stability Beta
186
+ */
187
+ declare function checkRelease(snapshot: ReleaseSnapshot, options?: CheckReleaseOptions): SiteIssue[];
188
+ /**
189
+ * A rendered page's outline, from its HTML: one `h1`, no skipped heading
190
+ * level, every `<img>` with an `alt` attribute. For tests and for hosts that
191
+ * want to audit what was actually served.
192
+ */
193
+ declare function checkHtmlOutline(html: string): SiteIssue[];
194
+
195
+ /**
196
+ * Where sites live: the `SiteStore` contract, the publish pipeline every
197
+ * store shares, cache invalidation, and an in-memory store for tests, demos
198
+ * and previews. `@marktiderman/genesis-sites/supabase` has the Postgres one.
199
+ */
200
+
201
+ type SaveResult<T> = {
202
+ rev: number;
203
+ } | {
204
+ conflict: T;
205
+ rev: number;
206
+ };
207
+ declare const isConflict: <T>(result: SaveResult<T>) => result is {
208
+ conflict: T;
209
+ rev: number;
210
+ };
211
+ interface AssetMeta {
212
+ alt?: string;
213
+ width?: number;
214
+ height?: number;
215
+ /** The file's name, for the storage key. */
216
+ name?: string;
217
+ }
218
+ interface Asset {
219
+ id: string;
220
+ siteId: string;
221
+ /** The storage key: always `sites/<siteId>/...`, so storage policies can scope by site. */
222
+ path: string;
223
+ /** Where the browser loads it. */
224
+ url: string;
225
+ mime: string;
226
+ alt: string | null;
227
+ width: number | null;
228
+ height: number | null;
229
+ }
230
+ interface PublishOptions {
231
+ note?: string;
232
+ /** The brand to check contrast against (and record the hash of). Required: a publish without it is blocked. */
233
+ brand?: BrandInput | null;
234
+ reserved?: ReservedPaths;
235
+ }
236
+ interface NewPage {
237
+ path: string;
238
+ kind?: PageKind;
239
+ doc: PageDoc;
240
+ }
241
+ interface SiteDraft {
242
+ site: SiteDoc;
243
+ rev: number;
244
+ }
245
+ interface PageDraft {
246
+ doc: PageDoc;
247
+ rev: number;
248
+ siteId: string;
249
+ path: string;
250
+ kind: PageKind;
251
+ }
252
+ /**
253
+ * A site's storage.
254
+ *
255
+ * Drafts change by compare-and-swap on a revision: a save names the revision
256
+ * it started from and gets the new one, or the current document when someone
257
+ * saved first. A publish checks the drafts (`checkRelease`) and freezes them
258
+ * into an immutable release that the live site points at; a rollback moves
259
+ * the pointer back. `getLive` is the public read and needs no session.
260
+ *
261
+ * @stability Beta
262
+ */
263
+ interface SiteStore {
264
+ /** The release the live site serves, or null (not published, archived, or unknown). */
265
+ getLive(siteId: string): Promise<Release | null>;
266
+ getDraft(siteId: string): Promise<SiteDraft | null>;
267
+ saveSiteDraft(siteId: string, rev: number, site: SiteDoc): Promise<SaveResult<SiteDoc>>;
268
+ listPages(siteId: string): Promise<PageMeta[]>;
269
+ getPageDraft(pageId: string): Promise<PageDraft | null>;
270
+ savePageDraft(pageId: string, rev: number, doc: PageDoc): Promise<SaveResult<PageDoc>>;
271
+ createPage(siteId: string, page: NewPage): Promise<PageMeta>;
272
+ /** Move a page to a new path; the old path redirects to it (301). */
273
+ movePage(pageId: string, path: string): Promise<PageMeta>;
274
+ deletePage(pageId: string): Promise<void>;
275
+ publish(siteId: string, options?: PublishOptions): Promise<Release>;
276
+ rollback(siteId: string, releaseId: string): Promise<Release>;
277
+ listReleases(siteId: string): Promise<ReleaseMeta[]>;
278
+ putAsset?(siteId: string, file: Blob, meta?: AssetMeta): Promise<Asset>;
279
+ }
280
+ /** A publish the checks refused: `issues` lists why. */
281
+ declare class PublishBlockedError extends Error {
282
+ readonly issues: SiteIssue[];
283
+ constructor(issues: SiteIssue[]);
284
+ }
285
+ /** Someone saved first; the store's current document is in the result of the save that raised this. */
286
+ declare class SiteConflictError extends Error {
287
+ constructor(message?: string);
288
+ }
289
+ interface PreparedRelease {
290
+ snapshot: ReleaseSnapshot;
291
+ issues: SiteIssue[];
292
+ /** Drafts whose props a migration changed: a store saves these before it publishes. */
293
+ migratedPages: {
294
+ id: string;
295
+ doc: PageDoc;
296
+ }[];
297
+ migratedSite: SiteDoc | null;
298
+ brandHash: string | null;
299
+ }
300
+ /**
301
+ * The publish pipeline every store shares: migrate every node, freeze the
302
+ * drafts into a snapshot, and check it. Throws `PublishBlockedError` when a
303
+ * check fails. Pages come in the order they are listed.
304
+ *
305
+ * @stability Beta
306
+ */
307
+ declare function prepareRelease(site: SiteDoc, pages: readonly {
308
+ id: string;
309
+ path: string;
310
+ kind: PageKind;
311
+ doc: PageDoc;
312
+ }[], options?: PublishOptions): PreparedRelease;
313
+ /** The cache tag every response of a site carries (`Vercel-Cache-Tag`). */
314
+ declare function siteCacheTag(siteId: string): string;
315
+ /** The cache tag of every page that embeds a form: purge it when the host changes that form. */
316
+ declare function formCacheTag(slug: string): string;
317
+ /** Why a site's cache is being purged. */
318
+ interface InvalidationInfo {
319
+ siteId: string;
320
+ action: "publish" | "rollback";
321
+ }
322
+ /**
323
+ * A store that purges the site's cached pages after every publish and
324
+ * rollback: `invalidate` gets `["site:<id>"]` and what happened.
325
+ *
326
+ * Mind what your CDN's purge does. On Vercel, `invalidateByTag` marks pages
327
+ * stale, so each cached URL serves the old release once more while it
328
+ * regenerates (no stampede); `dangerouslyDeleteByTag` drops them, so the
329
+ * next request waits for a render. Use the delete for a rollback that takes
330
+ * something down:
331
+ *
332
+ * withCacheInvalidation(store, (tags, { action }) =>
333
+ * action === "rollback" ? dangerouslyDeleteByTag(tags) : invalidateByTag(tags));
334
+ *
335
+ * Archiving a site goes through your own server or the database (the
336
+ * `genesis_sites` notification), so purge there too. A failed purge is
337
+ * logged and rethrown, so the editor shows that the live site may be stale.
338
+ *
339
+ * @stability Beta
340
+ */
341
+ declare function withCacheInvalidation(store: SiteStore, invalidate: (tags: string[], info: InvalidationInfo) => Promise<void> | void): SiteStore;
342
+ interface NewSite {
343
+ id?: string;
344
+ /** The site document (defaults applied). */
345
+ site: SiteDocInput;
346
+ pages: {
347
+ id?: string;
348
+ path: string;
349
+ kind?: PageKind;
350
+ doc: PageDoc;
351
+ }[];
352
+ }
353
+ interface MemorySiteStore extends SiteStore {
354
+ createSite(input: NewSite): Promise<{
355
+ siteId: string;
356
+ pageIds: string[];
357
+ }>;
358
+ /** Archive (or bring back) a site: an archived site serves nothing. */
359
+ setStatus(siteId: string, status: "draft" | "live" | "archived"): Promise<void>;
360
+ }
361
+ interface MemoryStoreOptions {
362
+ reserved?: ReservedPaths;
363
+ /** Who may edit (default: everyone). Mirrors the database's `can_edit` hook, for tests. */
364
+ canEdit?: (siteId: string) => boolean;
365
+ now?: () => Date;
366
+ }
367
+ /**
368
+ * An in-memory `SiteStore`: the same rules as the database (compare-and-swap
369
+ * revisions, path checks, redirects on move, immutable releases), for tests,
370
+ * demos and local previews. Documents are copied in and out, so callers
371
+ * can't change stored state by mutating what they hold.
372
+ *
373
+ * @stability Beta
374
+ */
375
+ declare function createMemorySiteStore(options?: MemoryStoreOptions): MemorySiteStore;
376
+
377
+ export { siteTheme as $, type Asset as A, type BrandInput as B, type CheckReleaseOptions as C, DEFAULT_RESERVED_PATHS as D, hexToHslTriplet as E, FONT_CHOICES as F, isConflict as G, isExternalHref as H, type InvalidationInfo as I, isPublishable as J, isReservedPath as K, isSafeHref as L, MAX_PAGE_PATH_LENGTH as M, type NewPage as N, isSafeImageSrc as O, PAGE_PATH as P, isSiteBrand as Q, type ReservedPaths as R, type SiteBrand as S, type ThemePair as T, luminance as U, mix as V, normalizeRequestPath as W, pagePathProblems as X, parseHex as Y, prepareRelease as Z, siteCacheTag as _, type AssetMeta as a, stableHash as a0, themeCss as a1, toCanonicalBrand as a2, validatePagePath as a3, withCacheInvalidation as a4, type BrandSource as b, type FontInfo as c, MAX_URL_LENGTH as d, type MemorySiteStore as e, type MemoryStoreOptions as f, type NewSite as g, type PageDraft as h, type PreparedRelease as i, PublishBlockedError as j, type PublishOptions as k, type SaveResult as l, SiteConflictError as m, type SiteDraft as n, type SiteIssue as o, type SiteStore as p, type SiteTheme as q, brandDisplayName as r, checkContrast as s, checkHtmlOutline as t, checkPage as u, checkRelease as v, checkSiteDoc as w, contrastRatio as x, createMemorySiteStore as y, formCacheTag as z };
@@ -0,0 +1,41 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { p as SiteStore } from './store-BzGw2i5J.js';
3
+ import './registry-CdgVymsu.js';
4
+ import 'react';
5
+ import 'zod';
6
+ import '@marktiderman/genesis-design-system';
7
+
8
+ /**
9
+ * @marktiderman/genesis-sites/supabase: `createSupabaseSiteStore`, the
10
+ * `SiteStore` over the `genesis_sites` schema (migration
11
+ * `migrations/0001_genesis_sites.sql`).
12
+ *
13
+ * Pass a client signed in as the editor for drafts and publishing (row level
14
+ * security and `can_edit` / `can_view` decide), or an anonymous one for the
15
+ * public read (`getLive`, through `genesis_sites.live_release`). The schema
16
+ * must be exposed to the API.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+
21
+ declare const SITES_SCHEMA = "genesis_sites";
22
+ /** The file types a site asset may be, as the database checks them. */
23
+ declare const ASSET_TYPES: readonly string[];
24
+ interface SupabaseSiteStoreOptions {
25
+ /** The storage bucket for `putAsset`. Default `site-assets`. */
26
+ bucket?: string;
27
+ }
28
+ /** A database error with its SQLSTATE (`GS409` a conflict, `42501` not allowed). */
29
+ declare class SiteStoreError extends Error {
30
+ readonly code: string | undefined;
31
+ readonly detail?: string | undefined;
32
+ constructor(message: string, code: string | undefined, detail?: string | undefined);
33
+ }
34
+ /**
35
+ * A `SiteStore` on Supabase.
36
+ *
37
+ * @stability Beta
38
+ */
39
+ declare function createSupabaseSiteStore(client: SupabaseClient, options?: SupabaseSiteStoreOptions): SiteStore;
40
+
41
+ export { ASSET_TYPES, SITES_SCHEMA, SiteStoreError, type SupabaseSiteStoreOptions, createSupabaseSiteStore };
@@ -0,0 +1,174 @@
1
+ import { prepareRelease } from './chunk-GA7U3CIN.js';
2
+ import { PageDoc, SiteDoc, parseSnapshot, ReleaseSnapshot } from './chunk-KEF3V5ZN.js';
3
+
4
+ // src/supabase/index.ts
5
+ var SITES_SCHEMA = "genesis_sites";
6
+ var ASSET_TYPES = Object.freeze(["image/png", "image/jpeg", "image/webp", "image/avif", "image/gif", "application/pdf"]);
7
+ var SiteStoreError = class extends Error {
8
+ constructor(message, code, detail) {
9
+ super(message);
10
+ this.code = code;
11
+ this.detail = detail;
12
+ this.name = "SiteStoreError";
13
+ }
14
+ code;
15
+ detail;
16
+ };
17
+ function fail(error, what) {
18
+ throw new SiteStoreError(`${what}: ${error.message}`, error.code, error.details);
19
+ }
20
+ var metaOf = (r) => ({ id: r.id, siteId: r.siteId, number: r.number, note: r.note, createdAt: r.createdAt, publishedAt: r.publishedAt, brandHash: r.brandHash });
21
+ var fromRow = (r) => ({
22
+ id: r.id,
23
+ siteId: r.site_id,
24
+ number: r.number,
25
+ note: r.note,
26
+ createdAt: r.created_at,
27
+ publishedAt: r.published_at,
28
+ brandHash: r.brand_hash,
29
+ ...r.snapshot !== void 0 ? { snapshot: r.snapshot } : {}
30
+ });
31
+ var pageMeta = (r) => ({ id: r.id, siteId: r.site_id, path: r.path, kind: r.kind, title: r.title ?? "", sort: r.sort, rev: r.draft_rev });
32
+ function createSupabaseSiteStore(client, options = {}) {
33
+ const db = client.schema(SITES_SCHEMA);
34
+ const bucket = options.bucket ?? "site-assets";
35
+ async function release(id) {
36
+ const { data, error } = await db.from("site_releases").select("id, site_id, number, note, created_at, published_at, brand_hash, snapshot").eq("id", id).single();
37
+ if (error) fail(error, "reading the release");
38
+ const r = fromRow(data);
39
+ return { ...metaOf(r), snapshot: ReleaseSnapshot.parse(r.snapshot) };
40
+ }
41
+ async function pageRows(siteId, withDraft) {
42
+ const columns = withDraft ? "id, site_id, path, kind, sort, draft_rev, draft" : "id, site_id, path, kind, sort, draft_rev, title:draft->root->props->>title";
43
+ const { data, error } = await db.from("site_pages").select(columns).eq("site_id", siteId).is("deleted_at", null).order("sort").order("path");
44
+ if (error) fail(error, "listing pages");
45
+ return data ?? [];
46
+ }
47
+ const store = {
48
+ async getLive(siteId) {
49
+ const { data, error } = await db.rpc("live_release", { site_id: siteId });
50
+ if (error) fail(error, "reading the live site");
51
+ if (!data) return null;
52
+ const r = data;
53
+ const snapshot = parseSnapshot(r.snapshot, (m) => console.error(`[genesis-sites] the live release of site ${siteId}: ${m}`));
54
+ if (!snapshot) return null;
55
+ return { ...metaOf(r), snapshot };
56
+ },
57
+ async getDraft(siteId) {
58
+ const { data, error } = await db.from("sites").select("draft, draft_rev").eq("id", siteId).maybeSingle();
59
+ if (error) fail(error, "reading the site");
60
+ if (!data) return null;
61
+ return { site: SiteDoc.parse(data.draft), rev: data.draft_rev };
62
+ },
63
+ async saveSiteDraft(siteId, rev, site) {
64
+ const doc = SiteDoc.parse(site);
65
+ const { data, error } = await db.rpc("save_site_draft", { site_id: siteId, expected_rev: rev, doc });
66
+ if (error?.code === "GS409") {
67
+ const current = await store.getDraft(siteId);
68
+ if (!current) fail(error, "saving the site");
69
+ return { conflict: current.site, rev: current.rev };
70
+ }
71
+ if (error) fail(error, "saving the site");
72
+ return { rev: data };
73
+ },
74
+ async listPages(siteId) {
75
+ return (await pageRows(siteId, false)).map(pageMeta);
76
+ },
77
+ async getPageDraft(pageId) {
78
+ const { data, error } = await db.from("site_pages").select("id, site_id, path, kind, draft, draft_rev").eq("id", pageId).is("deleted_at", null).maybeSingle();
79
+ if (error) fail(error, "reading the page");
80
+ if (!data) return null;
81
+ const row = data;
82
+ return { doc: PageDoc.parse(row.draft), rev: row.draft_rev, siteId: row.site_id, path: row.path, kind: row.kind };
83
+ },
84
+ async savePageDraft(pageId, rev, doc) {
85
+ const parsed = PageDoc.parse(doc);
86
+ const { data, error } = await db.rpc("save_page_draft", { page_id: pageId, expected_rev: rev, doc: parsed });
87
+ if (error?.code === "GS409") {
88
+ const current = await store.getPageDraft(pageId);
89
+ if (!current) fail(error, "saving the page");
90
+ return { conflict: current.doc, rev: current.rev };
91
+ }
92
+ if (error) fail(error, "saving the page");
93
+ return { rev: data };
94
+ },
95
+ async createPage(siteId, page) {
96
+ const doc = PageDoc.parse(page.doc);
97
+ const { data, error } = await db.rpc("create_page", { site_id: siteId, path: page.path, kind: page.kind ?? "page", doc });
98
+ if (error) fail(error, "creating the page");
99
+ const meta = (await store.listPages(siteId)).find((p) => p.id === data);
100
+ if (!meta) throw new SiteStoreError("The page was created but can't be read back.", void 0);
101
+ return meta;
102
+ },
103
+ async movePage(pageId, path) {
104
+ const { error } = await db.rpc("move_page", { page_id: pageId, new_path: path });
105
+ if (error) fail(error, "moving the page");
106
+ const draft = await store.getPageDraft(pageId);
107
+ if (!draft) throw new SiteStoreError("The page moved but can't be read back.", void 0);
108
+ const meta = (await store.listPages(draft.siteId)).find((p) => p.id === pageId);
109
+ return meta;
110
+ },
111
+ async deletePage(pageId) {
112
+ const { error } = await db.rpc("delete_page", { page_id: pageId });
113
+ if (error) fail(error, "deleting the page");
114
+ },
115
+ async publish(siteId, opts = {}) {
116
+ const draft = await store.getDraft(siteId);
117
+ if (!draft) throw new SiteStoreError(`No site ${siteId} (or you can't see it).`, "P0002");
118
+ const rows = await pageRows(siteId, true);
119
+ const pages = rows.map((r) => ({ id: r.id, path: r.path, kind: r.kind, doc: PageDoc.parse(r.draft), rev: r.draft_rev }));
120
+ const prepared = prepareRelease(draft.site, pages, opts);
121
+ const revs = Object.fromEntries(pages.map((p) => [p.id, p.rev]));
122
+ let siteRev = draft.rev;
123
+ for (const m of prepared.migratedPages) {
124
+ const saved = await store.savePageDraft(m.id, revs[m.id], m.doc);
125
+ if ("conflict" in saved) throw new SiteStoreError("A page changed while publishing; check again.", "GS409");
126
+ revs[m.id] = saved.rev;
127
+ }
128
+ if (prepared.migratedSite) {
129
+ const saved = await store.saveSiteDraft(siteId, siteRev, prepared.migratedSite);
130
+ if ("conflict" in saved) throw new SiteStoreError("The site changed while publishing; check again.", "GS409");
131
+ siteRev = saved.rev;
132
+ }
133
+ const { data, error } = await db.rpc("publish_site", {
134
+ site_id: siteId,
135
+ expected_site_rev: siteRev,
136
+ expected_pages: revs,
137
+ note: opts.note ?? null,
138
+ brand_hash: prepared.brandHash
139
+ });
140
+ if (error) fail(error, "publishing");
141
+ return release(data.id);
142
+ },
143
+ async rollback(siteId, releaseId) {
144
+ const { data, error } = await db.rpc("rollback_site", { site_id: siteId, release_id: releaseId });
145
+ if (error) fail(error, "rolling back");
146
+ return release(data.id);
147
+ },
148
+ async listReleases(siteId) {
149
+ const { data, error } = await db.from("site_releases").select("id, site_id, number, note, created_at, published_at, brand_hash").eq("site_id", siteId).order("number", { ascending: false });
150
+ if (error) fail(error, "listing releases");
151
+ return (data ?? []).map((r) => metaOf(fromRow(r)));
152
+ },
153
+ async putAsset(siteId, file, meta = {}) {
154
+ const name = (meta.name ?? "file").toLowerCase().replace(/[^a-z0-9.]+/g, "-").replace(/\.{2,}/g, ".").slice(0, 80) || "file";
155
+ const path = `sites/${siteId}/${crypto.randomUUID()}-${name}`;
156
+ const mime = file.type || "application/octet-stream";
157
+ if (!ASSET_TYPES.includes(mime)) throw new SiteStoreError(`uploading: ${mime} isn't an image (PNG, JPEG, WebP, AVIF, GIF) or a PDF.`, "22023");
158
+ const upload = await client.storage.from(bucket).upload(path, file, { contentType: mime, upsert: false });
159
+ if (upload.error) throw new SiteStoreError(`uploading: ${upload.error.message}`, void 0);
160
+ const { data, error } = await db.from("site_assets").insert({ site_id: siteId, path, mime, alt: meta.alt ?? null, width: meta.width ?? null, height: meta.height ?? null }).select("id").single();
161
+ if (error) {
162
+ await client.storage.from(bucket).remove([path]);
163
+ fail(error, "recording the upload");
164
+ }
165
+ const url = client.storage.from(bucket).getPublicUrl(path).data.publicUrl;
166
+ return { id: data.id, siteId, path, url, mime, alt: meta.alt ?? null, width: meta.width ?? null, height: meta.height ?? null };
167
+ }
168
+ };
169
+ return store;
170
+ }
171
+
172
+ export { ASSET_TYPES, SITES_SCHEMA, SiteStoreError, createSupabaseSiteStore };
173
+ //# sourceMappingURL=supabase.js.map
174
+ //# sourceMappingURL=supabase.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/supabase/index.ts"],"names":[],"mappings":"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","file":"supabase.js","sourcesContent":["/**\n * @marktiderman/genesis-sites/supabase: `createSupabaseSiteStore`, the\n * `SiteStore` over the `genesis_sites` schema (migration\n * `migrations/0001_genesis_sites.sql`).\n *\n * Pass a client signed in as the editor for drafts and publishing (row level\n * security and `can_edit` / `can_view` decide), or an anonymous one for the\n * public read (`getLive`, through `genesis_sites.live_release`). The schema\n * must be exposed to the API.\n *\n * @packageDocumentation\n */\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport { PageDoc, ReleaseSnapshot, SiteDoc, parseSnapshot, type PageKind, type PageMeta, type Release, type ReleaseMeta } from \"../core/schemas\";\nimport { prepareRelease, type Asset, type AssetMeta, type PublishOptions, type SaveResult, type SiteStore } from \"../core/store\";\n\nexport const SITES_SCHEMA = \"genesis_sites\";\n\n/** The file types a site asset may be, as the database checks them. */\nexport const ASSET_TYPES: readonly string[] = Object.freeze([\"image/png\", \"image/jpeg\", \"image/webp\", \"image/avif\", \"image/gif\", \"application/pdf\"]);\n\nexport interface SupabaseSiteStoreOptions {\n /** The storage bucket for `putAsset`. Default `site-assets`. */\n bucket?: string;\n}\n\n/** A database error with its SQLSTATE (`GS409` a conflict, `42501` not allowed). */\nexport class SiteStoreError extends Error {\n constructor(\n message: string,\n readonly code: string | undefined,\n readonly detail?: string,\n ) {\n super(message);\n this.name = \"SiteStoreError\";\n }\n}\n\ninterface PgError {\n message: string;\n code?: string;\n details?: string;\n}\n\nfunction fail(error: PgError, what: string): never {\n throw new SiteStoreError(`${what}: ${error.message}`, error.code, error.details);\n}\n\ninterface ReleaseJson {\n id: string;\n siteId: string;\n number: number;\n note: string | null;\n createdAt: string;\n publishedAt: string | null;\n brandHash: string | null;\n snapshot?: unknown;\n}\n\nconst metaOf = (r: ReleaseJson): ReleaseMeta => ({ id: r.id, siteId: r.siteId, number: r.number, note: r.note, createdAt: r.createdAt, publishedAt: r.publishedAt, brandHash: r.brandHash });\n\ninterface ReleaseRow {\n id: string;\n site_id: string;\n number: number;\n note: string | null;\n created_at: string;\n published_at: string | null;\n brand_hash: string | null;\n snapshot?: unknown;\n}\n\nconst fromRow = (r: ReleaseRow): ReleaseJson => ({\n id: r.id,\n siteId: r.site_id,\n number: r.number,\n note: r.note,\n createdAt: r.created_at,\n publishedAt: r.published_at,\n brandHash: r.brand_hash,\n ...(r.snapshot !== undefined ? { snapshot: r.snapshot } : {}),\n});\n\ninterface PageRow {\n id: string;\n site_id: string;\n path: string;\n kind: PageKind;\n sort: number;\n draft_rev: number;\n title?: string | null;\n draft?: unknown;\n}\n\nconst pageMeta = (r: PageRow): PageMeta => ({ id: r.id, siteId: r.site_id, path: r.path, kind: r.kind, title: r.title ?? \"\", sort: r.sort, rev: r.draft_rev });\n\n/**\n * A `SiteStore` on Supabase.\n *\n * @stability Beta\n */\nexport function createSupabaseSiteStore(client: SupabaseClient, options: SupabaseSiteStoreOptions = {}): SiteStore {\n const db = client.schema(SITES_SCHEMA);\n const bucket = options.bucket ?? \"site-assets\";\n\n async function release(id: string): Promise<Release> {\n const { data, error } = await db.from(\"site_releases\").select(\"id, site_id, number, note, created_at, published_at, brand_hash, snapshot\").eq(\"id\", id).single();\n if (error) fail(error, \"reading the release\");\n const r = fromRow(data as ReleaseRow);\n return { ...metaOf(r), snapshot: ReleaseSnapshot.parse(r.snapshot) };\n }\n\n async function pageRows(siteId: string, withDraft: boolean): Promise<PageRow[]> {\n const columns = withDraft ? \"id, site_id, path, kind, sort, draft_rev, draft\" : \"id, site_id, path, kind, sort, draft_rev, title:draft->root->props->>title\";\n const { data, error } = await db.from(\"site_pages\").select(columns).eq(\"site_id\", siteId).is(\"deleted_at\", null).order(\"sort\").order(\"path\");\n if (error) fail(error, \"listing pages\");\n return (data ?? []) as unknown as PageRow[];\n }\n\n const store: SiteStore = {\n async getLive(siteId) {\n const { data, error } = await db.rpc(\"live_release\", { site_id: siteId });\n if (error) fail(error, \"reading the live site\");\n if (!data) return null;\n const r = data as ReleaseJson;\n // Page by page: a stored page that no longer parses is left out (and logged), not the whole site.\n const snapshot = parseSnapshot(r.snapshot, (m) => console.error(`[genesis-sites] the live release of site ${siteId}: ${m}`));\n if (!snapshot) return null;\n return { ...metaOf(r), snapshot };\n },\n async getDraft(siteId) {\n const { data, error } = await db.from(\"sites\").select(\"draft, draft_rev\").eq(\"id\", siteId).maybeSingle();\n if (error) fail(error, \"reading the site\");\n if (!data) return null;\n return { site: SiteDoc.parse((data as { draft: unknown }).draft), rev: (data as { draft_rev: number }).draft_rev };\n },\n async saveSiteDraft(siteId, rev, site): Promise<SaveResult<SiteDoc>> {\n const doc = SiteDoc.parse(site);\n const { data, error } = await db.rpc(\"save_site_draft\", { site_id: siteId, expected_rev: rev, doc });\n if (error?.code === \"GS409\") {\n const current = await store.getDraft(siteId);\n if (!current) fail(error, \"saving the site\");\n return { conflict: current.site, rev: current.rev };\n }\n if (error) fail(error, \"saving the site\");\n return { rev: data as number };\n },\n async listPages(siteId) {\n return (await pageRows(siteId, false)).map(pageMeta);\n },\n async getPageDraft(pageId) {\n const { data, error } = await db.from(\"site_pages\").select(\"id, site_id, path, kind, draft, draft_rev\").eq(\"id\", pageId).is(\"deleted_at\", null).maybeSingle();\n if (error) fail(error, \"reading the page\");\n if (!data) return null;\n const row = data as PageRow;\n return { doc: PageDoc.parse(row.draft), rev: row.draft_rev, siteId: row.site_id, path: row.path, kind: row.kind };\n },\n async savePageDraft(pageId, rev, doc): Promise<SaveResult<PageDoc>> {\n const parsed = PageDoc.parse(doc);\n const { data, error } = await db.rpc(\"save_page_draft\", { page_id: pageId, expected_rev: rev, doc: parsed });\n if (error?.code === \"GS409\") {\n const current = await store.getPageDraft(pageId);\n if (!current) fail(error, \"saving the page\");\n return { conflict: current.doc, rev: current.rev };\n }\n if (error) fail(error, \"saving the page\");\n return { rev: data as number };\n },\n async createPage(siteId, page) {\n const doc = PageDoc.parse(page.doc);\n const { data, error } = await db.rpc(\"create_page\", { site_id: siteId, path: page.path, kind: page.kind ?? \"page\", doc });\n if (error) fail(error, \"creating the page\");\n const meta = (await store.listPages(siteId)).find((p) => p.id === data);\n if (!meta) throw new SiteStoreError(\"The page was created but can't be read back.\", undefined);\n return meta;\n },\n async movePage(pageId, path) {\n const { error } = await db.rpc(\"move_page\", { page_id: pageId, new_path: path });\n if (error) fail(error, \"moving the page\");\n const draft = await store.getPageDraft(pageId);\n if (!draft) throw new SiteStoreError(\"The page moved but can't be read back.\", undefined);\n const meta = (await store.listPages(draft.siteId)).find((p) => p.id === pageId)!;\n return meta;\n },\n async deletePage(pageId) {\n const { error } = await db.rpc(\"delete_page\", { page_id: pageId });\n if (error) fail(error, \"deleting the page\");\n },\n async publish(siteId, opts: PublishOptions = {}) {\n const draft = await store.getDraft(siteId);\n if (!draft) throw new SiteStoreError(`No site ${siteId} (or you can't see it).`, \"P0002\");\n const rows = await pageRows(siteId, true);\n const pages = rows.map((r) => ({ id: r.id, path: r.path, kind: r.kind, doc: PageDoc.parse(r.draft), rev: r.draft_rev }));\n // Throws PublishBlockedError when a check fails.\n const prepared = prepareRelease(draft.site, pages, opts);\n // Migrated props go back into the drafts first, so the release holds them.\n const revs: Record<string, number> = Object.fromEntries(pages.map((p) => [p.id, p.rev]));\n let siteRev = draft.rev;\n for (const m of prepared.migratedPages) {\n const saved = await store.savePageDraft(m.id, revs[m.id]!, m.doc);\n if (\"conflict\" in saved) throw new SiteStoreError(\"A page changed while publishing; check again.\", \"GS409\");\n revs[m.id] = saved.rev;\n }\n if (prepared.migratedSite) {\n const saved = await store.saveSiteDraft(siteId, siteRev, prepared.migratedSite);\n if (\"conflict\" in saved) throw new SiteStoreError(\"The site changed while publishing; check again.\", \"GS409\");\n siteRev = saved.rev;\n }\n const { data, error } = await db.rpc(\"publish_site\", {\n site_id: siteId,\n expected_site_rev: siteRev,\n expected_pages: revs,\n note: opts.note ?? null,\n brand_hash: prepared.brandHash,\n });\n if (error) fail(error, \"publishing\");\n return release((data as ReleaseJson).id);\n },\n async rollback(siteId, releaseId) {\n const { data, error } = await db.rpc(\"rollback_site\", { site_id: siteId, release_id: releaseId });\n if (error) fail(error, \"rolling back\");\n return release((data as ReleaseJson).id);\n },\n async listReleases(siteId) {\n const { data, error } = await db.from(\"site_releases\").select(\"id, site_id, number, note, created_at, published_at, brand_hash\").eq(\"site_id\", siteId).order(\"number\", { ascending: false });\n if (error) fail(error, \"listing releases\");\n return ((data ?? []) as ReleaseRow[]).map((r) => metaOf(fromRow(r)));\n },\n async putAsset(siteId, file: Blob, meta: AssetMeta = {}): Promise<Asset> {\n const name = (meta.name ?? \"file\").toLowerCase().replace(/[^a-z0-9.]+/g, \"-\").replace(/\\.{2,}/g, \".\").slice(0, 80) || \"file\";\n const path = `sites/${siteId}/${crypto.randomUUID()}-${name}`;\n const mime = file.type || \"application/octet-stream\";\n // The types the database records (no SVG or HTML: a browser runs them as pages). Checked before the upload,\n // so a refused file never reaches the bucket.\n if (!ASSET_TYPES.includes(mime)) throw new SiteStoreError(`uploading: ${mime} isn't an image (PNG, JPEG, WebP, AVIF, GIF) or a PDF.`, \"22023\");\n const upload = await client.storage.from(bucket).upload(path, file, { contentType: mime, upsert: false });\n if (upload.error) throw new SiteStoreError(`uploading: ${upload.error.message}`, undefined);\n const { data, error } = await db\n .from(\"site_assets\")\n .insert({ site_id: siteId, path, mime, alt: meta.alt ?? null, width: meta.width ?? null, height: meta.height ?? null })\n .select(\"id\")\n .single();\n if (error) {\n await client.storage.from(bucket).remove([path]);\n fail(error, \"recording the upload\");\n }\n const url = client.storage.from(bucket).getPublicUrl(path).data.publicUrl;\n return { id: (data as { id: string }).id, siteId, path, url, mime, alt: meta.alt ?? null, width: meta.width ?? null, height: meta.height ?? null };\n },\n };\n return store;\n}\n"]}