smoodly 0.0.47 → 0.0.49
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/dist/admin/browser/main.js +2 -0
- package/dist/admin/client-ops.js +1 -0
- package/dist/admin/delivery-mode.d.ts +5 -0
- package/dist/admin/delivery-mode.js +20 -0
- package/dist/admin/editor/EditorView.js +3 -0
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/handler.d.ts +5 -1
- package/dist/admin/handler.js +2 -1
- package/dist/admin/ops-impl.d.ts +13 -0
- package/dist/admin/ops-impl.js +46 -0
- package/dist/admin/ops.d.ts +11 -0
- package/dist/admin/page-data.d.ts +2 -0
- package/dist/admin/shell/AdminApp.js +47 -1
- package/dist/admin/shell/EntryForm.js +3 -0
- package/dist/admin/shell/GlobalForm.js +5 -1
- package/dist/admin/shell/IconRail.d.ts +8 -1
- package/dist/admin/shell/IconRail.js +8 -3
- package/dist/admin/shell/deploy-tile.d.ts +20 -0
- package/dist/admin/shell/deploy-tile.js +35 -0
- package/dist/admin/ui/PublishControl.js +3 -1
- package/dist/admin/ui/theme.js +12 -0
- package/dist/astro/deploy-deps.d.ts +10 -0
- package/dist/astro/deploy-deps.js +8 -0
- package/dist/astro/index.d.ts +9 -4
- package/dist/astro/index.js +75 -15
- package/dist/astro/middleware.js +4 -0
- package/dist/astro/routes/admin.js +2 -1
- package/dist/astro/routes/preview.astro +17 -5
- package/dist/astro/routes/static-page.astro +21 -0
- package/dist/astro/runtime.d.ts +6 -0
- package/dist/astro/runtime.js +4 -1
- package/dist/astro/server.d.ts +8 -0
- package/dist/astro/server.js +9 -1
- package/dist/astro/static.d.ts +39 -0
- package/dist/astro/static.js +63 -0
- package/dist/cli/load-config.d.ts +3 -0
- package/dist/cli/load-config.js +8 -1
- package/dist/deploy-state.d.ts +28 -0
- package/dist/deploy-state.js +31 -0
- package/dist/project.d.ts +3 -0
- package/dist/project.js +8 -5
- package/dist/published.d.ts +31 -4
- package/dist/published.js +35 -7
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +1 -1
- package/dist/site.js +2 -2
- package/dist/sql-space.d.ts +3 -2
- package/dist/sql-space.js +3 -2
- package/dist/sql.js +10 -0
- package/dist/supabase-published-store.d.ts +14 -3
- package/dist/supabase-published-store.js +72 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/migrations/20261009120000_smoodly_v11.sql +49 -0
- package/package.json +1 -1
package/dist/published.js
CHANGED
|
@@ -50,6 +50,8 @@ export class MemoryPublishedStore {
|
|
|
50
50
|
this.pages = new Map();
|
|
51
51
|
this.entryRows = new Map();
|
|
52
52
|
this.recorded = null;
|
|
53
|
+
this.deploy = null;
|
|
54
|
+
this.removedAt = null;
|
|
53
55
|
}
|
|
54
56
|
async recordedModel() {
|
|
55
57
|
return this.recorded;
|
|
@@ -57,27 +59,53 @@ export class MemoryPublishedStore {
|
|
|
57
59
|
async recordModel(model) {
|
|
58
60
|
this.recorded = model;
|
|
59
61
|
}
|
|
62
|
+
async newestPublishedAt() {
|
|
63
|
+
const all = [...this.pages.values(), ...this.entryRows.values()].map((r) => r.publishedAt);
|
|
64
|
+
return all.length === 0 ? null : Math.max(...all);
|
|
65
|
+
}
|
|
66
|
+
async countPublishedAfter(since) {
|
|
67
|
+
return [...this.pages.values(), ...this.entryRows.values()].filter((r) => r.publishedAt > since).length;
|
|
68
|
+
}
|
|
69
|
+
async deployRequest() {
|
|
70
|
+
return this.deploy;
|
|
71
|
+
}
|
|
72
|
+
async requestDeploy(by, at = Date.now()) {
|
|
73
|
+
this.deploy = { at, by };
|
|
74
|
+
}
|
|
60
75
|
async countBehind(model) {
|
|
61
76
|
const rows = [...this.pages.values(), ...this.entryRows.values()];
|
|
62
77
|
return { behind: rows.filter((r) => r.model !== model).length, total: rows.length };
|
|
63
78
|
}
|
|
64
|
-
async putPages(rows) {
|
|
79
|
+
async putPages(rows, options) {
|
|
65
80
|
vacate(this.pages, rows);
|
|
66
|
-
for (const row of rows)
|
|
67
|
-
this.pages.
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const kept = options?.keep ? this.pages.get(keyOf(row.id, row.locale))?.publishedAt : undefined;
|
|
83
|
+
this.pages.set(keyOf(row.id, row.locale), structuredClone(kept === undefined ? row : { ...row, publishedAt: kept }));
|
|
84
|
+
}
|
|
68
85
|
}
|
|
69
|
-
async putEntries(rows) {
|
|
86
|
+
async putEntries(rows, options) {
|
|
70
87
|
vacate(this.entryRows, rows);
|
|
71
|
-
for (const row of rows)
|
|
72
|
-
this.entryRows.
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
const kept = options?.keep ? this.entryRows.get(keyOf(row.id, row.locale))?.publishedAt : undefined;
|
|
90
|
+
this.entryRows.set(keyOf(row.id, row.locale), structuredClone(kept === undefined ? row : { ...row, publishedAt: kept }));
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
async remove(keys) {
|
|
94
|
+
let deleted = 0;
|
|
75
95
|
for (const key of keys) {
|
|
76
96
|
const map = key.kind === "page" ? this.pages : this.entryRows;
|
|
77
97
|
for (const [k, row] of map)
|
|
78
|
-
if (row.id === key.id && (key.locale === undefined || row.locale === key.locale))
|
|
98
|
+
if (row.id === key.id && (key.locale === undefined || row.locale === key.locale)) {
|
|
79
99
|
map.delete(k);
|
|
100
|
+
deleted += 1;
|
|
101
|
+
}
|
|
80
102
|
}
|
|
103
|
+
// Only a row that went is a change (a key with no row is not): the Deploy tile counts it (spec 2026-10-09 §5).
|
|
104
|
+
if (deleted > 0)
|
|
105
|
+
this.removedAt = Date.now();
|
|
106
|
+
}
|
|
107
|
+
async lastRemovalAt() {
|
|
108
|
+
return this.removedAt;
|
|
81
109
|
}
|
|
82
110
|
async byAddress(locale, path, globals) {
|
|
83
111
|
const found = () => {
|
package/dist/schema-version.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
2
2
|
* that adds a table, column, function or policy the package reads; a
|
|
3
3
|
* unit test holds it equal to the highest file in `migrations/`. */
|
|
4
|
-
export declare const SCHEMA_VERSION =
|
|
4
|
+
export declare const SCHEMA_VERSION = 11;
|
|
5
5
|
export declare const SCHEMA_VERSION_KEY = "schema_version";
|
|
6
6
|
export declare const CORE_FILE_RE: RegExp;
|
|
7
7
|
export declare const VIEWS_FILE_RE: RegExp;
|
package/dist/schema-version.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/** The minimum schema version the package needs. Moves ONLY in a release
|
|
8
8
|
* that adds a table, column, function or policy the package reads; a
|
|
9
9
|
* unit test holds it equal to the highest file in `migrations/`. */
|
|
10
|
-
export const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 11;
|
|
11
11
|
export const SCHEMA_VERSION_KEY = "schema_version";
|
|
12
12
|
export const CORE_FILE_RE = /^(\d{14})_smoodly_v(\d+)\.sql$/;
|
|
13
13
|
export const VIEWS_FILE_RE = /^(\d{14})_smoodly_views\.sql$/;
|
package/dist/site.js
CHANGED
|
@@ -109,7 +109,7 @@ export function createSmoodlySite(options) {
|
|
|
109
109
|
return (await assembler.entries(collection, { ...o, locale, draft: false })).map(assembledEntry(collection, locale));
|
|
110
110
|
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
111
111
|
try {
|
|
112
|
-
await projector.entries(behind, { cap: Infinity });
|
|
112
|
+
await projector.entries(behind, { cap: Infinity, stamp: false });
|
|
113
113
|
rows = await read();
|
|
114
114
|
}
|
|
115
115
|
catch (e) {
|
|
@@ -139,7 +139,7 @@ export function createSmoodlySite(options) {
|
|
|
139
139
|
// One batched projection for every row behind (one tree read in the projector), then one more tree read.
|
|
140
140
|
// A failed rebuild never fails the read: the rows as read are served, and the next read tries again.
|
|
141
141
|
try {
|
|
142
|
-
await projector.pages(behind, { cap: Infinity });
|
|
142
|
+
await projector.pages(behind, { cap: Infinity, stamp: false });
|
|
143
143
|
rows = await published().tree(locale);
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
package/dist/sql-space.d.ts
CHANGED
|
@@ -9,8 +9,9 @@ export type ContentTable = (typeof CONTENT_TABLES)[number];
|
|
|
9
9
|
/** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
|
|
10
10
|
* space-stamped like content, but not transferred (export skips them, import
|
|
11
11
|
* rebuilds them) and not counted toward the row cap, since they are derived.
|
|
12
|
-
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current
|
|
13
|
-
|
|
12
|
+
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current;
|
|
13
|
+
* `space_deploys` (schema v11) one row per space: the last Deploy press of static mode. */
|
|
14
|
+
export declare const DELIVERY_TABLES: readonly ["published_pages", "published_entries", "published_models", "space_deploys"];
|
|
14
15
|
export declare function currentSpaceSQL(): string;
|
|
15
16
|
/** One permissive policy per action (spec 2026-09-29 pre-install fixes §3):
|
|
16
17
|
* the write key's insert, update and delete, each alone for its action; its
|
package/dist/sql-space.js
CHANGED
|
@@ -22,8 +22,9 @@ export const CONTENT_TABLES = [
|
|
|
22
22
|
/** The delivery model's derived tables (spec 2026-10-07 §1): policy-covered and
|
|
23
23
|
* space-stamped like content, but not transferred (export skips them, import
|
|
24
24
|
* rebuilds them) and not counted toward the row cap, since they are derived.
|
|
25
|
-
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current
|
|
26
|
-
|
|
25
|
+
* `published_models` (schema v10) holds one row per space: the document stamp the space recorded as current;
|
|
26
|
+
* `space_deploys` (schema v11) one row per space: the last Deploy press of static mode. */
|
|
27
|
+
export const DELIVERY_TABLES = ["published_pages", "published_entries", "published_models", "space_deploys"];
|
|
27
28
|
export function currentSpaceSQL() {
|
|
28
29
|
return `
|
|
29
30
|
-- The space a request acts in: the key's claim in Smoodly Cloud, the
|
package/dist/sql.js
CHANGED
|
@@ -181,9 +181,19 @@ create table if not exists "published_models" (
|
|
|
181
181
|
"recorded_at" timestamptz not null default now()
|
|
182
182
|
);
|
|
183
183
|
|
|
184
|
+
-- The last Deploy press of a space (spec 2026-10-09 static mode §7, schema v11): who pressed and when;
|
|
185
|
+
-- the admin compares it with the build marker the live site serves.
|
|
186
|
+
create table if not exists "space_deploys" (
|
|
187
|
+
"space_id" uuid primary key default ${DEFAULT_SPACE},
|
|
188
|
+
"requested_at" timestamptz,
|
|
189
|
+
"requested_by" text,
|
|
190
|
+
"removed_at" timestamptz
|
|
191
|
+
);
|
|
192
|
+
|
|
184
193
|
alter table "published_pages" enable row level security;
|
|
185
194
|
alter table "published_entries" enable row level security;
|
|
186
195
|
alter table "published_models" enable row level security;
|
|
196
|
+
alter table "space_deploys" enable row level security;
|
|
187
197
|
|
|
188
198
|
-- smoodly_list_entries without the draft branch, over the delivery table (spec 2026-10-07 §3):
|
|
189
199
|
-- \`where\` and \`order\` read the raw published snapshot, the row answers with its document.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
2
|
import type { CollectionSchema } from "./collections.ts";
|
|
3
|
-
import type { PublishedEntryRow, PublishedKey, PublishedPageRow, PublishedQuery, PublishedRoute, PublishedStore, PublishedTreeRow } from "./published.ts";
|
|
3
|
+
import type { PublishedEntryRow, PublishedKey, PublishedPageRow, PublishedQuery, PublishedRoute, PublishedStore, PublishedTreeRow, PutOptions } from "./published.ts";
|
|
4
4
|
export declare class SupabasePublishedStore implements PublishedStore {
|
|
5
5
|
private db;
|
|
6
6
|
private schemas;
|
|
@@ -8,8 +8,8 @@ export declare class SupabasePublishedStore implements PublishedStore {
|
|
|
8
8
|
/** `collections` encode a `where` on an array field as containment, as SupabaseEntryStore.query does. */
|
|
9
9
|
constructor(db: SupabaseClient, collections?: CollectionSchema<any>[]);
|
|
10
10
|
private fail;
|
|
11
|
-
putPages(rows: PublishedPageRow[]): Promise<void>;
|
|
12
|
-
putEntries(rows: PublishedEntryRow[]): Promise<void>;
|
|
11
|
+
putPages(rows: PublishedPageRow[], options?: PutOptions): Promise<void>;
|
|
12
|
+
putEntries(rows: PublishedEntryRow[], options?: PutOptions): Promise<void>;
|
|
13
13
|
/** The space this client writes in (the service role's is the default): a path delete must not reach past it,
|
|
14
14
|
* since the service role passes every policy. Cached for the life of the client; a failed lookup is not. */
|
|
15
15
|
private space;
|
|
@@ -20,6 +20,7 @@ export declare class SupabasePublishedStore implements PublishedStore {
|
|
|
20
20
|
/** One request per kind and locale filter: the keys without a locale go in one in(). */
|
|
21
21
|
private eachGroup;
|
|
22
22
|
remove(keys: PublishedKey[]): Promise<void>;
|
|
23
|
+
lastRemovalAt(): Promise<number | null>;
|
|
23
24
|
/** The space's recorded stamp (DESIGN §3): one row per space in published_models; the key's space, or the
|
|
24
25
|
* service role's default one. */
|
|
25
26
|
recordedModel(): Promise<string | null>;
|
|
@@ -30,6 +31,16 @@ export declare class SupabasePublishedStore implements PublishedStore {
|
|
|
30
31
|
behind: number;
|
|
31
32
|
total: number;
|
|
32
33
|
}>;
|
|
34
|
+
/** The static build's watermark (spec 2026-10-09 static mode §5): the newest published_at over both tables. */
|
|
35
|
+
newestPublishedAt(): Promise<number | null>;
|
|
36
|
+
/** Rows published strictly after `since`: the Deploy tile's "changes waiting". Two head counts. */
|
|
37
|
+
countPublishedAfter(since: number): Promise<number>;
|
|
38
|
+
/** The space's last Deploy press, one row in space_deploys (schema v11). */
|
|
39
|
+
deployRequest(): Promise<{
|
|
40
|
+
at: number;
|
|
41
|
+
by: string | null;
|
|
42
|
+
} | null>;
|
|
43
|
+
requestDeploy(by: string | null, at?: number): Promise<void>;
|
|
33
44
|
markStale(keys: PublishedKey[]): Promise<void>;
|
|
34
45
|
/** A route row is for serving, not for the tree: a page route carries the
|
|
35
46
|
* document, model, stale flag and tags; its tree columns are empty, and an
|
|
@@ -13,6 +13,13 @@ const fromPage = (r) => ({
|
|
|
13
13
|
id: r.page_id, locale: r.locale, parentId: r.parent_id, sort: num(r.sort), path: r.path, title: r.title, hasPage: r.has_page,
|
|
14
14
|
linkFields: r.link_fields, document: r.document, tags: r.tags, model: r.model, stale: r.stale, publishedAt: Date.parse(r.published_at),
|
|
15
15
|
});
|
|
16
|
+
/** `keep` strips published_at from an upsert's payload (PutOptions): the row keeps its own, a new row is dated by the default. */
|
|
17
|
+
function keeping(row, options) {
|
|
18
|
+
if (!options?.keep)
|
|
19
|
+
return row;
|
|
20
|
+
const { published_at: _at, ...rest } = row;
|
|
21
|
+
return rest;
|
|
22
|
+
}
|
|
16
23
|
const toPage = (r) => ({
|
|
17
24
|
page_id: r.id, locale: r.locale, parent_id: r.parentId, sort: r.sort, path: r.path, title: r.title, has_page: r.hasPage,
|
|
18
25
|
link_fields: r.linkFields, document: r.document, tags: r.tags, model: r.model, stale: r.stale, published_at: iso(r.publishedAt),
|
|
@@ -36,20 +43,21 @@ export class SupabasePublishedStore {
|
|
|
36
43
|
fail(message) {
|
|
37
44
|
throw new Error(`smoodly: published store: ${message}`);
|
|
38
45
|
}
|
|
39
|
-
async putPages(rows) {
|
|
46
|
+
async putPages(rows, options) {
|
|
40
47
|
for (let i = 0; i < rows.length; i += CHUNK) {
|
|
41
48
|
const chunk = rows.slice(i, i + CHUNK);
|
|
42
49
|
await this.vacate("published_pages", "page_id", chunk);
|
|
43
|
-
|
|
50
|
+
// `keep`: published_at leaves the payload, so an updated row keeps its own and an inserted one takes the default (now).
|
|
51
|
+
const res = await this.db.from("published_pages").upsert(chunk.map((r) => keeping(toPage(r), options)), { onConflict: "page_id,locale" });
|
|
44
52
|
if (res.error)
|
|
45
53
|
this.fail(res.error.message);
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
|
-
async putEntries(rows) {
|
|
56
|
+
async putEntries(rows, options) {
|
|
49
57
|
for (let i = 0; i < rows.length; i += CHUNK) {
|
|
50
58
|
const chunk = rows.slice(i, i + CHUNK);
|
|
51
59
|
await this.vacate("published_entries", "entry_id", chunk);
|
|
52
|
-
const res = await this.db.from("published_entries").upsert(chunk.map(toEntry), { onConflict: "entry_id,locale" });
|
|
60
|
+
const res = await this.db.from("published_entries").upsert(chunk.map((r) => keeping(toEntry(r), options)), { onConflict: "entry_id,locale" });
|
|
53
61
|
if (res.error)
|
|
54
62
|
this.fail(res.error.message);
|
|
55
63
|
}
|
|
@@ -108,14 +116,33 @@ export class SupabasePublishedStore {
|
|
|
108
116
|
}
|
|
109
117
|
}
|
|
110
118
|
async remove(keys) {
|
|
119
|
+
if (keys.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
let deleted = 0;
|
|
111
122
|
await this.eachGroup(keys, async (table, column, ids, locale) => {
|
|
112
|
-
let q = this.db.from(table).delete().in(column, ids);
|
|
123
|
+
let q = this.db.from(table).delete({ count: "exact" }).in(column, ids);
|
|
113
124
|
if (locale !== undefined)
|
|
114
125
|
q = q.eq("locale", locale);
|
|
115
126
|
const res = await q;
|
|
116
127
|
if (res.error)
|
|
117
128
|
this.fail(res.error.message);
|
|
129
|
+
deleted += res.count ?? 0;
|
|
118
130
|
});
|
|
131
|
+
// A row that went is a change the count of newer rows cannot see (spec 2026-10-09 static mode §5): the space's
|
|
132
|
+
// row keeps when one last happened. A key with no row (a publish vacating a locale that never had one) is no
|
|
133
|
+
// change. The press's columns are absent from the payload, so they stay as they are.
|
|
134
|
+
if (deleted === 0)
|
|
135
|
+
return;
|
|
136
|
+
const res = await this.db.from("space_deploys").upsert({ space_id: await this.space(), removed_at: new Date().toISOString() }, { onConflict: "space_id" });
|
|
137
|
+
if (res.error)
|
|
138
|
+
this.fail(res.error.message);
|
|
139
|
+
}
|
|
140
|
+
async lastRemovalAt() {
|
|
141
|
+
const res = await this.db.from("space_deploys").select("removed_at").eq("space_id", await this.space()).maybeSingle();
|
|
142
|
+
if (res.error)
|
|
143
|
+
this.fail(res.error.message);
|
|
144
|
+
const at = res.data?.removed_at;
|
|
145
|
+
return at ? Date.parse(at) : null;
|
|
119
146
|
}
|
|
120
147
|
/** The space's recorded stamp (DESIGN §3): one row per space in published_models; the key's space, or the
|
|
121
148
|
* service role's default one. */
|
|
@@ -149,6 +176,46 @@ export class SupabasePublishedStore {
|
|
|
149
176
|
]);
|
|
150
177
|
return { behind: pagesBehind + entriesBehind, total: pages + entries };
|
|
151
178
|
}
|
|
179
|
+
/** The static build's watermark (spec 2026-10-09 static mode §5): the newest published_at over both tables. */
|
|
180
|
+
async newestPublishedAt() {
|
|
181
|
+
const space = await this.space();
|
|
182
|
+
const latest = async (table) => {
|
|
183
|
+
const res = await this.db.from(table).select("published_at").eq("space_id", space).order("published_at", { ascending: false }).limit(1).maybeSingle();
|
|
184
|
+
if (res.error)
|
|
185
|
+
this.fail(res.error.message);
|
|
186
|
+
const at = res.data?.published_at;
|
|
187
|
+
return at ? Date.parse(at) : null;
|
|
188
|
+
};
|
|
189
|
+
const [p, e] = await Promise.all([latest("published_pages"), latest("published_entries")]);
|
|
190
|
+
return p === null && e === null ? null : Math.max(p ?? 0, e ?? 0);
|
|
191
|
+
}
|
|
192
|
+
/** Rows published strictly after `since`: the Deploy tile's "changes waiting". Two head counts. */
|
|
193
|
+
async countPublishedAfter(since) {
|
|
194
|
+
const space = await this.space();
|
|
195
|
+
const count = async (table) => {
|
|
196
|
+
const res = await this.db.from(table).select("*", { count: "exact", head: true }).eq("space_id", space).gt("published_at", new Date(since).toISOString());
|
|
197
|
+
if (res.error)
|
|
198
|
+
this.fail(res.error.message);
|
|
199
|
+
return res.count ?? 0;
|
|
200
|
+
};
|
|
201
|
+
const [p, e] = await Promise.all([count("published_pages"), count("published_entries")]);
|
|
202
|
+
return p + e;
|
|
203
|
+
}
|
|
204
|
+
/** The space's last Deploy press, one row in space_deploys (schema v11). */
|
|
205
|
+
async deployRequest() {
|
|
206
|
+
const res = await this.db.from("space_deploys").select("requested_at, requested_by").eq("space_id", await this.space()).maybeSingle();
|
|
207
|
+
if (res.error)
|
|
208
|
+
this.fail(res.error.message);
|
|
209
|
+
// A removal may have made the row before any press: no requested_at is no press.
|
|
210
|
+
const row = res.data;
|
|
211
|
+
return row?.requested_at ? { at: Date.parse(row.requested_at), by: row.requested_by } : null;
|
|
212
|
+
}
|
|
213
|
+
async requestDeploy(by, at = Date.now()) {
|
|
214
|
+
const res = await this.db.from("space_deploys")
|
|
215
|
+
.upsert({ space_id: await this.space(), requested_at: new Date(at).toISOString(), requested_by: by }, { onConflict: "space_id" });
|
|
216
|
+
if (res.error)
|
|
217
|
+
this.fail(res.error.message);
|
|
218
|
+
}
|
|
152
219
|
async markStale(keys) {
|
|
153
220
|
await this.eachGroup(keys, async (table, column, ids, locale) => {
|
|
154
221
|
let q = this.db.from(table).update({ stale: true }).in(column, ids);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.49";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.49";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
-- Smoodly core schema, version 11 (DESIGN §3, static mode, decided 2026-10-09).
|
|
2
|
+
-- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
|
|
3
|
+
-- Never edit a published file: the next change is the next file.
|
|
4
|
+
-- The delta over version 10: space_deploys, one row per space holding the last
|
|
5
|
+
-- Deploy press (static mode's button), so a serverless function can tell a
|
|
6
|
+
-- build in flight from one that landed. Additive: no function changes.
|
|
7
|
+
|
|
8
|
+
set client_min_messages = warning;
|
|
9
|
+
|
|
10
|
+
-- The last Deploy press of a space (spec 2026-10-09 static mode §7): who pressed and when; the admin
|
|
11
|
+
-- compares it with the build marker the live site serves to show building, landed or not landed.
|
|
12
|
+
create table if not exists "space_deploys" (
|
|
13
|
+
"space_id" uuid primary key default '00000000-0000-0000-0000-000000000000',
|
|
14
|
+
-- Null until the first press: a removal may make the row first.
|
|
15
|
+
"requested_at" timestamptz,
|
|
16
|
+
"requested_by" text,
|
|
17
|
+
-- When a delivery row last went (an unpublish, a delete): a change the count of newer rows cannot see.
|
|
18
|
+
"removed_at" timestamptz
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
alter table "space_deploys" enable row level security;
|
|
22
|
+
|
|
23
|
+
-- Stamped with the caller's space on insert, as every content table.
|
|
24
|
+
drop trigger if exists "smoodly_a_set_space" on "space_deploys";
|
|
25
|
+
create trigger "smoodly_a_set_space" before insert on "space_deploys"
|
|
26
|
+
for each row execute function "smoodly_set_space"();
|
|
27
|
+
|
|
28
|
+
-- A write key reads and writes its own space's row; no other key reads (spec 2026-09-29 content API contract §8).
|
|
29
|
+
drop policy if exists "smoodly_write" on "space_deploys";
|
|
30
|
+
|
|
31
|
+
drop policy if exists "smoodly_insert" on "space_deploys";
|
|
32
|
+
create policy "smoodly_insert" on "space_deploys" for insert
|
|
33
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
34
|
+
|
|
35
|
+
drop policy if exists "smoodly_update" on "space_deploys";
|
|
36
|
+
create policy "smoodly_update" on "space_deploys" for update
|
|
37
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
38
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
39
|
+
|
|
40
|
+
drop policy if exists "smoodly_delete" on "space_deploys";
|
|
41
|
+
create policy "smoodly_delete" on "space_deploys" for delete
|
|
42
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
43
|
+
|
|
44
|
+
drop policy if exists "smoodly_read" on "space_deploys";
|
|
45
|
+
create policy "smoodly_read" on "space_deploys" for select
|
|
46
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
47
|
+
|
|
48
|
+
insert into "smoodly_meta" ("key", "value") values ('schema_version', '11')
|
|
49
|
+
on conflict ("key") do update set "value" = excluded."value";
|
package/package.json
CHANGED