smoodly 0.0.23 → 0.0.25
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 +192 -43
- package/bin/smoodly.js +6 -0
- package/dist/admin/auth.d.ts +10 -5
- package/dist/admin/auth.js +14 -13
- package/dist/admin/client-ops.js +2 -1
- package/dist/admin/editor/EditorView.js +20 -6
- 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 +6 -0
- package/dist/admin/editor/SeoSettings.js +1 -1
- package/dist/admin/editor/locale-url.d.ts +7 -4
- package/dist/admin/editor/locale-url.js +14 -7
- 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/index.d.ts +2 -0
- package/dist/admin/next/bridge.d.ts +2 -1
- package/dist/admin/next/bridge.js +35 -5
- package/dist/admin/next/index.d.ts +2 -0
- package/dist/admin/ops-impl.js +73 -1
- package/dist/admin/ops.d.ts +22 -2
- package/dist/admin/serialize.d.ts +2 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.js +48 -3
- package/dist/admin/shell/EntriesList.js +12 -21
- package/dist/admin/shell/EntryForm.js +63 -5
- package/dist/admin/shell/GlobalForm.js +22 -2
- package/dist/admin/shell/GlobalList.js +4 -18
- package/dist/admin/shell/IconRail.d.ts +3 -1
- package/dist/admin/shell/IconRail.js +17 -7
- package/dist/admin/shell/InviteModal.d.ts +6 -0
- package/dist/admin/shell/InviteModal.js +28 -0
- package/dist/admin/shell/ListView.js +2 -2
- package/dist/admin/shell/LoginView.js +34 -5
- package/dist/admin/shell/MediaScreen.js +11 -1
- package/dist/admin/shell/MembersPage.d.ts +2 -1
- package/dist/admin/shell/MembersPage.js +90 -25
- package/dist/admin/shell/PageDialogs.d.ts +26 -0
- package/dist/admin/shell/PageDialogs.js +38 -0
- package/dist/admin/shell/PagesList.js +100 -70
- 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/SetPasswordView.d.ts +7 -0
- package/dist/admin/shell/SetPasswordView.js +41 -0
- package/dist/admin/shell/SettingsView.d.ts +5 -1
- package/dist/admin/shell/SettingsView.js +17 -11
- package/dist/admin/shell/live-url.d.ts +10 -0
- package/dist/admin/shell/live-url.js +19 -0
- 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 +15 -1
- package/dist/admin/shell/members-list.js +24 -7
- package/dist/admin/shell/page-dialogs.d.ts +9 -0
- package/dist/admin/shell/page-dialogs.js +19 -0
- package/dist/admin/shell/router.d.ts +10 -7
- package/dist/admin/shell/router.js +18 -9
- package/dist/admin/shell/session.d.ts +6 -0
- package/dist/admin/shell/session.js +17 -2
- package/dist/admin/shell/settings.d.ts +0 -6
- package/dist/admin/shell/settings.js +4 -5
- package/dist/admin/supabase-auth.js +6 -5
- 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/ResizeHandle.js +4 -4
- 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 +11 -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 +368 -203
- 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/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/member-store.d.ts +30 -1
- package/dist/member-store.js +37 -6
- package/dist/migrations.d.ts +37 -0
- package/dist/migrations.js +73 -0
- package/dist/permissions.d.ts +7 -0
- package/dist/permissions.js +19 -0
- package/dist/schema-version.d.ts +17 -0
- package/dist/schema-version.js +32 -0
- package/dist/sql-space.js +6 -0
- package/dist/sql.js +10 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +4 -0
- package/dist/supabase-member-store.d.ts +8 -1
- package/dist/supabase-member-store.js +62 -2
- package/dist/supabase-store.d.ts +3 -0
- package/dist/supabase-store.js +23 -0
- package/dist/time-zone.d.ts +6 -0
- package/dist/time-zone.js +27 -0
- package/migrations/20260926120000_smoodly_v1.sql +535 -0
- package/package.json +13 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Roles and what each may do (spec 2026-09-25 members management §1–§2).
|
|
2
|
+
// The ONE place a role string is read: the ops and the shell ask can(),
|
|
3
|
+
// the gate normalizes what the table holds. A third role is one row in
|
|
4
|
+
// GRANTS; a stored matrix, if ever wanted, replaces GRANTS behind can().
|
|
5
|
+
export const MEMBER_ROLES = ["admin", "editor"];
|
|
6
|
+
const GRANTS = {
|
|
7
|
+
admin: ["content.write", "settings", "members.manage"],
|
|
8
|
+
editor: ["content.write"],
|
|
9
|
+
};
|
|
10
|
+
export function can(role, capability) {
|
|
11
|
+
return GRANTS[role].includes(capability);
|
|
12
|
+
}
|
|
13
|
+
export const isMemberRole = (v) => typeof v === "string" && MEMBER_ROLES.includes(v);
|
|
14
|
+
/** What the table holds → a role: `owner` (the pre-2026-09-25 word) is admin; anything unknown reads as editor, the safer side. */
|
|
15
|
+
export function normalizeRole(stored) {
|
|
16
|
+
if (stored === "owner")
|
|
17
|
+
return "admin";
|
|
18
|
+
return isMemberRole(stored) ? stored : "editor";
|
|
19
|
+
}
|
|
@@ -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 = 1;
|
|
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 = 1;
|
|
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,12 @@ $$;`.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"()));`);
|
|
196
202
|
statements.push(storageSQL());
|
|
197
203
|
return statements.join("\n\n");
|
|
198
204
|
}
|
package/dist/sql.js
CHANGED
|
@@ -286,6 +286,15 @@ $$;
|
|
|
286
286
|
-- to anything but a write key. The generated per-collection views
|
|
287
287
|
-- (collectionSQL) run with security_invoker = true, so they are subject
|
|
288
288
|
-- to this same smoodly_read policy rather than their owner's privileges.
|
|
289
|
+
-- smoodly_meta: which version of this schema the database holds (spec
|
|
290
|
+
-- 2026-09-26 §1). One row per database, not per space; every core
|
|
291
|
+
-- migration file upserts the version row as its last statement, and the
|
|
292
|
+
-- package compares it with SCHEMA_VERSION (behind blocks the admin).
|
|
293
|
+
create table if not exists "smoodly_meta" (
|
|
294
|
+
"key" text primary key,
|
|
295
|
+
"value" text not null
|
|
296
|
+
);
|
|
297
|
+
|
|
289
298
|
alter table "pages" enable row level security;
|
|
290
299
|
alter table "page_locales" enable row level security;
|
|
291
300
|
alter table "page_versions" enable row level security;
|
|
@@ -295,6 +304,7 @@ alter table "entry_versions" enable row level security;
|
|
|
295
304
|
alter table "refs" enable row level security;
|
|
296
305
|
alter table "paths" enable row level security;
|
|
297
306
|
alter table "assets" enable row level security;
|
|
307
|
+
alter table "smoodly_meta" enable row level security;
|
|
298
308
|
`.trim(),
|
|
299
309
|
spaceLayerSQL(),
|
|
300
310
|
].join("\n\n");
|
package/dist/store.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export interface PageStore {
|
|
|
54
54
|
getPage(id: string): Promise<PageRecord | null>;
|
|
55
55
|
listPages(): Promise<PageRecord[]>;
|
|
56
56
|
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
57
|
+
/** 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). */
|
|
58
|
+
schemaVersion(): Promise<number | null>;
|
|
57
59
|
/** locale → path, for the locales the page exists in. */
|
|
58
60
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
59
61
|
/** Refused when the page already has the locale or its parent lacks it (the parent rule). */
|
|
@@ -194,6 +196,7 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
194
196
|
getPage(id: string): Promise<PageRecord | null>;
|
|
195
197
|
listPages(): Promise<PageRecord[]>;
|
|
196
198
|
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
199
|
+
schemaVersion(): Promise<number>;
|
|
197
200
|
pathsOf(id: string): Promise<Record<string, string>>;
|
|
198
201
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
199
202
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
package/dist/store.js
CHANGED
|
@@ -13,6 +13,7 @@ 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 { SCHEMA_VERSION } from "./schema-version.js";
|
|
16
17
|
import { assertDepth, assertSegment, assertSupportedLocale, chainOf, isHomeNode, pagePathRows, unsupportedLocale, } from "./paths.js";
|
|
17
18
|
export const DEFAULT_LOCALES = { default: "en", supported: ["en"] };
|
|
18
19
|
/** The section list every adapter walks refs with: the registered ones plus the custom section's schema (spec 2026-09-14 §6). */
|
|
@@ -266,6 +267,9 @@ export class MemoryPageStore {
|
|
|
266
267
|
resolvePath(locale, path) {
|
|
267
268
|
return this.paths.resolve(locale, path);
|
|
268
269
|
}
|
|
270
|
+
schemaVersion() {
|
|
271
|
+
return Promise.resolve(SCHEMA_VERSION);
|
|
272
|
+
}
|
|
269
273
|
pathsOf(id) {
|
|
270
274
|
return this.paths.pathsOf({ kind: "page", id });
|
|
271
275
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { type MemberRecord, type MemberStore } from "./member-store.ts";
|
|
2
|
+
import { type InviteInput, type InviteResult, type MemberRecord, type MemberStore } from "./member-store.ts";
|
|
3
|
+
import type { MemberRole } from "./permissions.ts";
|
|
3
4
|
export declare class SupabaseMemberStore implements MemberStore {
|
|
4
5
|
private db;
|
|
5
6
|
constructor(db: SupabaseClient);
|
|
6
7
|
list(): Promise<MemberRecord[]>;
|
|
7
8
|
consoleUrl(): Promise<string | null>;
|
|
9
|
+
private toRecord;
|
|
10
|
+
/** The admin API has no lookup by email: page through the users (tens, not thousands). */
|
|
11
|
+
private userIdByEmail;
|
|
12
|
+
invite(input: InviteInput): Promise<InviteResult>;
|
|
13
|
+
setRole(userId: string, role: MemberRole): Promise<MemberRecord>;
|
|
14
|
+
remove(userId: string): Promise<void>;
|
|
8
15
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sortMembers } from "./member-store.js";
|
|
1
|
+
import { memberErrors, sortMembers } from "./member-store.js";
|
|
2
2
|
export class SupabaseMemberStore {
|
|
3
3
|
constructor(db) {
|
|
4
4
|
this.db = db;
|
|
@@ -7,7 +7,7 @@ export class SupabaseMemberStore {
|
|
|
7
7
|
const res = await this.db.from("members").select("user_id, email, role");
|
|
8
8
|
if (res.error)
|
|
9
9
|
throw new Error(`smoodly: ${res.error.message}`);
|
|
10
|
-
return sortMembers(res.data.map((r) => (
|
|
10
|
+
return sortMembers(res.data.map((r) => this.toRecord(r)));
|
|
11
11
|
}
|
|
12
12
|
async consoleUrl() {
|
|
13
13
|
const res = await this.db.from("spaces").select("console_url").limit(1).maybeSingle();
|
|
@@ -15,4 +15,64 @@ export class SupabaseMemberStore {
|
|
|
15
15
|
throw new Error(`smoodly: ${res.error.message}`);
|
|
16
16
|
return res.data?.console_url ?? null;
|
|
17
17
|
}
|
|
18
|
+
toRecord(row) {
|
|
19
|
+
return { userId: row.user_id, email: row.email, role: row.role };
|
|
20
|
+
}
|
|
21
|
+
/** The admin API has no lookup by email: page through the users (tens, not thousands). */
|
|
22
|
+
async userIdByEmail(email) {
|
|
23
|
+
const wanted = email.toLowerCase();
|
|
24
|
+
for (let page = 1; page <= 50; page++) {
|
|
25
|
+
const res = await this.db.auth.admin.listUsers({ page, perPage: 200 });
|
|
26
|
+
if (res.error)
|
|
27
|
+
throw new Error(`smoodly: ${res.error.message}`);
|
|
28
|
+
const hit = res.data.users.find((u) => u.email?.toLowerCase() === wanted);
|
|
29
|
+
if (hit)
|
|
30
|
+
return hit.id;
|
|
31
|
+
if (res.data.users.length < 200)
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
throw memberErrors.noAccount(email);
|
|
35
|
+
}
|
|
36
|
+
async invite(input) {
|
|
37
|
+
// Self-host: the content client is the auth project's service role, so
|
|
38
|
+
// the admin API is reachable. A cloud write key never gets here — the
|
|
39
|
+
// ops refuse behind the console URL first.
|
|
40
|
+
const sent = await this.db.auth.admin.inviteUserByEmail(input.email, { redirectTo: input.redirectTo });
|
|
41
|
+
let userId;
|
|
42
|
+
let invited;
|
|
43
|
+
if (!sent.error) {
|
|
44
|
+
userId = sent.data.user.id;
|
|
45
|
+
invited = true;
|
|
46
|
+
}
|
|
47
|
+
else if (sent.error.code === "email_exists" || /already (been )?registered/i.test(sent.error.message)) {
|
|
48
|
+
userId = await this.userIdByEmail(input.email);
|
|
49
|
+
invited = false;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new Error(`smoodly: ${sent.error.message}`);
|
|
53
|
+
}
|
|
54
|
+
const res = await this.db
|
|
55
|
+
.from("members")
|
|
56
|
+
.upsert({ user_id: userId, email: input.email, role: input.role }, { onConflict: "space_id,user_id" })
|
|
57
|
+
.select("user_id, email, role")
|
|
58
|
+
.single();
|
|
59
|
+
if (res.error)
|
|
60
|
+
throw new Error(`smoodly: ${res.error.message}`);
|
|
61
|
+
return { member: this.toRecord(res.data), invited };
|
|
62
|
+
}
|
|
63
|
+
async setRole(userId, role) {
|
|
64
|
+
const res = await this.db.from("members").update({ role }).eq("user_id", userId).select("user_id, email, role").maybeSingle();
|
|
65
|
+
if (res.error)
|
|
66
|
+
throw new Error(`smoodly: ${res.error.message}`);
|
|
67
|
+
if (!res.data)
|
|
68
|
+
throw memberErrors.notFound(userId);
|
|
69
|
+
return this.toRecord(res.data);
|
|
70
|
+
}
|
|
71
|
+
async remove(userId) {
|
|
72
|
+
const res = await this.db.from("members").delete().eq("user_id", userId).select("user_id");
|
|
73
|
+
if (res.error)
|
|
74
|
+
throw new Error(`smoodly: ${res.error.message}`);
|
|
75
|
+
if (!res.data || res.data.length === 0)
|
|
76
|
+
throw memberErrors.notFound(userId);
|
|
77
|
+
}
|
|
18
78
|
}
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -48,7 +48,10 @@ export declare class SupabasePageStore implements PageStore {
|
|
|
48
48
|
createPage(input: CreatePageInput, by?: string): Promise<PageRecord>;
|
|
49
49
|
getPage(id: string): Promise<PageRecord | null>;
|
|
50
50
|
listPages(): Promise<PageRecord[]>;
|
|
51
|
+
/** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
|
|
52
|
+
private schemaChecked;
|
|
51
53
|
resolvePath(locale: string, path: string): Promise<PathTarget | null>;
|
|
54
|
+
schemaVersion(): Promise<number | null>;
|
|
52
55
|
pathsOf(id: string): Promise<{}>;
|
|
53
56
|
addLocale(id: string, locale: string, options: AddLocaleOptions, by?: string): Promise<PageRecord>;
|
|
54
57
|
removeLocale(id: string, locale: string): Promise<PageRecord>;
|
package/dist/supabase-store.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// rewritten through ONE RPC per write, so a collision rolls back.
|
|
9
9
|
import { assertPublishable, assertSupportedLocale, localeRow, pageErrors, pageRefEdges, pathOptions, refSections, } from "./store.js";
|
|
10
10
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
11
|
+
import { SCHEMA_VERSION_KEY, schemaBehind, schemaBehindMessage } from "./schema-version.js";
|
|
11
12
|
import { assertDepth, assertSegment, isHomeNode, pagePathRows } from "./paths.js";
|
|
12
13
|
const SELECT = "*, page_locales(*)";
|
|
13
14
|
const UNIQUE_VIOLATION = "23505";
|
|
@@ -40,6 +41,8 @@ export class SupabasePageStore {
|
|
|
40
41
|
constructor(db, options = {}) {
|
|
41
42
|
this.db = db;
|
|
42
43
|
this.options = options;
|
|
44
|
+
/** The site's one warning per process (spec 2026-09-26 §5): fired by the first locate, never blocking. */
|
|
45
|
+
this.schemaChecked = false;
|
|
43
46
|
this.options = { ...options, sections: refSections(options) };
|
|
44
47
|
this.paths = options.paths ?? new SupabasePathIndex(db);
|
|
45
48
|
this.opts = pathOptions(options);
|
|
@@ -300,8 +303,28 @@ export class SupabasePageStore {
|
|
|
300
303
|
return res.data.map(toRecord);
|
|
301
304
|
}
|
|
302
305
|
resolvePath(locale, path) {
|
|
306
|
+
if (!this.schemaChecked) {
|
|
307
|
+
this.schemaChecked = true;
|
|
308
|
+
void this.schemaVersion()
|
|
309
|
+
.then((v) => {
|
|
310
|
+
if (schemaBehind(v))
|
|
311
|
+
console.warn(schemaBehindMessage(v));
|
|
312
|
+
})
|
|
313
|
+
.catch(() => { });
|
|
314
|
+
}
|
|
303
315
|
return this.paths.resolve(locale, path);
|
|
304
316
|
}
|
|
317
|
+
async schemaVersion() {
|
|
318
|
+
const res = await this.db.from("smoodly_meta").select("value").eq("key", SCHEMA_VERSION_KEY).maybeSingle();
|
|
319
|
+
if (res.error) {
|
|
320
|
+
// PGRST205: not in PostgREST's schema cache; 42P01: undefined table.
|
|
321
|
+
if (res.error.code === "PGRST205" || res.error.code === "42P01")
|
|
322
|
+
return null;
|
|
323
|
+
this.fail(res.error.message);
|
|
324
|
+
}
|
|
325
|
+
const n = Number(res.data?.value);
|
|
326
|
+
return Number.isInteger(n) ? n : null;
|
|
327
|
+
}
|
|
305
328
|
pathsOf(id) {
|
|
306
329
|
return UUID_RE.test(id) ? this.paths.pathsOf({ kind: "page", id }) : Promise.resolve({});
|
|
307
330
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether this runtime's Intl knows the zone: canonical names, the
|
|
2
|
+
* aliases its database still carries ("Europe/Kiev"), "UTC". An offset
|
|
3
|
+
* like "+3" or a bare city is refused. */
|
|
4
|
+
export declare function isValidZone(zone: string): boolean;
|
|
5
|
+
/** The zone the runtime resolves for itself — the editor's browser in the admin; "UTC" when it has none. */
|
|
6
|
+
export declare function browserZone(): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The site's time zone (spec 2026-09-25 date pickers §5): an IANA name,
|
|
2
|
+
// never an offset, so daylight-saving rules come from the runtime's own
|
|
3
|
+
// zone database. Core and pure: defineConfig validates with it, the
|
|
4
|
+
// admin's picker falls back to the browser's zone through it.
|
|
5
|
+
/** Whether this runtime's Intl knows the zone: canonical names, the
|
|
6
|
+
* aliases its database still carries ("Europe/Kiev"), "UTC". An offset
|
|
7
|
+
* like "+3" or a bare city is refused. */
|
|
8
|
+
export function isValidZone(zone) {
|
|
9
|
+
if (zone === "")
|
|
10
|
+
return false;
|
|
11
|
+
try {
|
|
12
|
+
new Intl.DateTimeFormat("en", { timeZone: zone });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The zone the runtime resolves for itself — the editor's browser in the admin; "UTC" when it has none. */
|
|
20
|
+
export function browserZone() {
|
|
21
|
+
try {
|
|
22
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "UTC";
|
|
26
|
+
}
|
|
27
|
+
}
|