mockpit 2.0.0 → 3.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.
- package/README.md +59 -223
- package/bin/demoData.js +2 -2
- package/bin/initDesign.js +17 -22
- package/bin/mockpit.js +626 -216
- package/bin/poster.js +434 -0
- package/dist/mcp/server.js +127 -29
- package/dist/server/answerAdditions.d.ts +7 -0
- package/dist/server/answerAdditions.js +76 -0
- package/dist/server/apiViews.d.ts +627 -385
- package/dist/server/apiViews.js +198 -81
- package/dist/server/app.d.ts +7 -2
- package/dist/server/app.js +3514 -984
- package/dist/server/appIcon.js +15 -5
- package/dist/server/appIconPng.js +3 -3
- package/dist/server/askNudges.js +1 -1
- package/dist/server/cdnInline.d.ts +8 -0
- package/dist/server/cdnInline.js +640 -0
- package/dist/server/codeWorker.js +2 -2
- package/dist/server/contrast.d.ts +4 -0
- package/dist/server/contrast.js +157 -0
- package/dist/server/decisionList.d.ts +48 -0
- package/dist/server/decisionList.js +340 -0
- package/dist/server/decisions.d.ts +4 -4
- package/dist/server/decisions.js +9 -9
- package/dist/server/designGuide.d.ts +14 -7
- package/dist/server/designGuide.js +314 -88
- package/dist/server/embeds.d.ts +3 -1
- package/dist/server/embeds.js +31 -6
- package/dist/server/eventLog.d.ts +2 -2
- package/dist/server/eventLog.js +11 -11
- package/dist/server/events.d.ts +15 -6
- package/dist/server/feedbackBatch.d.ts +39 -13
- package/dist/server/feedbackBatch.js +129 -47
- package/dist/server/fills.d.ts +4 -0
- package/dist/server/fills.js +55 -1
- package/dist/server/guidance.d.ts +1 -1
- package/dist/server/guidance.js +2 -2
- package/dist/server/handoff.d.ts +4 -1
- package/dist/server/handoff.js +8 -2
- package/dist/server/icons.js +1 -1
- package/dist/server/index.js +39 -10
- package/dist/server/jump.d.ts +21 -0
- package/dist/server/jump.js +175 -0
- package/dist/server/kits.d.ts +2 -2
- package/dist/server/kits.js +54 -1
- package/dist/server/knobs.d.ts +3 -3
- package/dist/server/knobs.js +6 -6
- package/dist/server/mcpHttp.d.ts +7 -1
- package/dist/server/mcpHttp.js +29 -12
- package/dist/server/mcpSpec.d.ts +27 -27
- package/dist/server/mcpSpec.js +196 -56
- package/dist/server/parts.js +9 -1
- package/dist/server/run.d.ts +1 -1
- package/dist/server/run.js +47 -17
- package/dist/server/runApi.d.ts +3 -3
- package/dist/server/runApi.js +34 -21
- package/dist/server/sqlStore.d.ts +148 -103
- package/dist/server/sqlStore.js +1052 -181
- package/dist/server/standings.d.ts +25 -0
- package/dist/server/standings.js +113 -0
- package/dist/server/surfacePage.d.ts +8 -9
- package/dist/server/surfacePage.js +423 -100
- package/dist/server/system.d.ts +234 -0
- package/dist/server/system.js +1508 -0
- package/dist/server/systemChecks.d.ts +15 -0
- package/dist/server/systemChecks.js +224 -0
- package/dist/server/systemSpecimen.d.ts +2 -0
- package/dist/server/systemSpecimen.js +49 -0
- package/dist/server/themes.d.ts +9 -7
- package/dist/server/themes.js +43 -90
- package/dist/server/types.d.ts +266 -93
- package/dist/server/types.js +76 -79
- package/extensions/mockpit.js +89 -72
- package/guide/AGENT_SETUP.md +15 -1
- package/guide/packs/design-craft.md +2 -3
- package/guide/packs/foundations.md +35 -0
- package/guide/topics/asks.md +108 -63
- package/guide/topics/feedback.md +129 -120
- package/guide/topics/html.md +18 -25
- package/guide/topics/http.md +43 -41
- package/guide/topics/knobs.md +7 -10
- package/guide/topics/scripts.md +22 -33
- package/guide/topics/surfaces.md +39 -48
- package/guide/topics/system.md +157 -0
- package/opencode/mockpit.js +120 -0
- package/opencode/package.json +8 -0
- package/package.json +15 -14
- package/skills/launch-video/SKILL.md +11 -11
- package/skills/mockpit/SKILL.md +33 -26
- package/viewer/dist/index.html +10 -9
package/dist/server/sqlStore.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES,
|
|
1
|
+
import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, isDesignKind, newId, normalizeSurfaceIds, isLive, projectFromCwd, AREA_ORDER_SETTING, DECISION_EDITS_SETTING, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
|
|
2
2
|
import { renameSlugRefs } from "./embeds.js";
|
|
3
|
+
import { CURRENT_CONTRACT, normalizeSystem, SYSTEM_LIMITS, systemFromDesignSettings, } from "./system.js";
|
|
3
4
|
import { RULES_SETTING } from "./guidance.js";
|
|
4
5
|
// The settings keyed `<prefix>:<project>`, which a project rename carries.
|
|
5
|
-
const PROJECT_SETTINGS = [
|
|
6
|
+
const PROJECT_SETTINGS = [AREA_ORDER_SETTING, RULES_SETTING, DECISION_EDITS_SETTING];
|
|
6
7
|
// An optional free-text label: blank means none.
|
|
7
8
|
const label = (v) => stripNul(v ?? "").trim() || null;
|
|
8
9
|
function parseJson(raw, fallback) {
|
|
@@ -28,7 +29,7 @@ function contractReply(payload) {
|
|
|
28
29
|
out.verdict = { ...decision, kind: kind === "accept" ? "use" : "revise" };
|
|
29
30
|
}
|
|
30
31
|
if (resolved !== undefined)
|
|
31
|
-
out.
|
|
32
|
+
out.archived = resolved;
|
|
32
33
|
return out;
|
|
33
34
|
}
|
|
34
35
|
// `surfaces` with every `<mockpit-slot>` tag spelled `<mockpit-embed>`, or null
|
|
@@ -44,12 +45,52 @@ function slotTagsToEmbed(surfaces) {
|
|
|
44
45
|
});
|
|
45
46
|
return changed ? out : null;
|
|
46
47
|
}
|
|
48
|
+
// What the post cache may hold by default, in stored surface characters: a
|
|
49
|
+
// Durable Object has 128 MB in all.
|
|
50
|
+
const POST_CACHE_BYTES = 32 * 1024 * 1024;
|
|
51
|
+
const surfaceHeads = (surfaces) => JSON.stringify(surfaces.map((s) => {
|
|
52
|
+
if (s.kind !== "html")
|
|
53
|
+
return { kind: s.kind };
|
|
54
|
+
return {
|
|
55
|
+
kind: s.kind,
|
|
56
|
+
...(s.poster ? { poster: s.poster } : {}),
|
|
57
|
+
...(s.poster && s.posterMode ? { posterMode: s.posterMode } : {}),
|
|
58
|
+
...(s.poster && s.posterMode && s.posterSystem !== undefined
|
|
59
|
+
? { posterSystem: s.posterSystem }
|
|
60
|
+
: {}),
|
|
61
|
+
...(/<mockpit-embed|data-fill/i.test(s.html) ? { marked: true } : {}),
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
// surfaceHeads in SQL, for the rows written before heads were: it runs without
|
|
65
|
+
// bringing a workspace's bodies into memory, which a Durable Object can't hold.
|
|
66
|
+
const SURFACE_HEADS_SQL = `CASE WHEN json_valid(surfaces) THEN (
|
|
67
|
+
SELECT json_group_array(json(h)) FROM (
|
|
68
|
+
SELECT '{"kind":' || json_quote(json_extract(value, '$.kind'))
|
|
69
|
+
|| CASE WHEN json_extract(value, '$.kind') = 'html' AND json_extract(value, '$.poster') <> ''
|
|
70
|
+
THEN ',"poster":' || json_quote(json_extract(value, '$.poster')) ELSE '' END
|
|
71
|
+
|| CASE WHEN json_extract(value, '$.kind') = 'html'
|
|
72
|
+
AND (json_extract(value, '$.html') LIKE '%<mockpit-embed%'
|
|
73
|
+
OR json_extract(value, '$.html') LIKE '%data-fill%')
|
|
74
|
+
THEN ',"marked":true' ELSE '' END
|
|
75
|
+
|| '}' AS h
|
|
76
|
+
FROM json_each(surfaces) ORDER BY key
|
|
77
|
+
)
|
|
78
|
+
) ELSE '[]' END`;
|
|
79
|
+
// Everything a head row needs, spelled out because `SELECT *` reads the bodies.
|
|
80
|
+
const POST_HEAD_COLUMNS = "id, sessionId, designId, state, variant, status, title, head, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, description, forkedFrom, copiedFrom, starred, acceptedVersion, hiddenVersions, curSession, amendedAt, rewrites, archiveReason, stayedVersion, archivedAt";
|
|
81
|
+
// Holding every column a head reads, so listing a project's variants never touches
|
|
82
|
+
// a row with a body: those columns sit after `surfaces`, and reaching them through
|
|
83
|
+
// the table walks the body's overflow pages. Any column added to the heads belongs
|
|
84
|
+
// here too (it is built from them), under a new index name so the old one is replaced.
|
|
85
|
+
const POST_HEADS_INDEX = `designId, ${POST_HEAD_COLUMNS.split(", ")
|
|
86
|
+
.filter((c) => c !== "designId")
|
|
87
|
+
.join(", ")}`;
|
|
47
88
|
const POST_COLUMNS = `
|
|
48
|
-
id TEXT PRIMARY KEY, sessionId TEXT NOT NULL,
|
|
89
|
+
id TEXT PRIMARY KEY, sessionId TEXT NOT NULL, designId TEXT NOT NULL, state TEXT,
|
|
49
90
|
variant TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', title TEXT NOT NULL,
|
|
50
91
|
surfaces TEXT NOT NULL, knobs TEXT, slots TEXT NOT NULL DEFAULT '[]',
|
|
51
92
|
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, version INTEGER NOT NULL,
|
|
52
|
-
curFrom INTEGER, curPrompt TEXT, curAuthor TEXT,
|
|
93
|
+
curFrom INTEGER, curPrompt TEXT, curAuthor TEXT, description TEXT`;
|
|
53
94
|
// Store implementation on SQLite — a Durable Object's `ctx.storage.sql` in the
|
|
54
95
|
// Worker, or node:sqlite via an adapter on Node (see server/sqliteStorage.ts).
|
|
55
96
|
// One workspace = one database, so plain SQL with no tenant columns.
|
|
@@ -62,8 +103,47 @@ export class SqlStore {
|
|
|
62
103
|
// post's surfaces on each call. Stays correct because history is append-only:
|
|
63
104
|
// an asset id once referenced stays referenced until its post is deleted.
|
|
64
105
|
assetRefCache;
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
// Parsed posts per design, least recently used first. A component is read again
|
|
107
|
+
// for every embed that shows it, and parsing its bodies each time is what a
|
|
108
|
+
// page full of embeds would wait on. A statement that writes posts or versions
|
|
109
|
+
// drops all of it unless `writingPosts` says which posts it touches; the byte
|
|
110
|
+
// budget keeps a Durable Object's memory bounded.
|
|
111
|
+
postCache = new Map();
|
|
112
|
+
postCacheBytes = 0;
|
|
113
|
+
postCacheLimit;
|
|
114
|
+
// Past the cache's drop, for a write that patches the cache itself.
|
|
115
|
+
raw;
|
|
116
|
+
holdPostDrop = false;
|
|
117
|
+
constructor(sql, opts = {}) {
|
|
118
|
+
this.raw = sql;
|
|
119
|
+
this.postCacheLimit = opts.postCacheBytes ?? POST_CACHE_BYTES;
|
|
120
|
+
const drop = () => {
|
|
121
|
+
if (this.holdPostDrop)
|
|
122
|
+
return;
|
|
123
|
+
this.postCache.clear();
|
|
124
|
+
this.postCacheBytes = 0;
|
|
125
|
+
};
|
|
126
|
+
this.sql = {
|
|
127
|
+
exec: (query, ...bindings) => {
|
|
128
|
+
if (!/^\s*SELECT\b/i.test(query) && /\bpost(s|_versions)\b/i.test(query))
|
|
129
|
+
drop();
|
|
130
|
+
return sql.exec(query, ...bindings);
|
|
131
|
+
},
|
|
132
|
+
...(sql.transactionSync
|
|
133
|
+
? {
|
|
134
|
+
transactionSync: (fn) => {
|
|
135
|
+
try {
|
|
136
|
+
return sql.transactionSync(fn);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
// Reads inside a rolled-back transaction may have cached its writes.
|
|
140
|
+
drop();
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
67
147
|
this.atomic(() => this.migrate());
|
|
68
148
|
}
|
|
69
149
|
// On a Durable Object the synchronous writes of one event already commit as a
|
|
@@ -91,20 +171,153 @@ export class SqlStore {
|
|
|
91
171
|
this.sql.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${decl}`);
|
|
92
172
|
}
|
|
93
173
|
}
|
|
174
|
+
// What was called a mock is a design, in the store as everywhere else. First of all
|
|
175
|
+
// the migrations: every one below it speaks the new names, and a workspace with no
|
|
176
|
+
// designs table would otherwise read as older than mocks.
|
|
177
|
+
// The words stored inside JSON and values, once every older migration has written
|
|
178
|
+
// its own: a kind, what an answer's comment is called, and an archive in a reply.
|
|
179
|
+
migrateToDesignWords() {
|
|
180
|
+
const kinds = { page: "screen", sketch: "idea" };
|
|
181
|
+
for (const [from, to] of Object.entries(kinds)) {
|
|
182
|
+
this.sql.exec("UPDATE designs SET kind = ? WHERE kind = ?", to, from);
|
|
183
|
+
}
|
|
184
|
+
const renameKeys = (o, names) => {
|
|
185
|
+
let changed = false;
|
|
186
|
+
for (const [from, to] of Object.entries(names)) {
|
|
187
|
+
if (from in o) {
|
|
188
|
+
o[to] = o[from];
|
|
189
|
+
delete o[from];
|
|
190
|
+
changed = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return changed;
|
|
194
|
+
};
|
|
195
|
+
for (const r of this.sql.exec("SELECT id, asks, draft FROM designs").toArray()) {
|
|
196
|
+
const asks = parseJson(r.asks, []);
|
|
197
|
+
let asksChanged = false;
|
|
198
|
+
for (const ask of asks) {
|
|
199
|
+
if (renameKeys(ask, { note: "comment", noteImages: "commentImages" }))
|
|
200
|
+
asksChanged = true;
|
|
201
|
+
}
|
|
202
|
+
if (asksChanged) {
|
|
203
|
+
this.sql.exec("UPDATE designs SET asks = ? WHERE id = ?", JSON.stringify(asks), r.id);
|
|
204
|
+
}
|
|
205
|
+
const draft = parseJson(r.draft, null);
|
|
206
|
+
if (draft && renameKeys(draft, { notes: "askComments", noteImages: "askCommentImages" })) {
|
|
207
|
+
this.sql.exec("UPDATE designs SET draft = ? WHERE id = ?", JSON.stringify(draft), r.id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const r of this.sql
|
|
211
|
+
.exec("SELECT seq, payload FROM comments WHERE kind = 'reply' AND payload IS NOT NULL")
|
|
212
|
+
.toArray()) {
|
|
213
|
+
const payload = parseJson(r.payload, null);
|
|
214
|
+
if (!payload)
|
|
215
|
+
continue;
|
|
216
|
+
let changed = renameKeys(payload, {
|
|
217
|
+
notes: "askComments",
|
|
218
|
+
noteImages: "askCommentImages",
|
|
219
|
+
closed: "archived",
|
|
220
|
+
});
|
|
221
|
+
const edited = payload.edited;
|
|
222
|
+
if (edited) {
|
|
223
|
+
if (typeof edited.kind === "string" && edited.kind in kinds) {
|
|
224
|
+
edited.kind = kinds[edited.kind];
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
if ("grouped" in edited) {
|
|
228
|
+
delete edited.grouped;
|
|
229
|
+
changed = true;
|
|
230
|
+
// All it said was about grouping, which no longer exists: it would read as a blank line.
|
|
231
|
+
if (!Object.keys(edited).length) {
|
|
232
|
+
this.sql.exec("DELETE FROM comments WHERE seq = ?", r.seq);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (changed) {
|
|
238
|
+
this.sql.exec("UPDATE comments SET payload = ? WHERE seq = ?", JSON.stringify(payload), r.seq);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('designWords', '1')");
|
|
242
|
+
}
|
|
243
|
+
// A value, so migrateToDesignWords' renames never reached it, and that one has
|
|
244
|
+
// already run on every workspace that needs this.
|
|
245
|
+
migrateAskScopeWord() {
|
|
246
|
+
for (const r of this.sql.exec("SELECT id, asks FROM designs").toArray()) {
|
|
247
|
+
const asks = parseJson(r.asks, []);
|
|
248
|
+
const old = asks.filter((a) => a.scope === "mock");
|
|
249
|
+
if (!old.length)
|
|
250
|
+
continue;
|
|
251
|
+
for (const a of old)
|
|
252
|
+
a.scope = "design";
|
|
253
|
+
this.sql.exec("UPDATE designs SET asks = ? WHERE id = ?", JSON.stringify(asks), r.id);
|
|
254
|
+
}
|
|
255
|
+
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('askScopeWord', '1')");
|
|
256
|
+
}
|
|
257
|
+
renameMocksToDesigns() {
|
|
258
|
+
if (!this.tables().has("mocks") || this.tables().has("designs"))
|
|
259
|
+
return;
|
|
260
|
+
for (const name of [
|
|
261
|
+
"mockpit_mocks_slug_idx",
|
|
262
|
+
"mockpit_mocks_updated_at_idx",
|
|
263
|
+
"mockpit_posts_mock_idx",
|
|
264
|
+
"mockpit_comments_mock_seq_idx",
|
|
265
|
+
"mockpit_posts_copies_idx",
|
|
266
|
+
]) {
|
|
267
|
+
this.sql.exec(`DROP INDEX IF EXISTS ${name}`);
|
|
268
|
+
}
|
|
269
|
+
this.sql.exec("ALTER TABLE mocks RENAME TO designs");
|
|
270
|
+
for (const table of ["posts", "comments"]) {
|
|
271
|
+
if (this.columns(table).has("mockId")) {
|
|
272
|
+
this.sql.exec(`ALTER TABLE ${table} RENAME COLUMN mockId TO designId`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Steps and copy references are stored as JSON, keyed by the old word.
|
|
276
|
+
const renamed = (raw) => typeof raw === "string" ? raw.replaceAll('"mock":', '"design":') : raw;
|
|
277
|
+
for (const r of this.sql.exec("SELECT project, name, steps FROM flows").toArray()) {
|
|
278
|
+
this.sql.exec("UPDATE flows SET steps = ? WHERE project = ? AND name = ?", renamed(r.steps), r.project, r.name);
|
|
279
|
+
}
|
|
280
|
+
// Only the key: a user's text can hold the same letters, escaped inside its string.
|
|
281
|
+
for (const r of this.sql
|
|
282
|
+
.exec("SELECT seq, payload FROM comments WHERE payload LIKE '%\"mockId\":%'")
|
|
283
|
+
.toArray()) {
|
|
284
|
+
this.sql.exec("UPDATE comments SET payload = ? WHERE seq = ?", r.payload.replace(/(?<!\\)"mockId":/g, '"designId":'), r.seq);
|
|
285
|
+
}
|
|
286
|
+
// The same release gave the design's archive fields and a variant's description
|
|
287
|
+
// their names; the JSON that speaks these words waits for migrateToDesignWords.
|
|
288
|
+
const designCols = this.columns("designs");
|
|
289
|
+
if (designCols.has("closedAt")) {
|
|
290
|
+
this.sql.exec("ALTER TABLE designs RENAME COLUMN closedAt TO archivedAt");
|
|
291
|
+
}
|
|
292
|
+
if (designCols.has("closeReason")) {
|
|
293
|
+
this.sql.exec("ALTER TABLE designs RENAME COLUMN closeReason TO archiveReason");
|
|
294
|
+
}
|
|
295
|
+
if (this.columns("posts").has("note")) {
|
|
296
|
+
this.sql.exec("ALTER TABLE posts RENAME COLUMN note TO description");
|
|
297
|
+
}
|
|
298
|
+
if (this.columns("posts").has("copiedFrom")) {
|
|
299
|
+
for (const r of this.sql
|
|
300
|
+
.exec("SELECT id, copiedFrom FROM posts WHERE copiedFrom IS NOT NULL")
|
|
301
|
+
.toArray()) {
|
|
302
|
+
this.sql.exec("UPDATE posts SET copiedFrom = ? WHERE id = ?", renamed(r.copiedFrom), r.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
94
306
|
migrate() {
|
|
307
|
+
this.renameMocksToDesigns();
|
|
95
308
|
const before = this.tables();
|
|
96
309
|
// Deployed Durable Objects can never be reset, so a workspace written by a
|
|
97
|
-
// build from before
|
|
98
|
-
const legacy = before.has("posts") && !before.has("
|
|
310
|
+
// build from before designs existed is lifted in place (see migrateLegacy).
|
|
311
|
+
const legacy = before.has("posts") && !before.has("designs");
|
|
99
312
|
this.sql.exec(`
|
|
100
313
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
101
314
|
id TEXT PRIMARY KEY, agent TEXT NOT NULL, title TEXT, cwd TEXT,
|
|
102
315
|
createdAt TEXT NOT NULL, lastActiveAt TEXT NOT NULL,
|
|
103
316
|
agentSeq INTEGER NOT NULL DEFAULT 0, project TEXT, key TEXT, agentActiveAt TEXT
|
|
104
317
|
);
|
|
105
|
-
CREATE TABLE IF NOT EXISTS
|
|
318
|
+
CREATE TABLE IF NOT EXISTS designs (
|
|
106
319
|
id TEXT PRIMARY KEY, project TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL,
|
|
107
|
-
kind TEXT NOT NULL DEFAULT '
|
|
320
|
+
kind TEXT NOT NULL DEFAULT 'idea', states TEXT NOT NULL DEFAULT '[]',
|
|
108
321
|
asks TEXT NOT NULL DEFAULT '[]', knobs TEXT NOT NULL DEFAULT '{}', draft TEXT,
|
|
109
322
|
sessionId TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
|
110
323
|
widths TEXT NOT NULL DEFAULT '[]'
|
|
@@ -118,7 +331,7 @@ export class SqlStore {
|
|
|
118
331
|
);
|
|
119
332
|
CREATE TABLE IF NOT EXISTS comments (
|
|
120
333
|
seq INTEGER PRIMARY KEY AUTOINCREMENT, id TEXT NOT NULL,
|
|
121
|
-
sessionId TEXT NOT NULL,
|
|
334
|
+
sessionId TEXT NOT NULL, designId TEXT, postId TEXT,
|
|
122
335
|
author TEXT NOT NULL, text TEXT NOT NULL, createdAt TEXT NOT NULL,
|
|
123
336
|
kind TEXT NOT NULL DEFAULT 'comment', anchor TEXT, anchors TEXT NOT NULL DEFAULT '[]',
|
|
124
337
|
postVersion INTEGER, viewport INTEGER, payload TEXT
|
|
@@ -136,6 +349,14 @@ export class SqlStore {
|
|
|
136
349
|
createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
|
|
137
350
|
PRIMARY KEY (project, name)
|
|
138
351
|
);
|
|
352
|
+
CREATE TABLE IF NOT EXISTS systems (
|
|
353
|
+
project TEXT PRIMARY KEY, rev INTEGER NOT NULL, json TEXT NOT NULL
|
|
354
|
+
);
|
|
355
|
+
CREATE TABLE IF NOT EXISTS system_revisions (
|
|
356
|
+
project TEXT NOT NULL, rev INTEGER NOT NULL, json TEXT NOT NULL, at TEXT NOT NULL,
|
|
357
|
+
sessionId TEXT, summary TEXT NOT NULL DEFAULT '',
|
|
358
|
+
PRIMARY KEY (project, rev)
|
|
359
|
+
);
|
|
139
360
|
`);
|
|
140
361
|
this.addMissing("sessions", {
|
|
141
362
|
agentSeq: "INTEGER NOT NULL DEFAULT 0",
|
|
@@ -144,40 +365,104 @@ export class SqlStore {
|
|
|
144
365
|
agentActiveAt: "TEXT",
|
|
145
366
|
});
|
|
146
367
|
// The close column's first name.
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
this.sql.exec("ALTER TABLE
|
|
368
|
+
const designCols = this.columns("designs");
|
|
369
|
+
if (designCols.has("resolvedAt") && !designCols.has("archivedAt")) {
|
|
370
|
+
this.sql.exec("ALTER TABLE designs RENAME COLUMN resolvedAt TO archivedAt");
|
|
150
371
|
}
|
|
151
|
-
this.addMissing("
|
|
372
|
+
this.addMissing("designs", {
|
|
152
373
|
widths: "TEXT NOT NULL DEFAULT '[]'",
|
|
153
|
-
|
|
374
|
+
archivedAt: "TEXT",
|
|
375
|
+
archiveReason: "TEXT",
|
|
154
376
|
area: "TEXT",
|
|
155
377
|
source: "TEXT",
|
|
156
|
-
|
|
378
|
+
shortlist: "INTEGER NOT NULL DEFAULT 0",
|
|
379
|
+
notes: "TEXT",
|
|
380
|
+
stateOutlines: "TEXT NOT NULL DEFAULT '{}'",
|
|
381
|
+
answerAdditions: "TEXT NOT NULL DEFAULT '[]'",
|
|
382
|
+
standings: "TEXT NOT NULL DEFAULT '{}'",
|
|
383
|
+
sets: "TEXT NOT NULL DEFAULT '[]'",
|
|
384
|
+
});
|
|
385
|
+
this.migrateSituations();
|
|
386
|
+
// Before the legacy lift, whose history rows are written with every column.
|
|
387
|
+
this.addMissing("post_versions", {
|
|
388
|
+
sessionId: "TEXT",
|
|
389
|
+
amendedAt: "TEXT",
|
|
390
|
+
contract: "INTEGER",
|
|
391
|
+
system: "INTEGER",
|
|
157
392
|
});
|
|
158
393
|
if (legacy)
|
|
159
394
|
this.migrateLegacy();
|
|
160
395
|
// After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
|
|
161
396
|
const pinned = this.columns("posts").has("acceptedVersion");
|
|
397
|
+
const dated = this.columns("posts").has("archivedAt");
|
|
162
398
|
this.addMissing("posts", {
|
|
163
|
-
|
|
399
|
+
description: "TEXT",
|
|
164
400
|
forkedFrom: "TEXT",
|
|
401
|
+
copiedFrom: "TEXT",
|
|
402
|
+
// Inert: kept answers (`Design.standings`) took over from the star.
|
|
165
403
|
starred: "INTEGER NOT NULL DEFAULT 0",
|
|
166
404
|
acceptedVersion: "INTEGER",
|
|
167
405
|
hiddenVersions: "TEXT",
|
|
406
|
+
curSession: "TEXT",
|
|
407
|
+
amendedAt: "TEXT",
|
|
408
|
+
rewrites: "INTEGER NOT NULL DEFAULT 0",
|
|
409
|
+
archiveReason: "TEXT",
|
|
410
|
+
stayedVersion: "INTEGER",
|
|
411
|
+
archivedAt: "TEXT",
|
|
412
|
+
curContract: "INTEGER",
|
|
413
|
+
curSystem: "INTEGER",
|
|
168
414
|
});
|
|
169
415
|
// Before the pin, accepted meant "its latest": freeze that as the version in use.
|
|
170
416
|
if (!pinned) {
|
|
171
417
|
this.sql.exec("UPDATE posts SET acceptedVersion = version WHERE status = 'accepted'");
|
|
172
418
|
}
|
|
419
|
+
for (const table of ["posts", "post_versions"]) {
|
|
420
|
+
if (this.columns(table).has("head"))
|
|
421
|
+
continue;
|
|
422
|
+
this.addMissing(table, { head: "TEXT" });
|
|
423
|
+
this.sql.exec(`UPDATE ${table} SET head = ${SURFACE_HEADS_SQL}`);
|
|
424
|
+
}
|
|
173
425
|
if (this.settingSync("sketchKind") !== "1")
|
|
174
426
|
this.migrateToSketches();
|
|
427
|
+
this.migrateSystems();
|
|
175
428
|
if (this.settingSync("contractTerms") !== "1")
|
|
176
429
|
this.migrateToContractTerms();
|
|
430
|
+
if (this.settingSync("designWords") !== "1")
|
|
431
|
+
this.migrateToDesignWords();
|
|
432
|
+
if (this.settingSync("askScopeWord") !== "1")
|
|
433
|
+
this.migrateAskScopeWord();
|
|
434
|
+
// The day it went was never kept, and the design's last change is the nearest thing.
|
|
435
|
+
// Frozen now, or the next reason or draft on the design would move it to that day.
|
|
436
|
+
if (!dated) {
|
|
437
|
+
this.sql.exec("UPDATE posts SET archivedAt = (SELECT updatedAt FROM designs WHERE designs.id = posts.designId) WHERE status = 'archived'");
|
|
438
|
+
}
|
|
177
439
|
this.createIndexes();
|
|
178
440
|
}
|
|
441
|
+
// A situation was already a state of its own, filed in `states` under the name
|
|
442
|
+
// feedback gave it ("Writing · Couldn't save"), with its asks, comments and posts
|
|
443
|
+
// there too, so those keep that name and only the map that set it apart goes. The
|
|
444
|
+
// column itself stays on a workspace that has it, empty: nothing reads it any more,
|
|
445
|
+
// and new workspaces never get one.
|
|
446
|
+
migrateSituations() {
|
|
447
|
+
if (!this.columns("designs").has("situations"))
|
|
448
|
+
return;
|
|
449
|
+
for (const r of this.sql
|
|
450
|
+
.exec("SELECT id, states, situations FROM designs WHERE situations != '{}'")
|
|
451
|
+
.toArray()) {
|
|
452
|
+
const states = parseJson(r.states, []);
|
|
453
|
+
const map = parseJson(r.situations, {});
|
|
454
|
+
for (const [state, names] of Object.entries(map)) {
|
|
455
|
+
for (const name of names) {
|
|
456
|
+
const key = `${state} · ${name}`;
|
|
457
|
+
if (!states.includes(key))
|
|
458
|
+
states.push(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this.sql.exec("UPDATE designs SET states = ?, situations = '{}' WHERE id = ?", JSON.stringify(states), r.id);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
179
464
|
// The agent contract renamed what replies and drafts store (mix → borrowed,
|
|
180
|
-
// decision → verdict with accept → use, resolved →
|
|
465
|
+
// decision → verdict with accept → use, resolved → archived) and retired drop
|
|
181
466
|
// and the `<mockpit-slot>` embed alias. Stored data is rewritten once, here,
|
|
182
467
|
// so nothing past the store reads an old name: a dropped variant is archived
|
|
183
468
|
// (both were hidden from the choice and restorable), a drop verdict loses its
|
|
@@ -195,13 +480,13 @@ export class SqlStore {
|
|
|
195
480
|
}
|
|
196
481
|
}
|
|
197
482
|
for (const r of this.sql
|
|
198
|
-
.exec("SELECT id, draft FROM
|
|
483
|
+
.exec("SELECT id, draft FROM designs WHERE draft IS NOT NULL")
|
|
199
484
|
.toArray()) {
|
|
200
485
|
const draft = parseJson(r.draft, null);
|
|
201
486
|
if (!draft || !("mix" in draft))
|
|
202
487
|
continue;
|
|
203
488
|
const { mix, ...rest } = draft;
|
|
204
|
-
this.sql.exec("UPDATE
|
|
489
|
+
this.sql.exec("UPDATE designs SET draft = ? WHERE id = ?", JSON.stringify({ ...rest, borrowed: mix ?? {} }), r.id);
|
|
205
490
|
}
|
|
206
491
|
for (const r of this.sql
|
|
207
492
|
.exec("SELECT id, surfaces FROM posts WHERE surfaces LIKE '%mockpit-slot%'")
|
|
@@ -223,19 +508,22 @@ export class SqlStore {
|
|
|
223
508
|
}
|
|
224
509
|
createIndexes() {
|
|
225
510
|
this.sql.exec(`
|
|
226
|
-
CREATE UNIQUE INDEX IF NOT EXISTS
|
|
227
|
-
CREATE INDEX IF NOT EXISTS
|
|
228
|
-
CREATE INDEX IF NOT EXISTS
|
|
511
|
+
CREATE UNIQUE INDEX IF NOT EXISTS mockpit_designs_slug_idx ON designs (project, slug);
|
|
512
|
+
CREATE INDEX IF NOT EXISTS mockpit_designs_updated_at_idx ON designs (updatedAt DESC);
|
|
513
|
+
CREATE INDEX IF NOT EXISTS mockpit_posts_design_idx ON posts (designId, state, variant);
|
|
229
514
|
CREATE INDEX IF NOT EXISTS mockpit_posts_session_created_at_idx ON posts (sessionId, createdAt);
|
|
515
|
+
CREATE INDEX IF NOT EXISTS mockpit_posts_copies_idx ON posts (designId, state, variant, status, copiedFrom) WHERE copiedFrom IS NOT NULL;
|
|
516
|
+
CREATE INDEX IF NOT EXISTS mockpit_posts_heads_idx ON posts (${POST_HEADS_INDEX});
|
|
517
|
+
CREATE INDEX IF NOT EXISTS mockpit_post_versions_heads_idx ON post_versions (postId, version, head);
|
|
230
518
|
CREATE INDEX IF NOT EXISTS mockpit_comments_session_seq_idx ON comments (sessionId, seq);
|
|
231
|
-
CREATE INDEX IF NOT EXISTS
|
|
519
|
+
CREATE INDEX IF NOT EXISTS mockpit_comments_design_seq_idx ON comments (designId, seq);
|
|
232
520
|
CREATE INDEX IF NOT EXISTS mockpit_comments_post_seq_idx ON comments (postId, seq);
|
|
233
521
|
CREATE INDEX IF NOT EXISTS mockpit_comments_id_idx ON comments (id);
|
|
234
522
|
CREATE INDEX IF NOT EXISTS mockpit_assets_session_idx ON assets (sessionId);
|
|
235
523
|
CREATE INDEX IF NOT EXISTS mockpit_sessions_key_idx ON sessions (key);
|
|
236
524
|
`);
|
|
237
525
|
}
|
|
238
|
-
// --- in-place migration of a pre-
|
|
526
|
+
// --- in-place migration of a pre-design workspace ---
|
|
239
527
|
migrateLegacy() {
|
|
240
528
|
this.addMissing("comments", {
|
|
241
529
|
anchor: "TEXT",
|
|
@@ -244,7 +532,7 @@ export class SqlStore {
|
|
|
244
532
|
draft: "INTEGER NOT NULL DEFAULT 0",
|
|
245
533
|
postVersion: "INTEGER",
|
|
246
534
|
viewport: "INTEGER",
|
|
247
|
-
|
|
535
|
+
designId: "TEXT",
|
|
248
536
|
payload: "TEXT",
|
|
249
537
|
});
|
|
250
538
|
this.migrateToSurfaces();
|
|
@@ -252,7 +540,7 @@ export class SqlStore {
|
|
|
252
540
|
this.migrateSurfaceIds();
|
|
253
541
|
this.migrateToItems();
|
|
254
542
|
this.migrateToVersions();
|
|
255
|
-
this.
|
|
543
|
+
this.migrateToDesigns();
|
|
256
544
|
this.sql.exec(`
|
|
257
545
|
DROP TABLE IF EXISTS trace_steps;
|
|
258
546
|
DROP INDEX IF EXISTS mockpit_comments_post_draft_idx;
|
|
@@ -379,11 +667,11 @@ export class SqlStore {
|
|
|
379
667
|
}
|
|
380
668
|
}
|
|
381
669
|
}
|
|
382
|
-
// Every (project, item slug) becomes one single-state
|
|
383
|
-
// the
|
|
384
|
-
//
|
|
670
|
+
// Every (project, item slug) becomes one single-state design; its posts become
|
|
671
|
+
// the design's variants with state null. Unsent operator drafts become the
|
|
672
|
+
// design's draft so they are still there to send; decisions become plain
|
|
385
673
|
// comments (they were already delivered).
|
|
386
|
-
|
|
674
|
+
migrateToDesigns() {
|
|
387
675
|
const rows = this.sql.exec("SELECT * FROM posts ORDER BY createdAt ASC, rowid ASC").toArray();
|
|
388
676
|
const groups = new Map();
|
|
389
677
|
for (const r of rows) {
|
|
@@ -394,13 +682,13 @@ export class SqlStore {
|
|
|
394
682
|
else
|
|
395
683
|
groups.set(key, [r]);
|
|
396
684
|
}
|
|
397
|
-
const
|
|
685
|
+
const designOf = new Map();
|
|
398
686
|
const variantOf = new Map();
|
|
399
687
|
for (const group of groups.values()) {
|
|
400
688
|
const first = group[0];
|
|
401
689
|
const latest = group.reduce((a, b) => b.updatedAt > a.updatedAt ? b : a);
|
|
402
690
|
const id = newId();
|
|
403
|
-
this.sql.exec("INSERT INTO
|
|
691
|
+
this.sql.exec("INSERT INTO designs (id, project, slug, title, kind, states, asks, knobs, draft, sessionId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, '[]', '[]', '{}', NULL, ?, ?, ?)", id, first.project || DEFAULT_PROJECT, first.slug || slugify(first.title), first.title, group.some((r) => r.kind === "page") ? "page" : "component", latest.sessionId, first.createdAt, latest.updatedAt);
|
|
404
692
|
const used = new Set();
|
|
405
693
|
for (const r of group) {
|
|
406
694
|
const base = r.variant || DEFAULT_VARIANT;
|
|
@@ -408,25 +696,25 @@ export class SqlStore {
|
|
|
408
696
|
for (let n = 2; used.has(variant); n++)
|
|
409
697
|
variant = `${base}-${n}`;
|
|
410
698
|
used.add(variant);
|
|
411
|
-
|
|
699
|
+
designOf.set(r.id, id);
|
|
412
700
|
variantOf.set(r.id, variant);
|
|
413
701
|
}
|
|
414
702
|
}
|
|
415
703
|
this.sql.exec(`CREATE TABLE posts_next (${POST_COLUMNS})`);
|
|
416
704
|
for (const r of rows) {
|
|
417
705
|
const id = r.id;
|
|
418
|
-
this.sql.exec("INSERT INTO posts_next (id, sessionId,
|
|
706
|
+
this.sql.exec("INSERT INTO posts_next (id, sessionId, designId, state, variant, status, title, surfaces, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)", id, r.sessionId, designOf.get(id), variantOf.get(id), r.status || "open", r.title, r.surfaces, r.slots || "[]", r.createdAt, r.updatedAt, r.version, r.curFrom ?? null, r.curPrompt ?? null, r.curAuthor ?? null);
|
|
419
707
|
}
|
|
420
708
|
this.sql.exec("DROP TABLE posts");
|
|
421
709
|
this.sql.exec("ALTER TABLE posts_next RENAME TO posts");
|
|
422
|
-
this.sql.exec("UPDATE comments SET
|
|
710
|
+
this.sql.exec("UPDATE comments SET designId = (SELECT designId FROM posts WHERE posts.id = comments.postId) WHERE designId IS NULL");
|
|
423
711
|
this.sql.exec("UPDATE comments SET text = (CASE kind WHEN 'accept' THEN 'Accepted' WHEN 'revise' THEN 'Revise' ELSE 'Dropped' END) || (CASE WHEN text = '' THEN '' ELSE ': ' || text END), kind = 'comment' WHERE kind IN ('accept', 'revise', 'drop')");
|
|
424
712
|
const drafts = new Map();
|
|
425
713
|
for (const c of this.sql
|
|
426
|
-
.exec("SELECT * FROM comments WHERE draft = 1 AND
|
|
714
|
+
.exec("SELECT * FROM comments WHERE draft = 1 AND designId IS NOT NULL ORDER BY seq ASC")
|
|
427
715
|
.toArray()) {
|
|
428
|
-
const
|
|
429
|
-
let draft = drafts.get(
|
|
716
|
+
const designId = c.designId;
|
|
717
|
+
let draft = drafts.get(designId);
|
|
430
718
|
if (!draft) {
|
|
431
719
|
draft = {
|
|
432
720
|
version: 1,
|
|
@@ -436,41 +724,41 @@ export class SqlStore {
|
|
|
436
724
|
comments: [],
|
|
437
725
|
updatedAt: c.createdAt,
|
|
438
726
|
};
|
|
439
|
-
drafts.set(
|
|
727
|
+
drafts.set(designId, draft);
|
|
440
728
|
}
|
|
441
729
|
draft.version = Math.max(draft.version, c.postVersion ?? 1);
|
|
442
730
|
draft.comments.push({ part: null, state: null, text: c.text });
|
|
443
731
|
draft.updatedAt = c.createdAt;
|
|
444
732
|
}
|
|
445
|
-
for (const [
|
|
446
|
-
this.sql.exec("UPDATE
|
|
733
|
+
for (const [designId, draft] of drafts) {
|
|
734
|
+
this.sql.exec("UPDATE designs SET draft = ? WHERE id = ?", JSON.stringify(draft), designId);
|
|
447
735
|
}
|
|
448
736
|
this.sql.exec("DELETE FROM comments WHERE draft = 1");
|
|
449
737
|
}
|
|
450
|
-
// A
|
|
738
|
+
// A design no one declared used to be a component. Keep that only where it acts
|
|
451
739
|
// as one, embedded somewhere or a step of a flow; the rest become sketches.
|
|
452
740
|
migrateToSketches() {
|
|
453
741
|
const projectOf = new Map();
|
|
454
|
-
for (const r of this.sql.exec("SELECT id, project FROM
|
|
742
|
+
for (const r of this.sql.exec("SELECT id, project FROM designs").toArray()) {
|
|
455
743
|
projectOf.set(r.id, r.project);
|
|
456
744
|
}
|
|
457
745
|
const used = new Set();
|
|
458
|
-
for (const r of this.sql.exec("SELECT
|
|
459
|
-
const project = projectOf.get(r.
|
|
746
|
+
for (const r of this.sql.exec("SELECT designId, slots FROM posts").toArray()) {
|
|
747
|
+
const project = projectOf.get(r.designId);
|
|
460
748
|
for (const e of parseJson(r.slots, []))
|
|
461
749
|
used.add(`${project}\u0000${e.slug}`);
|
|
462
750
|
}
|
|
463
751
|
for (const r of this.sql.exec("SELECT project, steps FROM flows").toArray()) {
|
|
464
752
|
for (const x of parseJson(r.steps, [])) {
|
|
465
|
-
used.add(`${r.project}\u0000${x.
|
|
753
|
+
used.add(`${r.project}\u0000${x.design}`);
|
|
466
754
|
}
|
|
467
755
|
}
|
|
468
756
|
for (const r of this.sql
|
|
469
|
-
.exec("SELECT id, project, slug FROM
|
|
757
|
+
.exec("SELECT id, project, slug FROM designs WHERE kind = 'component'")
|
|
470
758
|
.toArray()) {
|
|
471
759
|
if (used.has(`${r.project}\u0000${r.slug}`))
|
|
472
760
|
continue;
|
|
473
|
-
this.sql.exec("UPDATE
|
|
761
|
+
this.sql.exec("UPDATE designs SET kind = 'sketch' WHERE id = ?", r.id);
|
|
474
762
|
}
|
|
475
763
|
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('sketchKind', '1')");
|
|
476
764
|
}
|
|
@@ -480,7 +768,7 @@ export class SqlStore {
|
|
|
480
768
|
}
|
|
481
769
|
// --- rows ---
|
|
482
770
|
insertVersion(postId, v) {
|
|
483
|
-
this.sql.exec("INSERT OR REPLACE INTO post_versions (postId, version, title, surfaces, at, fromVersion, prompt, author) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", postId, v.version, v.title, JSON.stringify(v.surfaces ?? []), v.at, v.from ?? null, v.prompt ?? null, v.author ?? null);
|
|
771
|
+
this.sql.exec("INSERT OR REPLACE INTO post_versions (postId, version, title, surfaces, head, at, fromVersion, prompt, author, sessionId, amendedAt, contract, system) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", postId, v.version, v.title, JSON.stringify(v.surfaces ?? []), surfaceHeads(v.surfaces ?? []), v.at, v.from ?? null, v.prompt ?? null, v.author ?? null, v.publishedBy ?? null, v.amendedAt ?? null, v.contract ?? null, v.system ?? null);
|
|
484
772
|
}
|
|
485
773
|
rowToVersion(r) {
|
|
486
774
|
return {
|
|
@@ -492,9 +780,13 @@ export class SqlStore {
|
|
|
492
780
|
...(r.fromVersion == null ? {} : { from: r.fromVersion }),
|
|
493
781
|
...(r.prompt == null ? {} : { prompt: r.prompt }),
|
|
494
782
|
...(r.author == null ? {} : { author: r.author }),
|
|
783
|
+
...(r.sessionId == null ? {} : { publishedBy: r.sessionId }),
|
|
784
|
+
...(r.amendedAt == null ? {} : { amendedAt: r.amendedAt }),
|
|
785
|
+
...(r.contract == null ? {} : { contract: r.contract }),
|
|
786
|
+
...(r.system == null ? {} : { system: r.system }),
|
|
495
787
|
};
|
|
496
788
|
}
|
|
497
|
-
historiesFor(ids) {
|
|
789
|
+
historiesFor(ids, weigh) {
|
|
498
790
|
const out = new Map();
|
|
499
791
|
if (ids.length === 0)
|
|
500
792
|
return out;
|
|
@@ -506,12 +798,13 @@ export class SqlStore {
|
|
|
506
798
|
let list = out.get(id);
|
|
507
799
|
if (!list)
|
|
508
800
|
out.set(id, (list = []));
|
|
801
|
+
weigh?.(r.surfaces.length);
|
|
509
802
|
list.push(this.rowToVersion(r));
|
|
510
803
|
}
|
|
511
804
|
return out;
|
|
512
805
|
}
|
|
513
|
-
rowsToPosts(rows) {
|
|
514
|
-
const histories = this.historiesFor(rows.map((r) => r.id));
|
|
806
|
+
rowsToPosts(rows, weigh) {
|
|
807
|
+
const histories = this.historiesFor(rows.map((r) => r.id), weigh);
|
|
515
808
|
return rows.map((r) => this.rowToPost(r, histories.get(r.id) ?? []));
|
|
516
809
|
}
|
|
517
810
|
rowToSession(r) {
|
|
@@ -528,23 +821,29 @@ export class SqlStore {
|
|
|
528
821
|
keyed: r.key != null,
|
|
529
822
|
};
|
|
530
823
|
}
|
|
531
|
-
|
|
824
|
+
rowToDesign(r) {
|
|
532
825
|
return {
|
|
533
826
|
id: r.id,
|
|
534
827
|
project: r.project,
|
|
535
828
|
slug: r.slug,
|
|
536
829
|
title: r.title,
|
|
537
|
-
kind:
|
|
830
|
+
kind: isDesignKind(r.kind) ? r.kind : "idea",
|
|
538
831
|
area: r.area ?? null,
|
|
539
832
|
source: r.source ?? null,
|
|
540
833
|
states: parseJson(r.states, []),
|
|
541
|
-
situations: parseJson(r.situations, {}),
|
|
542
834
|
asks: parseJson(r.asks, []),
|
|
543
835
|
knobs: parseJson(r.knobs, {}),
|
|
544
836
|
widths: parseJson(r.widths, []),
|
|
545
837
|
draft: parseJson(r.draft, null),
|
|
546
838
|
sessionId: r.sessionId ?? null,
|
|
547
|
-
|
|
839
|
+
shortlist: !!r.shortlist,
|
|
840
|
+
archivedAt: r.archivedAt ?? null,
|
|
841
|
+
archiveReason: r.archiveReason ?? null,
|
|
842
|
+
notes: parseJson(r.notes, null),
|
|
843
|
+
stateOutlines: parseJson(r.stateOutlines, {}),
|
|
844
|
+
answerAdditions: parseJson(r.answerAdditions, []),
|
|
845
|
+
standings: parseJson(r.standings, {}),
|
|
846
|
+
sets: parseJson(r.sets, []),
|
|
548
847
|
createdAt: r.createdAt,
|
|
549
848
|
updatedAt: r.updatedAt,
|
|
550
849
|
};
|
|
@@ -555,7 +854,7 @@ export class SqlStore {
|
|
|
555
854
|
return {
|
|
556
855
|
id: r.id,
|
|
557
856
|
sessionId: r.sessionId,
|
|
558
|
-
|
|
857
|
+
design: r.designId,
|
|
559
858
|
state: r.state ?? null,
|
|
560
859
|
variant: r.variant,
|
|
561
860
|
status: (r.status || "open"),
|
|
@@ -566,18 +865,26 @@ export class SqlStore {
|
|
|
566
865
|
version: r.version,
|
|
567
866
|
history,
|
|
568
867
|
...(knobs ? { knobs } : {}),
|
|
569
|
-
...(r.
|
|
868
|
+
...(r.description == null ? {} : { description: r.description }),
|
|
570
869
|
// The column keeps its first name, from when embeds were called slots.
|
|
571
870
|
embeds: parseJson(r.slots, []),
|
|
572
871
|
...(r.forkedFrom == null ? {} : { forkedFrom: parseJson(r.forkedFrom, null) }),
|
|
573
|
-
...(r.
|
|
872
|
+
...(r.copiedFrom == null ? {} : { copiedFrom: parseJson(r.copiedFrom, null) }),
|
|
574
873
|
...(r.status === "accepted" && r.acceptedVersion != null
|
|
575
874
|
? { acceptedVersion: r.acceptedVersion }
|
|
576
875
|
: {}),
|
|
577
876
|
...(hidden.length ? { hiddenVersions: hidden } : {}),
|
|
877
|
+
...(r.archiveReason == null ? {} : { archiveReason: r.archiveReason }),
|
|
878
|
+
...(r.stayedVersion == null ? {} : { stayedVersion: r.stayedVersion }),
|
|
879
|
+
...(r.archivedAt == null ? {} : { archivedAt: r.archivedAt }),
|
|
578
880
|
...(r.curFrom == null ? {} : { from: r.curFrom }),
|
|
579
881
|
...(r.curPrompt == null ? {} : { prompt: r.curPrompt }),
|
|
580
882
|
...(r.curAuthor == null ? {} : { author: r.curAuthor }),
|
|
883
|
+
...(r.curSession == null ? {} : { publishedBy: r.curSession }),
|
|
884
|
+
...(r.amendedAt == null ? {} : { amendedAt: r.amendedAt }),
|
|
885
|
+
...(r.curContract == null ? {} : { contract: r.curContract }),
|
|
886
|
+
...(r.curSystem == null ? {} : { system: r.curSystem }),
|
|
887
|
+
...(r.rewrites ? { rewrites: r.rewrites } : {}),
|
|
581
888
|
};
|
|
582
889
|
}
|
|
583
890
|
// The BLOB comes back as an ArrayBuffer (real DO) or a Uint8Array
|
|
@@ -603,7 +910,7 @@ export class SqlStore {
|
|
|
603
910
|
id: r.id,
|
|
604
911
|
seq: r.seq,
|
|
605
912
|
sessionId: r.sessionId,
|
|
606
|
-
|
|
913
|
+
designId: r.designId ?? null,
|
|
607
914
|
postId: r.postId ?? null,
|
|
608
915
|
author: r.author,
|
|
609
916
|
text: r.text,
|
|
@@ -666,11 +973,18 @@ export class SqlStore {
|
|
|
666
973
|
this.atomic(() => {
|
|
667
974
|
this.sql.exec("DELETE FROM comments WHERE sessionId = ?", id);
|
|
668
975
|
this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE sessionId = ?)", id);
|
|
976
|
+
const drawn = JSON.stringify(this.sql
|
|
977
|
+
.exec("SELECT DISTINCT designId FROM posts WHERE sessionId = ?", id)
|
|
978
|
+
.toArray()
|
|
979
|
+
.map((r) => r.designId));
|
|
669
980
|
this.sql.exec("DELETE FROM posts WHERE sessionId = ?", id);
|
|
670
|
-
// A
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
981
|
+
// A design left with no variants has nothing to show, unless it was declared ahead
|
|
982
|
+
// of being drawn, which only goes with the session that planned it. One this
|
|
983
|
+
// session drew is not a plan, whoever wrote to it last: it would come back as one.
|
|
984
|
+
const emptied = "id NOT IN (SELECT designId FROM posts) AND (kind = 'idea' OR sessionId = ? OR id IN (SELECT value FROM json_each(?)))";
|
|
985
|
+
this.sql.exec(`DELETE FROM comments WHERE designId IN (SELECT id FROM designs WHERE ${emptied})`, id, drawn);
|
|
986
|
+
this.sql.exec(`DELETE FROM designs WHERE ${emptied}`, id, drawn);
|
|
987
|
+
this.sql.exec("UPDATE designs SET sessionId = NULL WHERE sessionId = ?", id);
|
|
674
988
|
// Posts are gone, so the referenced set reflects survivors only: drop this
|
|
675
989
|
// session's own assets except any a surviving surface still points at
|
|
676
990
|
// (assets are content-addressed and may be shared across sessions).
|
|
@@ -704,91 +1018,132 @@ export class SqlStore {
|
|
|
704
1018
|
return this.settingSync(key);
|
|
705
1019
|
}
|
|
706
1020
|
async setSetting(key, value) {
|
|
707
|
-
if (key.startsWith("design:"))
|
|
708
|
-
this.invalidateAssetRefs();
|
|
709
1021
|
this.sql.exec("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", stripNul(key), stripNul(value));
|
|
710
1022
|
}
|
|
711
|
-
// --- projects /
|
|
1023
|
+
// --- projects / designs ---
|
|
712
1024
|
async listProjects() {
|
|
713
|
-
return summarizeProjects(await this.
|
|
1025
|
+
return summarizeProjects(await this.listDesigns(), await this.listSessions());
|
|
1026
|
+
}
|
|
1027
|
+
// Spelled out because `SELECT *` on posts reads the bodies, the very cost search avoids.
|
|
1028
|
+
async listJumpRows() {
|
|
1029
|
+
return {
|
|
1030
|
+
designs: this.sql
|
|
1031
|
+
.exec("SELECT id, project, slug, title, kind, area, states, asks, widths, archivedAt, updatedAt FROM designs")
|
|
1032
|
+
.toArray()
|
|
1033
|
+
.map((r) => ({
|
|
1034
|
+
id: r.id,
|
|
1035
|
+
project: r.project,
|
|
1036
|
+
slug: r.slug,
|
|
1037
|
+
title: r.title,
|
|
1038
|
+
kind: r.kind,
|
|
1039
|
+
area: r.area ?? null,
|
|
1040
|
+
states: parseJson(r.states, []),
|
|
1041
|
+
asks: parseJson(r.asks, []),
|
|
1042
|
+
widths: parseJson(r.widths, []),
|
|
1043
|
+
archivedAt: r.archivedAt ?? null,
|
|
1044
|
+
updatedAt: r.updatedAt,
|
|
1045
|
+
})),
|
|
1046
|
+
variants: this.sql
|
|
1047
|
+
.exec("SELECT designId, state, variant, description, status FROM posts")
|
|
1048
|
+
.toArray()
|
|
1049
|
+
.map((r) => ({
|
|
1050
|
+
designId: r.designId,
|
|
1051
|
+
state: r.state ?? null,
|
|
1052
|
+
variant: r.variant,
|
|
1053
|
+
description: r.description ?? null,
|
|
1054
|
+
status: r.status,
|
|
1055
|
+
})),
|
|
1056
|
+
};
|
|
714
1057
|
}
|
|
715
|
-
async
|
|
1058
|
+
async listDesigns(project) {
|
|
716
1059
|
const rows = project === undefined
|
|
717
|
-
? this.sql.exec("SELECT * FROM
|
|
1060
|
+
? this.sql.exec("SELECT * FROM designs ORDER BY updatedAt DESC, rowid DESC").toArray()
|
|
718
1061
|
: this.sql
|
|
719
|
-
.exec("SELECT * FROM
|
|
1062
|
+
.exec("SELECT * FROM designs WHERE project = ? ORDER BY updatedAt DESC, rowid DESC", project)
|
|
720
1063
|
.toArray();
|
|
721
|
-
return rows.map((r) => this.
|
|
1064
|
+
return rows.map((r) => this.rowToDesign(r));
|
|
722
1065
|
}
|
|
723
|
-
async
|
|
724
|
-
const rows = this.sql.exec("SELECT * FROM
|
|
725
|
-
return rows.length > 0 ? this.
|
|
1066
|
+
async getDesign(id) {
|
|
1067
|
+
const rows = this.sql.exec("SELECT * FROM designs WHERE id = ?", id).toArray();
|
|
1068
|
+
return rows.length > 0 ? this.rowToDesign(rows[0]) : null;
|
|
726
1069
|
}
|
|
727
|
-
async
|
|
1070
|
+
async findDesign(project, slug) {
|
|
728
1071
|
const rows = this.sql
|
|
729
|
-
.exec("SELECT * FROM
|
|
1072
|
+
.exec("SELECT * FROM designs WHERE project = ? AND slug = ?", project, slug)
|
|
730
1073
|
.toArray();
|
|
731
|
-
return rows.length > 0 ? this.
|
|
1074
|
+
return rows.length > 0 ? this.rowToDesign(rows[0]) : null;
|
|
732
1075
|
}
|
|
733
|
-
async
|
|
1076
|
+
async createDesign(input) {
|
|
734
1077
|
const now = new Date().toISOString();
|
|
735
|
-
const
|
|
1078
|
+
const design = {
|
|
736
1079
|
id: newId(),
|
|
737
1080
|
project: stripNul(input.project).trim() || DEFAULT_PROJECT,
|
|
738
1081
|
slug: slugify(stripNul(input.slug)),
|
|
739
1082
|
title: stripNul(input.title)?.trim() || input.slug,
|
|
740
|
-
kind:
|
|
1083
|
+
kind: isDesignKind(input.kind) ? input.kind : "idea",
|
|
741
1084
|
area: label(input.area),
|
|
742
1085
|
source: label(input.source),
|
|
743
1086
|
states: (input.states ?? []).map((s) => stripNul(s)),
|
|
744
|
-
situations: {},
|
|
745
1087
|
asks: [],
|
|
746
1088
|
knobs: input.knobs ?? {},
|
|
747
1089
|
widths: input.widths ?? [],
|
|
748
1090
|
draft: null,
|
|
749
1091
|
sessionId: input.sessionId ?? null,
|
|
750
|
-
|
|
1092
|
+
shortlist: !!input.shortlist,
|
|
1093
|
+
archivedAt: null,
|
|
1094
|
+
archiveReason: null,
|
|
1095
|
+
notes: null,
|
|
1096
|
+
stateOutlines: {},
|
|
1097
|
+
answerAdditions: [],
|
|
1098
|
+
standings: {},
|
|
1099
|
+
sets: [],
|
|
751
1100
|
createdAt: now,
|
|
752
1101
|
updatedAt: now,
|
|
753
1102
|
};
|
|
754
|
-
this.sql.exec("INSERT INTO
|
|
755
|
-
return
|
|
1103
|
+
this.sql.exec("INSERT INTO designs (id, project, slug, title, kind, area, source, states, asks, knobs, widths, draft, sessionId, shortlist, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '[]', ?, ?, NULL, ?, ?, ?, ?)", design.id, design.project, design.slug, design.title, design.kind, design.area, design.source, JSON.stringify(design.states), JSON.stringify(design.knobs), JSON.stringify(design.widths), design.sessionId, design.shortlist ? 1 : 0, design.createdAt, design.updatedAt);
|
|
1104
|
+
return design;
|
|
756
1105
|
}
|
|
757
|
-
async
|
|
758
|
-
const
|
|
759
|
-
if (!
|
|
1106
|
+
async updateDesign(id, patch) {
|
|
1107
|
+
const design = await this.getDesign(id);
|
|
1108
|
+
if (!design)
|
|
760
1109
|
return null;
|
|
761
1110
|
const next = {
|
|
762
|
-
...
|
|
763
|
-
...(patch.title !== undefined && { title: stripNul(patch.title).trim() ||
|
|
1111
|
+
...design,
|
|
1112
|
+
...(patch.title !== undefined && { title: stripNul(patch.title).trim() || design.title }),
|
|
764
1113
|
...(patch.kind !== undefined && { kind: patch.kind }),
|
|
765
1114
|
...(patch.area !== undefined && { area: label(patch.area) }),
|
|
766
1115
|
...(patch.source !== undefined && { source: label(patch.source) }),
|
|
767
1116
|
...(patch.states !== undefined && { states: patch.states.map((s) => stripNul(s)) }),
|
|
768
|
-
...(patch.situations !== undefined && { situations: patch.situations }),
|
|
769
1117
|
...(patch.asks !== undefined && { asks: patch.asks }),
|
|
770
1118
|
...(patch.knobs !== undefined && { knobs: patch.knobs }),
|
|
771
1119
|
...(patch.widths !== undefined && { widths: patch.widths }),
|
|
772
1120
|
...(patch.sessionId !== undefined && { sessionId: patch.sessionId }),
|
|
773
|
-
...(patch.
|
|
1121
|
+
...(patch.archivedAt !== undefined && { archivedAt: patch.archivedAt }),
|
|
1122
|
+
...(patch.archivedAt === null && { archiveReason: null }),
|
|
1123
|
+
...(patch.archiveReason !== undefined && { archiveReason: patch.archiveReason }),
|
|
1124
|
+
...(patch.notes !== undefined && { notes: patch.notes }),
|
|
1125
|
+
...(patch.stateOutlines !== undefined && { stateOutlines: patch.stateOutlines }),
|
|
1126
|
+
...(patch.answerAdditions !== undefined && { answerAdditions: patch.answerAdditions }),
|
|
1127
|
+
...(patch.standings !== undefined && { standings: patch.standings }),
|
|
1128
|
+
...(patch.sets !== undefined && { sets: patch.sets }),
|
|
774
1129
|
updatedAt: new Date().toISOString(),
|
|
775
1130
|
};
|
|
776
|
-
this.sql.exec("UPDATE
|
|
1131
|
+
this.sql.exec("UPDATE designs SET title = ?, kind = ?, area = ?, source = ?, states = ?, asks = ?, knobs = ?, widths = ?, sessionId = ?, archivedAt = ?, archiveReason = ?, notes = ?, stateOutlines = ?, answerAdditions = ?, standings = ?, sets = ?, updatedAt = ? WHERE id = ?", next.title, next.kind, next.area, next.source, JSON.stringify(next.states), JSON.stringify(next.asks), JSON.stringify(next.knobs), JSON.stringify(next.widths), next.sessionId, next.archivedAt, next.archiveReason, next.notes ? JSON.stringify(next.notes) : null, JSON.stringify(next.stateOutlines), JSON.stringify(next.answerAdditions), JSON.stringify(next.standings), JSON.stringify(next.sets), next.updatedAt, id);
|
|
777
1132
|
if (patch.asks !== undefined)
|
|
778
1133
|
this.invalidateAssetRefs();
|
|
779
1134
|
return next;
|
|
780
1135
|
}
|
|
781
|
-
async
|
|
782
|
-
const
|
|
783
|
-
if (!
|
|
1136
|
+
async removeDesign(id) {
|
|
1137
|
+
const design = await this.getDesign(id);
|
|
1138
|
+
if (!design)
|
|
784
1139
|
return false;
|
|
785
1140
|
this.atomic(() => {
|
|
786
|
-
this.sql.exec("DELETE FROM comments WHERE
|
|
787
|
-
this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE
|
|
788
|
-
this.sql.exec("DELETE FROM posts WHERE
|
|
789
|
-
this.sql.exec("DELETE FROM
|
|
790
|
-
for (const flow of this.flowsOf(
|
|
791
|
-
const steps = flow.steps.filter((x) => x.
|
|
1141
|
+
this.sql.exec("DELETE FROM comments WHERE designId = ?", id);
|
|
1142
|
+
this.sql.exec("DELETE FROM post_versions WHERE postId IN (SELECT id FROM posts WHERE designId = ?)", id);
|
|
1143
|
+
this.sql.exec("DELETE FROM posts WHERE designId = ?", id);
|
|
1144
|
+
this.sql.exec("DELETE FROM designs WHERE id = ?", id);
|
|
1145
|
+
for (const flow of this.flowsOf(design.project)) {
|
|
1146
|
+
const steps = flow.steps.filter((x) => x.design !== design.slug);
|
|
792
1147
|
if (steps.length !== flow.steps.length)
|
|
793
1148
|
this.writeFlow(flow, steps);
|
|
794
1149
|
}
|
|
@@ -796,23 +1151,55 @@ export class SqlStore {
|
|
|
796
1151
|
this.invalidateAssetRefs();
|
|
797
1152
|
return true;
|
|
798
1153
|
}
|
|
799
|
-
async
|
|
1154
|
+
async nameFirstState(id, state) {
|
|
1155
|
+
const name = stripNul(state);
|
|
1156
|
+
const design = await this.getDesign(id);
|
|
1157
|
+
if (!design || design.states.length > 0)
|
|
1158
|
+
return null;
|
|
1159
|
+
const updatedAt = new Date().toISOString();
|
|
1160
|
+
this.atomic(() => {
|
|
1161
|
+
this.sql.exec("UPDATE posts SET state = ? WHERE designId = ? AND state IS NULL", name, id);
|
|
1162
|
+
this.sql.exec("UPDATE designs SET states = ?, updatedAt = ? WHERE id = ?", JSON.stringify([name]), updatedAt, id);
|
|
1163
|
+
for (const r of this.sql
|
|
1164
|
+
.exec("SELECT id, copiedFrom FROM posts WHERE copiedFrom IS NOT NULL AND designId IN (SELECT id FROM designs WHERE project = ?)", design.project)
|
|
1165
|
+
.toArray()) {
|
|
1166
|
+
const from = parseJson(r.copiedFrom, null);
|
|
1167
|
+
if (from?.design !== id || from.state !== null)
|
|
1168
|
+
continue;
|
|
1169
|
+
this.sql.exec("UPDATE posts SET copiedFrom = ? WHERE id = ?", JSON.stringify({ ...from, state: name }), r.id);
|
|
1170
|
+
}
|
|
1171
|
+
for (const flow of this.flowsOf(design.project)) {
|
|
1172
|
+
if (!flow.steps.some((x) => x.design === design.slug && x.state === null))
|
|
1173
|
+
continue;
|
|
1174
|
+
this.writeFlow(flow, flow.steps.map((x) => x.design === design.slug && x.state === null ? { ...x, state: name } : x));
|
|
1175
|
+
}
|
|
1176
|
+
if (design.draft) {
|
|
1177
|
+
const comments = design.draft.comments.map((c) => c.state === null ? { ...c, state: name } : c);
|
|
1178
|
+
this.sql.exec("UPDATE designs SET draft = ? WHERE id = ?", JSON.stringify({ ...design.draft, comments }), id);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
// Copies elsewhere changed too, so nothing cached can be patched one post at a time.
|
|
1182
|
+
this.postCache.clear();
|
|
1183
|
+
this.postCacheBytes = 0;
|
|
1184
|
+
return this.getDesign(id);
|
|
1185
|
+
}
|
|
1186
|
+
async renameDesign(id, slug) {
|
|
800
1187
|
const to = slugify(stripNul(slug));
|
|
801
1188
|
return this.atomic(() => {
|
|
802
|
-
const rows = this.sql.exec("SELECT * FROM
|
|
1189
|
+
const rows = this.sql.exec("SELECT * FROM designs WHERE id = ?", id).toArray();
|
|
803
1190
|
if (rows.length === 0)
|
|
804
1191
|
return null;
|
|
805
|
-
const
|
|
806
|
-
const from =
|
|
1192
|
+
const design = this.rowToDesign(rows[0]);
|
|
1193
|
+
const from = design.slug;
|
|
807
1194
|
if (to === from)
|
|
808
|
-
return
|
|
1195
|
+
return design;
|
|
809
1196
|
const taken = this.sql
|
|
810
|
-
.exec("SELECT id FROM
|
|
1197
|
+
.exec("SELECT id FROM designs WHERE project = ? AND slug = ?", design.project, to)
|
|
811
1198
|
.toArray();
|
|
812
1199
|
if (taken.length > 0)
|
|
813
1200
|
return null;
|
|
814
1201
|
const updatedAt = new Date().toISOString();
|
|
815
|
-
this.sql.exec("UPDATE
|
|
1202
|
+
this.sql.exec("UPDATE designs SET slug = ?, updatedAt = ? WHERE id = ?", to, updatedAt, id);
|
|
816
1203
|
// History is rewritten too: embeds resolve by slug at render time, so an
|
|
817
1204
|
// old version left naming the old slug would show its components missing.
|
|
818
1205
|
const rewrite = (raw) => {
|
|
@@ -829,7 +1216,7 @@ export class SqlStore {
|
|
|
829
1216
|
return changed ? JSON.stringify(next) : null;
|
|
830
1217
|
};
|
|
831
1218
|
const posts = this.sql
|
|
832
|
-
.exec("SELECT id, surfaces, slots FROM posts WHERE
|
|
1219
|
+
.exec("SELECT id, surfaces, slots FROM posts WHERE designId IN (SELECT id FROM designs WHERE project = ?)", design.project)
|
|
833
1220
|
.toArray();
|
|
834
1221
|
for (const r of posts) {
|
|
835
1222
|
const postId = r.id;
|
|
@@ -849,25 +1236,25 @@ export class SqlStore {
|
|
|
849
1236
|
this.sql.exec("UPDATE post_versions SET surfaces = ? WHERE postId = ? AND version = ?", old, postId, v.version);
|
|
850
1237
|
}
|
|
851
1238
|
}
|
|
852
|
-
for (const flow of this.flowsOf(
|
|
853
|
-
if (!flow.steps.some((x) => x.
|
|
1239
|
+
for (const flow of this.flowsOf(design.project)) {
|
|
1240
|
+
if (!flow.steps.some((x) => x.design === from))
|
|
854
1241
|
continue;
|
|
855
|
-
this.writeFlow(flow, flow.steps.map((x) => (x.
|
|
1242
|
+
this.writeFlow(flow, flow.steps.map((x) => (x.design === from ? { ...x, design: to } : x)));
|
|
856
1243
|
}
|
|
857
|
-
return { ...
|
|
1244
|
+
return { ...design, slug: to, updatedAt };
|
|
858
1245
|
});
|
|
859
1246
|
}
|
|
860
1247
|
async renameProject(from, to) {
|
|
861
1248
|
const target = stripNul(to).trim();
|
|
862
1249
|
return this.atomic(() => {
|
|
863
1250
|
const occupied = this.sql
|
|
864
|
-
.exec("SELECT COUNT(*) AS n FROM
|
|
1251
|
+
.exec("SELECT COUNT(*) AS n FROM designs WHERE project = ?", target)
|
|
865
1252
|
.one().n;
|
|
866
1253
|
if (occupied > 0)
|
|
867
1254
|
return false;
|
|
868
|
-
// A project with no
|
|
1255
|
+
// A project with no designs has nothing a leftover flow could walk.
|
|
869
1256
|
this.sql.exec("DELETE FROM flows WHERE project = ?", target);
|
|
870
|
-
this.sql.exec("UPDATE
|
|
1257
|
+
this.sql.exec("UPDATE designs SET project = ? WHERE project = ?", target, from);
|
|
871
1258
|
this.sql.exec("UPDATE sessions SET project = ? WHERE project = ?", target, from);
|
|
872
1259
|
this.sql.exec("UPDATE flows SET project = ? WHERE project = ?", target, from);
|
|
873
1260
|
for (const prefix of PROJECT_SETTINGS) {
|
|
@@ -876,12 +1263,98 @@ export class SqlStore {
|
|
|
876
1263
|
continue;
|
|
877
1264
|
this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", `${prefix}:${target}`, value);
|
|
878
1265
|
this.sql.exec("DELETE FROM settings WHERE key = ?", `${prefix}:${from}`);
|
|
879
|
-
|
|
880
|
-
|
|
1266
|
+
}
|
|
1267
|
+
if (this.sql.exec("SELECT 1 FROM systems WHERE project = ?", from).toArray().length) {
|
|
1268
|
+
this.sql.exec("DELETE FROM systems WHERE project = ?", target);
|
|
1269
|
+
this.sql.exec("DELETE FROM system_revisions WHERE project = ?", target);
|
|
1270
|
+
for (const table of ["systems", "system_revisions"]) {
|
|
1271
|
+
for (const r of this.sql
|
|
1272
|
+
.exec(`SELECT rowid AS id, json FROM ${table} WHERE project = ?`, from)
|
|
1273
|
+
.toArray()) {
|
|
1274
|
+
const moved = { ...parseJson(r.json, {}), project: target };
|
|
1275
|
+
this.sql.exec(`UPDATE ${table} SET project = ?, json = ? WHERE rowid = ?`, target, JSON.stringify(moved), r.id);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
881
1278
|
}
|
|
882
1279
|
return true;
|
|
883
1280
|
});
|
|
884
1281
|
}
|
|
1282
|
+
// --- systems ---
|
|
1283
|
+
async getSystem(project) {
|
|
1284
|
+
const rows = this.sql.exec("SELECT json FROM systems WHERE project = ?", project).toArray();
|
|
1285
|
+
return rows.length ? normalizeSystem(project, parseJson(rows[0].json, null)) : null;
|
|
1286
|
+
}
|
|
1287
|
+
async putSystem(system, meta) {
|
|
1288
|
+
this.atomic(() => this.writeSystem(system, meta));
|
|
1289
|
+
this.invalidateAssetRefs();
|
|
1290
|
+
return system;
|
|
1291
|
+
}
|
|
1292
|
+
async commitSystemDecision(input) {
|
|
1293
|
+
const tell = input.tell;
|
|
1294
|
+
const comment = this.atomic(() => {
|
|
1295
|
+
this.writeSystem(input.system, { sessionId: null, summary: input.summary });
|
|
1296
|
+
return tell
|
|
1297
|
+
? this.insertComment({
|
|
1298
|
+
sessionId: tell.sessionId,
|
|
1299
|
+
author: "user",
|
|
1300
|
+
text: "",
|
|
1301
|
+
kind: "reply",
|
|
1302
|
+
payload: tell.payload,
|
|
1303
|
+
})
|
|
1304
|
+
: null;
|
|
1305
|
+
});
|
|
1306
|
+
this.invalidateAssetRefs();
|
|
1307
|
+
return comment;
|
|
1308
|
+
}
|
|
1309
|
+
// Unwrapped, so a reply's transaction can carry it.
|
|
1310
|
+
writeSystem(system, meta) {
|
|
1311
|
+
const json = JSON.stringify(system);
|
|
1312
|
+
const at = new Date().toISOString();
|
|
1313
|
+
{
|
|
1314
|
+
this.sql.exec("INSERT INTO systems (project, rev, json) VALUES (?, ?, ?) ON CONFLICT(project) DO UPDATE SET rev = excluded.rev, json = excluded.json", system.project, system.rev, json);
|
|
1315
|
+
this.sql.exec("INSERT OR REPLACE INTO system_revisions (project, rev, json, at, sessionId, summary) VALUES (?, ?, ?, ?, ?, ?)", system.project, system.rev, json, at, meta.sessionId, stripNul(meta.summary).slice(0, SYSTEM_LIMITS.summary));
|
|
1316
|
+
this.sql.exec("DELETE FROM system_revisions WHERE project = ? AND rev <= ?", system.project, system.rev - SYSTEM_LIMITS.revisions);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async listSystemRevisions(project) {
|
|
1320
|
+
return this.sql
|
|
1321
|
+
.exec("SELECT rev, at, sessionId, summary FROM system_revisions WHERE project = ? ORDER BY rev DESC", project)
|
|
1322
|
+
.toArray()
|
|
1323
|
+
.map((r) => ({
|
|
1324
|
+
rev: r.rev,
|
|
1325
|
+
at: r.at,
|
|
1326
|
+
sessionId: r.sessionId ?? null,
|
|
1327
|
+
summary: r.summary,
|
|
1328
|
+
}));
|
|
1329
|
+
}
|
|
1330
|
+
async getSystemRevision(project, rev) {
|
|
1331
|
+
const rows = this.sql
|
|
1332
|
+
.exec("SELECT json FROM system_revisions WHERE project = ? AND rev = ?", project, rev)
|
|
1333
|
+
.toArray();
|
|
1334
|
+
return rows.length ? normalizeSystem(project, parseJson(rows[0].json, null)) : null;
|
|
1335
|
+
}
|
|
1336
|
+
// Each project's old design settings become its system, with the repo's
|
|
1337
|
+
// tokens derived, and the setting goes.
|
|
1338
|
+
migrateSystems() {
|
|
1339
|
+
const rows = this.sql
|
|
1340
|
+
.exec("SELECT key, value FROM settings WHERE key LIKE 'design:%'")
|
|
1341
|
+
.toArray();
|
|
1342
|
+
if (rows.length === 0)
|
|
1343
|
+
return;
|
|
1344
|
+
const at = new Date().toISOString();
|
|
1345
|
+
for (const r of rows) {
|
|
1346
|
+
const project = r.key.slice("design:".length);
|
|
1347
|
+
const have = this.sql.exec("SELECT 1 FROM systems WHERE project = ?", project).toArray();
|
|
1348
|
+
if (have.length === 0) {
|
|
1349
|
+
const system = { ...systemFromDesignSettings(project, parseJson(r.value, null)), rev: 1 };
|
|
1350
|
+
const json = JSON.stringify(system);
|
|
1351
|
+
this.sql.exec("INSERT INTO systems (project, rev, json) VALUES (?, 1, ?)", project, json);
|
|
1352
|
+
this.sql.exec("INSERT INTO system_revisions (project, rev, json, at, sessionId, summary) VALUES (?, 1, ?, ?, NULL, ?)", project, json, at, "from what the project stored before");
|
|
1353
|
+
}
|
|
1354
|
+
this.sql.exec("DELETE FROM settings WHERE key = ?", r.key);
|
|
1355
|
+
}
|
|
1356
|
+
this.invalidateAssetRefs();
|
|
1357
|
+
}
|
|
885
1358
|
// --- flows ---
|
|
886
1359
|
flowsOf(project) {
|
|
887
1360
|
return this.sql
|
|
@@ -909,7 +1382,7 @@ export class SqlStore {
|
|
|
909
1382
|
return this.flowsOf(project);
|
|
910
1383
|
}
|
|
911
1384
|
async placeFlowStep(project, name, screen, step) {
|
|
912
|
-
const clean = {
|
|
1385
|
+
const clean = { design: stripNul(screen.design), state: stripNul(screen.state) };
|
|
913
1386
|
const label = stripNul(name);
|
|
914
1387
|
return this.atomic(() => {
|
|
915
1388
|
const now = new Date().toISOString();
|
|
@@ -928,24 +1401,27 @@ export class SqlStore {
|
|
|
928
1401
|
return this.writeFlow(flow, steps);
|
|
929
1402
|
});
|
|
930
1403
|
}
|
|
931
|
-
async putDraft(
|
|
932
|
-
const
|
|
933
|
-
if (!
|
|
1404
|
+
async putDraft(designId, draft) {
|
|
1405
|
+
const design = await this.getDesign(designId);
|
|
1406
|
+
if (!design)
|
|
934
1407
|
return null;
|
|
935
|
-
this.sql.exec("UPDATE
|
|
1408
|
+
this.sql.exec("UPDATE designs SET draft = ? WHERE id = ?", draft ? JSON.stringify(draft) : null, designId);
|
|
936
1409
|
this.invalidateAssetRefs();
|
|
937
|
-
return { ...
|
|
1410
|
+
return { ...design, draft };
|
|
938
1411
|
}
|
|
939
|
-
|
|
940
|
-
this.sql.exec("UPDATE
|
|
1412
|
+
touchDesign(designId, at) {
|
|
1413
|
+
this.sql.exec("UPDATE designs SET updatedAt = ? WHERE id = ?", at, designId);
|
|
941
1414
|
}
|
|
942
1415
|
// --- posts ---
|
|
943
1416
|
async listPosts(query = {}) {
|
|
1417
|
+
if (query.designId !== undefined && query.sessionId === undefined) {
|
|
1418
|
+
return this.postsOfDesign(query.designId).slice();
|
|
1419
|
+
}
|
|
944
1420
|
const clauses = [];
|
|
945
1421
|
const params = [];
|
|
946
|
-
if (query.
|
|
947
|
-
clauses.push("
|
|
948
|
-
params.push(query.
|
|
1422
|
+
if (query.designId !== undefined) {
|
|
1423
|
+
clauses.push("designId = ?");
|
|
1424
|
+
params.push(query.designId);
|
|
949
1425
|
}
|
|
950
1426
|
if (query.sessionId !== undefined) {
|
|
951
1427
|
clauses.push("sessionId = ?");
|
|
@@ -957,6 +1433,129 @@ export class SqlStore {
|
|
|
957
1433
|
.toArray();
|
|
958
1434
|
return this.rowsToPosts(rows);
|
|
959
1435
|
}
|
|
1436
|
+
async listPostHeads(project) {
|
|
1437
|
+
const scope = project === undefined ? "" : "WHERE designId IN (SELECT id FROM designs WHERE project = ?)";
|
|
1438
|
+
const args = project === undefined ? [] : [project];
|
|
1439
|
+
const history = new Map();
|
|
1440
|
+
for (const r of this.sql
|
|
1441
|
+
.exec(`SELECT postId, version, head FROM post_versions WHERE postId IN (SELECT id FROM posts ${scope}) ORDER BY version ASC`, ...args)
|
|
1442
|
+
.toArray()) {
|
|
1443
|
+
const id = r.postId;
|
|
1444
|
+
let list = history.get(id);
|
|
1445
|
+
if (!list)
|
|
1446
|
+
history.set(id, (list = []));
|
|
1447
|
+
list.push({ version: r.version, surfaces: parseJson(r.head, []) });
|
|
1448
|
+
}
|
|
1449
|
+
return this.sql
|
|
1450
|
+
.exec(`SELECT ${POST_HEAD_COLUMNS} FROM posts ${scope} ORDER BY createdAt ASC, rowid ASC`, ...args)
|
|
1451
|
+
.toArray()
|
|
1452
|
+
.map((r) => ({
|
|
1453
|
+
...this.rowToPost(r, []),
|
|
1454
|
+
surfaces: parseJson(r.head, []),
|
|
1455
|
+
history: history.get(r.id) ?? [],
|
|
1456
|
+
}));
|
|
1457
|
+
}
|
|
1458
|
+
// Every open design screen asks, and a project's posts can be hundreds of MB:
|
|
1459
|
+
// the index holds each column read here, so no row with a body is touched.
|
|
1460
|
+
async listCopies(project) {
|
|
1461
|
+
return this.sql
|
|
1462
|
+
.exec("SELECT designId, state, variant, status, copiedFrom FROM posts WHERE copiedFrom IS NOT NULL AND designId IN (SELECT id FROM designs WHERE project = ?)", project)
|
|
1463
|
+
.toArray()
|
|
1464
|
+
.filter((r) => isLive(r.status))
|
|
1465
|
+
.map((r) => ({
|
|
1466
|
+
design: r.designId,
|
|
1467
|
+
state: r.state,
|
|
1468
|
+
variant: r.variant,
|
|
1469
|
+
copiedFrom: parseJson(r.copiedFrom, null),
|
|
1470
|
+
}));
|
|
1471
|
+
}
|
|
1472
|
+
postsOfDesign(designId) {
|
|
1473
|
+
const hit = this.postCache.get(designId);
|
|
1474
|
+
if (hit) {
|
|
1475
|
+
this.postCache.delete(designId);
|
|
1476
|
+
this.postCache.set(designId, hit);
|
|
1477
|
+
return hit.posts;
|
|
1478
|
+
}
|
|
1479
|
+
const rows = this.sql
|
|
1480
|
+
.exec("SELECT * FROM posts WHERE designId = ? ORDER BY createdAt ASC, rowid ASC", designId)
|
|
1481
|
+
.toArray();
|
|
1482
|
+
let bytes = 0;
|
|
1483
|
+
for (const r of rows)
|
|
1484
|
+
bytes += r.surfaces.length;
|
|
1485
|
+
const posts = this.rowsToPosts(rows, (n) => (bytes += n));
|
|
1486
|
+
if (bytes > this.postCacheLimit)
|
|
1487
|
+
return posts;
|
|
1488
|
+
this.postCache.set(designId, { posts, bytes });
|
|
1489
|
+
this.postCacheBytes += bytes;
|
|
1490
|
+
this.trimPostCache();
|
|
1491
|
+
return posts;
|
|
1492
|
+
}
|
|
1493
|
+
trimPostCache() {
|
|
1494
|
+
for (const [id, entry] of this.postCache) {
|
|
1495
|
+
if (this.postCacheBytes <= this.postCacheLimit)
|
|
1496
|
+
break;
|
|
1497
|
+
this.postCache.delete(id);
|
|
1498
|
+
this.postCacheBytes -= entry.bytes;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
// A synchronous write to these posts of one design, re-reading just them after:
|
|
1502
|
+
// a publish into a big lab would otherwise leave the viewer's next read parsing
|
|
1503
|
+
// every design in it.
|
|
1504
|
+
writingPosts(designId, ids, write) {
|
|
1505
|
+
this.holdPostDrop = true;
|
|
1506
|
+
let out;
|
|
1507
|
+
try {
|
|
1508
|
+
out = write();
|
|
1509
|
+
}
|
|
1510
|
+
catch (err) {
|
|
1511
|
+
const entry = this.postCache.get(designId);
|
|
1512
|
+
if (entry) {
|
|
1513
|
+
this.postCache.delete(designId);
|
|
1514
|
+
this.postCacheBytes -= entry.bytes;
|
|
1515
|
+
}
|
|
1516
|
+
throw err;
|
|
1517
|
+
}
|
|
1518
|
+
finally {
|
|
1519
|
+
this.holdPostDrop = false;
|
|
1520
|
+
}
|
|
1521
|
+
const entry = this.postCache.get(designId);
|
|
1522
|
+
if (entry) {
|
|
1523
|
+
const weigh = (p) => p
|
|
1524
|
+
? [p.surfaces, ...p.history.map((h) => h.surfaces)].reduce((n, x) => n + JSON.stringify(x).length, 0)
|
|
1525
|
+
: 0;
|
|
1526
|
+
for (const id of ids) {
|
|
1527
|
+
const rows = this.sql.exec("SELECT * FROM posts WHERE id = ?", id).toArray();
|
|
1528
|
+
const fresh = rows.length ? this.rowsToPosts(rows)[0] : null;
|
|
1529
|
+
const stale = entry.posts.find((p) => p.id === id);
|
|
1530
|
+
const change = weigh(fresh) - weigh(stale);
|
|
1531
|
+
entry.bytes += change;
|
|
1532
|
+
this.postCacheBytes += change;
|
|
1533
|
+
if (!fresh)
|
|
1534
|
+
entry.posts = entry.posts.filter((p) => p.id !== id);
|
|
1535
|
+
else if (stale)
|
|
1536
|
+
entry.posts = entry.posts.map((p) => (p.id === id ? fresh : p));
|
|
1537
|
+
else
|
|
1538
|
+
entry.posts = [...entry.posts, fresh];
|
|
1539
|
+
}
|
|
1540
|
+
this.trimPostCache();
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
cachedPost(id) {
|
|
1545
|
+
for (const { posts } of this.postCache.values()) {
|
|
1546
|
+
const hit = posts.find((p) => p.id === id);
|
|
1547
|
+
if (hit)
|
|
1548
|
+
return hit;
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
// For a write that leaves the bodies alone, so the size stands. Archiving
|
|
1553
|
+
// through a lab would otherwise re-read every design in it after each ×.
|
|
1554
|
+
recache(post) {
|
|
1555
|
+
const entry = this.postCache.get(post.design);
|
|
1556
|
+
if (entry)
|
|
1557
|
+
entry.posts = entry.posts.map((p) => (p.id === post.id ? post : p));
|
|
1558
|
+
}
|
|
960
1559
|
async countPostsBySession() {
|
|
961
1560
|
const counts = new Map();
|
|
962
1561
|
for (const row of this.sql
|
|
@@ -970,22 +1569,25 @@ export class SqlStore {
|
|
|
970
1569
|
const rows = this.sql.exec("SELECT * FROM posts WHERE id = ?", id).toArray();
|
|
971
1570
|
return rows.length > 0 ? this.rowsToPosts(rows)[0] : null;
|
|
972
1571
|
}
|
|
973
|
-
async findPost(
|
|
1572
|
+
async findPost(designId, state, variant) {
|
|
1573
|
+
const cached = this.postCache.get(designId);
|
|
1574
|
+
if (cached)
|
|
1575
|
+
return cached.posts.find((p) => p.state === state && p.variant === variant) ?? null;
|
|
974
1576
|
const rows = this.sql
|
|
975
|
-
.exec("SELECT * FROM posts WHERE
|
|
1577
|
+
.exec("SELECT * FROM posts WHERE designId = ? AND state IS ? AND variant = ? ORDER BY createdAt ASC LIMIT 1", designId, state, variant)
|
|
976
1578
|
.toArray();
|
|
977
1579
|
return rows.length > 0 ? this.rowsToPosts(rows)[0] : null;
|
|
978
1580
|
}
|
|
979
1581
|
async createPost(input) {
|
|
980
1582
|
if (!(await this.getSession(input.sessionId)))
|
|
981
1583
|
return null;
|
|
982
|
-
if (!(await this.
|
|
1584
|
+
if (!(await this.getDesign(input.design)))
|
|
983
1585
|
return null;
|
|
984
1586
|
const now = new Date().toISOString();
|
|
985
1587
|
const post = {
|
|
986
1588
|
id: newId(),
|
|
987
1589
|
sessionId: input.sessionId,
|
|
988
|
-
|
|
1590
|
+
design: input.design,
|
|
989
1591
|
state: input.state === null ? null : stripNul(input.state),
|
|
990
1592
|
variant: stripNul(input.variant)?.trim() || DEFAULT_VARIANT,
|
|
991
1593
|
status: "open",
|
|
@@ -996,16 +1598,22 @@ export class SqlStore {
|
|
|
996
1598
|
version: 1,
|
|
997
1599
|
history: [],
|
|
998
1600
|
...(input.knobs ? { knobs: input.knobs } : {}),
|
|
999
|
-
...(stripNul(input.
|
|
1601
|
+
...(stripNul(input.description ?? "").trim()
|
|
1602
|
+
? { description: stripNul(input.description ?? "").trim() }
|
|
1603
|
+
: {}),
|
|
1000
1604
|
embeds: input.embeds ?? [],
|
|
1001
1605
|
...(input.forkedFrom ? { forkedFrom: input.forkedFrom } : {}),
|
|
1606
|
+
...(input.copiedFrom ? { copiedFrom: input.copiedFrom } : {}),
|
|
1002
1607
|
...(input.from === undefined ? {} : { from: input.from }),
|
|
1003
1608
|
...(input.prompt === undefined ? {} : { prompt: stripNul(input.prompt) }),
|
|
1004
1609
|
...(input.author === undefined ? {} : { author: stripNul(input.author) }),
|
|
1610
|
+
publishedBy: input.publishedBy ?? input.sessionId,
|
|
1611
|
+
contract: input.contract ?? CURRENT_CONTRACT,
|
|
1612
|
+
...(input.system === undefined ? {} : { system: input.system }),
|
|
1005
1613
|
};
|
|
1006
|
-
this.sql.exec("INSERT INTO posts (id, sessionId,
|
|
1614
|
+
this.writingPosts(post.design, [post.id], () => this.sql.exec("INSERT INTO posts (id, sessionId, designId, state, variant, status, title, surfaces, head, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, description, forkedFrom, copiedFrom, curSession, curContract, curSystem) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.design, post.state, post.variant, post.title, JSON.stringify(post.surfaces), surfaceHeads(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.description ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null, post.copiedFrom ? JSON.stringify(post.copiedFrom) : null, post.publishedBy ?? null, post.contract ?? null, post.system ?? null));
|
|
1007
1615
|
this.touch(input.sessionId);
|
|
1008
|
-
this.
|
|
1616
|
+
this.touchDesign(post.design, now);
|
|
1009
1617
|
this.addAssetRefs(post.surfaces);
|
|
1010
1618
|
return post;
|
|
1011
1619
|
}
|
|
@@ -1024,6 +1632,10 @@ export class SqlStore {
|
|
|
1024
1632
|
...(post.from === undefined ? {} : { from: post.from }),
|
|
1025
1633
|
...(post.prompt === undefined ? {} : { prompt: post.prompt }),
|
|
1026
1634
|
...(post.author === undefined ? {} : { author: post.author }),
|
|
1635
|
+
...(post.publishedBy === undefined ? {} : { publishedBy: post.publishedBy }),
|
|
1636
|
+
...(post.amendedAt === undefined ? {} : { amendedAt: post.amendedAt }),
|
|
1637
|
+
...(post.contract === undefined ? {} : { contract: post.contract }),
|
|
1638
|
+
...(post.system === undefined ? {} : { system: post.system }),
|
|
1027
1639
|
};
|
|
1028
1640
|
// The version in use is kept past the limit: export and embeds resolve to it.
|
|
1029
1641
|
const pin = post.acceptedVersion ?? -1;
|
|
@@ -1034,7 +1646,9 @@ export class SqlStore {
|
|
|
1034
1646
|
const title = patch.title !== undefined ? stripNul(patch.title).trim() || post.title : post.title;
|
|
1035
1647
|
const surfaces = patch.surfaces !== undefined ? normalizeSurfaceIds(patch.surfaces) : post.surfaces;
|
|
1036
1648
|
const knobs = patch.knobs === undefined ? post.knobs : (patch.knobs ?? undefined);
|
|
1037
|
-
const
|
|
1649
|
+
const description = patch.description === undefined
|
|
1650
|
+
? post.description
|
|
1651
|
+
: stripNul(patch.description).trim() || undefined;
|
|
1038
1652
|
const embeds = patch.embeds ?? post.embeds;
|
|
1039
1653
|
const state = patch.state === undefined ? post.state : patch.state;
|
|
1040
1654
|
const version = post.version + 1;
|
|
@@ -1043,22 +1657,31 @@ export class SqlStore {
|
|
|
1043
1657
|
const from = patch.from ?? post.version;
|
|
1044
1658
|
const prompt = stripNul(patch.prompt ?? "");
|
|
1045
1659
|
const author = patch.author === undefined ? null : stripNul(patch.author);
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1660
|
+
const publishedBy = patch.publishedBy ?? null;
|
|
1661
|
+
const contract = patch.contract ?? CURRENT_CONTRACT;
|
|
1662
|
+
const system = patch.system ?? null;
|
|
1663
|
+
const wrote = this.writingPosts(post.design, [id], () => {
|
|
1664
|
+
this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = NULL, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, publishedBy, contract, system, id, post.version);
|
|
1665
|
+
if (this.sql.exec("SELECT changes() AS n").one().n === 0)
|
|
1666
|
+
return false;
|
|
1049
1667
|
this.insertVersion(id, archived);
|
|
1050
1668
|
this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version <= ? AND version != ?", id, version - 1 - HISTORY_LIMIT, pin);
|
|
1669
|
+
return true;
|
|
1670
|
+
});
|
|
1671
|
+
if (wrote) {
|
|
1051
1672
|
this.touch(post.sessionId);
|
|
1052
|
-
this.
|
|
1673
|
+
this.touchDesign(post.design, updatedAt);
|
|
1053
1674
|
if (patch.surfaces !== undefined)
|
|
1054
1675
|
this.addAssetRefs(surfaces);
|
|
1055
|
-
const { knobs: _knobs,
|
|
1676
|
+
const { knobs: _knobs, description: _description, author: _author, publishedBy: _by, amendedAt: _amended, contract: _contract, system: _system, ...rest } = post;
|
|
1056
1677
|
return {
|
|
1057
1678
|
...rest,
|
|
1679
|
+
contract,
|
|
1680
|
+
...(system === null ? {} : { system }),
|
|
1058
1681
|
title,
|
|
1059
1682
|
surfaces,
|
|
1060
1683
|
...(knobs ? { knobs } : {}),
|
|
1061
|
-
...(
|
|
1684
|
+
...(description ? { description } : {}),
|
|
1062
1685
|
embeds,
|
|
1063
1686
|
state,
|
|
1064
1687
|
version,
|
|
@@ -1067,11 +1690,136 @@ export class SqlStore {
|
|
|
1067
1690
|
from,
|
|
1068
1691
|
prompt,
|
|
1069
1692
|
...(author === null ? {} : { author }),
|
|
1693
|
+
...(publishedBy === null ? {} : { publishedBy }),
|
|
1070
1694
|
};
|
|
1071
1695
|
}
|
|
1072
1696
|
}
|
|
1073
1697
|
return null;
|
|
1074
1698
|
}
|
|
1699
|
+
async amendPost(id, version, patch) {
|
|
1700
|
+
const post = await this.getPost(id);
|
|
1701
|
+
if (!post || post.version !== version)
|
|
1702
|
+
return null;
|
|
1703
|
+
const title = patch.title !== undefined ? stripNul(patch.title).trim() || post.title : post.title;
|
|
1704
|
+
const surfaces = patch.surfaces !== undefined ? normalizeSurfaceIds(patch.surfaces) : post.surfaces;
|
|
1705
|
+
const knobs = patch.knobs === undefined ? post.knobs : (patch.knobs ?? undefined);
|
|
1706
|
+
const description = patch.description === undefined
|
|
1707
|
+
? post.description
|
|
1708
|
+
: stripNul(patch.description).trim() || undefined;
|
|
1709
|
+
const embeds = patch.embeds ?? post.embeds;
|
|
1710
|
+
const from = patch.from ?? post.from;
|
|
1711
|
+
const prompt = patch.prompt === undefined ? post.prompt : stripNul(patch.prompt);
|
|
1712
|
+
const author = patch.author === undefined ? post.author : stripNul(patch.author);
|
|
1713
|
+
const amendedAt = new Date().toISOString();
|
|
1714
|
+
const rewrites = (post.rewrites ?? 0) + 1;
|
|
1715
|
+
const contract = patch.contract ?? CURRENT_CONTRACT;
|
|
1716
|
+
const system = patch.system ?? post.system;
|
|
1717
|
+
const wrote = this.writingPosts(post.design, [id], () => {
|
|
1718
|
+
this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, amendedAt = ?, rewrites = ?, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), from ?? null, prompt ?? null, author ?? null, amendedAt, rewrites, contract, system ?? null, id, version);
|
|
1719
|
+
return this.sql.exec("SELECT changes() AS n").one().n > 0;
|
|
1720
|
+
});
|
|
1721
|
+
if (!wrote)
|
|
1722
|
+
return null;
|
|
1723
|
+
this.touch(post.sessionId);
|
|
1724
|
+
this.touchDesign(post.design, amendedAt);
|
|
1725
|
+
// The surfaces it replaced may have held the only reference to an asset.
|
|
1726
|
+
this.invalidateAssetRefs();
|
|
1727
|
+
const { knobs: _knobs, description: _description, from: _from, prompt: _prompt, author: _author, ...rest } = post;
|
|
1728
|
+
return {
|
|
1729
|
+
...rest,
|
|
1730
|
+
title,
|
|
1731
|
+
surfaces,
|
|
1732
|
+
...(knobs ? { knobs } : {}),
|
|
1733
|
+
...(description ? { description } : {}),
|
|
1734
|
+
embeds,
|
|
1735
|
+
...(from === undefined ? {} : { from }),
|
|
1736
|
+
...(prompt === undefined ? {} : { prompt }),
|
|
1737
|
+
...(author === undefined ? {} : { author }),
|
|
1738
|
+
amendedAt,
|
|
1739
|
+
rewrites,
|
|
1740
|
+
contract,
|
|
1741
|
+
...(system === undefined ? {} : { system }),
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
async setPosterOnce(id, version, poster, mode, system) {
|
|
1745
|
+
const post = await this.getPost(id);
|
|
1746
|
+
if (!post || post.version !== version)
|
|
1747
|
+
return false;
|
|
1748
|
+
const at = post.surfaces.findIndex((s) => s.kind === "html");
|
|
1749
|
+
const page = post.surfaces[at];
|
|
1750
|
+
if (!page || page.kind !== "html")
|
|
1751
|
+
return false;
|
|
1752
|
+
// A still mockpit took under another system revision no longer shows, so a new
|
|
1753
|
+
// one may take its place; an agent's own, or a current one, stays.
|
|
1754
|
+
const stale = page.posterMode !== undefined &&
|
|
1755
|
+
system !== undefined &&
|
|
1756
|
+
(page.posterSystem === undefined ? system > 1 : page.posterSystem !== system);
|
|
1757
|
+
if (page.poster && !stale)
|
|
1758
|
+
return false;
|
|
1759
|
+
const surfaces = post.surfaces.map((s, i) => i === at
|
|
1760
|
+
? {
|
|
1761
|
+
...page,
|
|
1762
|
+
poster,
|
|
1763
|
+
...(mode ? { posterMode: mode } : {}),
|
|
1764
|
+
...(mode && system !== undefined ? { posterSystem: system } : {}),
|
|
1765
|
+
}
|
|
1766
|
+
: s);
|
|
1767
|
+
const wrote = this.writingPosts(post.design, [id], () => {
|
|
1768
|
+
this.sql.exec("UPDATE posts SET surfaces = ?, head = ? WHERE id = ? AND version = ?", JSON.stringify(surfaces), surfaceHeads(surfaces), id, version);
|
|
1769
|
+
return this.sql.exec("SELECT changes() AS n").one().n > 0;
|
|
1770
|
+
});
|
|
1771
|
+
if (wrote)
|
|
1772
|
+
this.invalidateAssetRefs();
|
|
1773
|
+
return wrote;
|
|
1774
|
+
}
|
|
1775
|
+
async withdrawVersion(id, version, embeds) {
|
|
1776
|
+
const post = await this.getPost(id);
|
|
1777
|
+
if (!post || post.version !== version)
|
|
1778
|
+
return null;
|
|
1779
|
+
const previous = post.history.at(-1);
|
|
1780
|
+
const now = new Date().toISOString();
|
|
1781
|
+
if (!previous) {
|
|
1782
|
+
this.writingPosts(post.design, [id], () => this.atomic(() => {
|
|
1783
|
+
// Only the agent can have written on a version the user never touched;
|
|
1784
|
+
// its messages stay in the design's thread.
|
|
1785
|
+
this.sql.exec("UPDATE comments SET postId = NULL WHERE postId = ?", id);
|
|
1786
|
+
this.sql.exec("DELETE FROM post_versions WHERE postId = ?", id);
|
|
1787
|
+
this.sql.exec("DELETE FROM posts WHERE id = ?", id);
|
|
1788
|
+
}));
|
|
1789
|
+
this.touchDesign(post.design, now);
|
|
1790
|
+
this.invalidateAssetRefs();
|
|
1791
|
+
return { post: null };
|
|
1792
|
+
}
|
|
1793
|
+
const rewrites = (post.rewrites ?? 0) + 1;
|
|
1794
|
+
const hidden = (post.hiddenVersions ?? []).filter((v) => v !== version);
|
|
1795
|
+
this.writingPosts(post.design, [id], () => this.atomic(() => {
|
|
1796
|
+
this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, slots = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = ?, rewrites = ?, hiddenVersions = ?, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", previous.title, JSON.stringify(previous.surfaces), surfaceHeads(previous.surfaces), JSON.stringify(embeds), previous.at, previous.version, previous.from ?? null, previous.prompt ?? null, previous.author ?? null, previous.publishedBy ?? null, previous.amendedAt ?? null, rewrites, hidden.length ? JSON.stringify(hidden) : null, previous.contract ?? null, previous.system ?? null, id, version);
|
|
1797
|
+
this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version = ?", id, previous.version);
|
|
1798
|
+
}));
|
|
1799
|
+
this.touchDesign(post.design, now);
|
|
1800
|
+
this.invalidateAssetRefs();
|
|
1801
|
+
const { from: _from, prompt: _prompt, author: _author, publishedBy: _by, amendedAt: _amended, hiddenVersions: _hidden, contract: _contract, system: _system, ...rest } = post;
|
|
1802
|
+
return {
|
|
1803
|
+
post: {
|
|
1804
|
+
...rest,
|
|
1805
|
+
title: previous.title,
|
|
1806
|
+
surfaces: previous.surfaces,
|
|
1807
|
+
embeds,
|
|
1808
|
+
updatedAt: previous.at,
|
|
1809
|
+
version: previous.version,
|
|
1810
|
+
history: post.history.slice(0, -1),
|
|
1811
|
+
...(previous.from === undefined ? {} : { from: previous.from }),
|
|
1812
|
+
...(previous.prompt === undefined ? {} : { prompt: previous.prompt }),
|
|
1813
|
+
...(previous.author === undefined ? {} : { author: previous.author }),
|
|
1814
|
+
...(previous.publishedBy === undefined ? {} : { publishedBy: previous.publishedBy }),
|
|
1815
|
+
...(previous.amendedAt === undefined ? {} : { amendedAt: previous.amendedAt }),
|
|
1816
|
+
...(hidden.length ? { hiddenVersions: hidden } : {}),
|
|
1817
|
+
...(previous.contract === undefined ? {} : { contract: previous.contract }),
|
|
1818
|
+
...(previous.system === undefined ? {} : { system: previous.system }),
|
|
1819
|
+
rewrites,
|
|
1820
|
+
},
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1075
1823
|
async removePost(id) {
|
|
1076
1824
|
if (!(await this.getPost(id)))
|
|
1077
1825
|
return false;
|
|
@@ -1084,24 +1832,61 @@ export class SqlStore {
|
|
|
1084
1832
|
return true;
|
|
1085
1833
|
}
|
|
1086
1834
|
async setPostStatus(id, status, acceptedVersion) {
|
|
1087
|
-
const post = await this.getPost(id);
|
|
1835
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1088
1836
|
if (!post)
|
|
1089
1837
|
return null;
|
|
1090
1838
|
const pin = status === "accepted" ? (acceptedVersion ?? post.version) : null;
|
|
1091
|
-
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1839
|
+
// A reason says why it left, and a stay was about the version once in use.
|
|
1840
|
+
const reason = status === "archived" ? (post.archiveReason ?? null) : null;
|
|
1841
|
+
// Archiving what's already archived keeps the day it went.
|
|
1842
|
+
const archivedAt = status === "archived"
|
|
1843
|
+
? ((post.status === "archived" ? post.archivedAt : undefined) ?? new Date().toISOString())
|
|
1844
|
+
: null;
|
|
1845
|
+
this.raw.exec("UPDATE posts SET status = ?, acceptedVersion = ?, archiveReason = ?, stayedVersion = NULL, archivedAt = ? WHERE id = ?", status, pin, reason, archivedAt, id);
|
|
1846
|
+
const { acceptedVersion: _, archiveReason: _reason, stayedVersion: _stayed, archivedAt: _archivedAt, ...rest } = post;
|
|
1847
|
+
const next = {
|
|
1848
|
+
...rest,
|
|
1849
|
+
status,
|
|
1850
|
+
...(pin === null ? {} : { acceptedVersion: pin }),
|
|
1851
|
+
...(reason === null ? {} : { archiveReason: reason }),
|
|
1852
|
+
...(archivedAt === null ? {} : { archivedAt }),
|
|
1853
|
+
};
|
|
1854
|
+
this.recache(next);
|
|
1855
|
+
return pin !== null && next.hiddenVersions?.includes(pin)
|
|
1856
|
+
? this.setVersionHidden(id, pin, false)
|
|
1857
|
+
: next;
|
|
1094
1858
|
}
|
|
1095
|
-
async
|
|
1096
|
-
const post = await this.getPost(id);
|
|
1859
|
+
async setPostArchived(id, archivedAt, reason) {
|
|
1860
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1097
1861
|
if (!post)
|
|
1098
1862
|
return null;
|
|
1099
|
-
this.
|
|
1100
|
-
const {
|
|
1101
|
-
|
|
1863
|
+
this.raw.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL, archiveReason = ?, stayedVersion = NULL, archivedAt = ? WHERE id = ?", reason, archivedAt, id);
|
|
1864
|
+
const { acceptedVersion: _, archiveReason: _reason, stayedVersion: _stayed, ...rest } = post;
|
|
1865
|
+
const next = {
|
|
1866
|
+
...rest,
|
|
1867
|
+
status: "archived",
|
|
1868
|
+
archivedAt,
|
|
1869
|
+
...(reason === null ? {} : { archiveReason: reason }),
|
|
1870
|
+
};
|
|
1871
|
+
this.recache(next);
|
|
1872
|
+
return next;
|
|
1873
|
+
}
|
|
1874
|
+
async setForkedFrom(id, from) {
|
|
1875
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1876
|
+
if (!post)
|
|
1877
|
+
return null;
|
|
1878
|
+
this.raw.exec("UPDATE posts SET forkedFrom = ? WHERE id = ?", from ? JSON.stringify(from) : null, id);
|
|
1879
|
+
const { forkedFrom: _, ...rest } = post;
|
|
1880
|
+
const next = from ? { ...rest, forkedFrom: from } : rest;
|
|
1881
|
+
this.recache(next);
|
|
1882
|
+
return next;
|
|
1883
|
+
}
|
|
1884
|
+
async bumpRewrites(designId) {
|
|
1885
|
+
const ids = (await this.listPosts({ designId })).map((p) => p.id);
|
|
1886
|
+
this.writingPosts(designId, ids, () => this.sql.exec("UPDATE posts SET rewrites = rewrites + 1 WHERE designId = ?", designId));
|
|
1102
1887
|
}
|
|
1103
1888
|
async setVersionHidden(id, version, hidden) {
|
|
1104
|
-
const post = await this.getPost(id);
|
|
1889
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1105
1890
|
if (!post)
|
|
1106
1891
|
return null;
|
|
1107
1892
|
const set = new Set(post.hiddenVersions ?? []);
|
|
@@ -1110,21 +1895,60 @@ export class SqlStore {
|
|
|
1110
1895
|
else
|
|
1111
1896
|
set.delete(version);
|
|
1112
1897
|
const list = [...set].sort((a, b) => a - b);
|
|
1113
|
-
this.
|
|
1898
|
+
this.raw.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", list.length ? JSON.stringify(list) : null, id);
|
|
1114
1899
|
const { hiddenVersions: _, ...rest } = post;
|
|
1115
|
-
|
|
1900
|
+
const next = list.length ? { ...rest, hiddenVersions: list } : rest;
|
|
1901
|
+
this.recache(next);
|
|
1902
|
+
return next;
|
|
1903
|
+
}
|
|
1904
|
+
async deleteOldVersions(id, versions) {
|
|
1905
|
+
const post = await this.getPost(id);
|
|
1906
|
+
if (!post)
|
|
1907
|
+
return null;
|
|
1908
|
+
const gone = new Set(versions.filter((v) => v !== post.version));
|
|
1909
|
+
const hidden = (post.hiddenVersions ?? []).filter((v) => !gone.has(v));
|
|
1910
|
+
this.writingPosts(post.design, [id], () => this.atomic(() => {
|
|
1911
|
+
for (const v of gone) {
|
|
1912
|
+
this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version = ?", id, v);
|
|
1913
|
+
}
|
|
1914
|
+
this.sql.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", hidden.length ? JSON.stringify(hidden) : null, id);
|
|
1915
|
+
}));
|
|
1916
|
+
this.touchDesign(post.design, new Date().toISOString());
|
|
1917
|
+
this.invalidateAssetRefs();
|
|
1918
|
+
return this.getPost(id);
|
|
1919
|
+
}
|
|
1920
|
+
async setPostReason(id, reason) {
|
|
1921
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1922
|
+
if (!post)
|
|
1923
|
+
return null;
|
|
1924
|
+
this.raw.exec("UPDATE posts SET archiveReason = ? WHERE id = ?", reason, id);
|
|
1925
|
+
const { archiveReason: _, ...rest } = post;
|
|
1926
|
+
const next = reason ? { ...rest, archiveReason: reason } : rest;
|
|
1927
|
+
this.recache(next);
|
|
1928
|
+
return next;
|
|
1929
|
+
}
|
|
1930
|
+
async setPostStayed(id, version) {
|
|
1931
|
+
const post = this.cachedPost(id) ?? (await this.getPost(id));
|
|
1932
|
+
if (!post)
|
|
1933
|
+
return null;
|
|
1934
|
+
this.raw.exec("UPDATE posts SET stayedVersion = ? WHERE id = ?", version, id);
|
|
1935
|
+
const next = { ...post, stayedVersion: version };
|
|
1936
|
+
this.recache(next);
|
|
1937
|
+
return next;
|
|
1116
1938
|
}
|
|
1117
1939
|
// --- comments ---
|
|
1118
1940
|
async listComments(query) {
|
|
1119
1941
|
const clauses = [];
|
|
1120
1942
|
const params = [];
|
|
1943
|
+
if (query.noDesign)
|
|
1944
|
+
clauses.push("designId IS NULL");
|
|
1121
1945
|
if (query.sessionId !== undefined) {
|
|
1122
1946
|
clauses.push("sessionId = ?");
|
|
1123
1947
|
params.push(query.sessionId);
|
|
1124
1948
|
}
|
|
1125
|
-
if (query.
|
|
1126
|
-
clauses.push("
|
|
1127
|
-
params.push(query.
|
|
1949
|
+
if (query.designId !== undefined) {
|
|
1950
|
+
clauses.push("designId = ?");
|
|
1951
|
+
params.push(query.designId);
|
|
1128
1952
|
}
|
|
1129
1953
|
if (query.postId !== undefined) {
|
|
1130
1954
|
clauses.push("postId = ?");
|
|
@@ -1145,7 +1969,7 @@ export class SqlStore {
|
|
|
1145
1969
|
const comment = {
|
|
1146
1970
|
id: newId(),
|
|
1147
1971
|
sessionId: input.sessionId,
|
|
1148
|
-
|
|
1972
|
+
designId: input.designId ?? null,
|
|
1149
1973
|
postId: input.postId ?? null,
|
|
1150
1974
|
author: stripNul(input.author).trim() || "user",
|
|
1151
1975
|
text: stripNul(input.text),
|
|
@@ -1157,7 +1981,7 @@ export class SqlStore {
|
|
|
1157
1981
|
viewport: input.viewport ?? null,
|
|
1158
1982
|
...(input.payload ? { payload: input.payload } : {}),
|
|
1159
1983
|
};
|
|
1160
|
-
this.sql.exec("INSERT INTO comments (id, sessionId,
|
|
1984
|
+
this.sql.exec("INSERT INTO comments (id, sessionId, designId, postId, author, text, createdAt, kind, anchor, anchors, postVersion, viewport, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", comment.id, comment.sessionId, comment.designId, comment.postId, comment.author, comment.text, comment.createdAt, comment.kind, comment.anchor ? JSON.stringify(comment.anchor) : null, JSON.stringify(comment.anchors), comment.postVersion, comment.viewport, comment.payload ? JSON.stringify(comment.payload) : null);
|
|
1161
1985
|
const seq = this.sql.exec("SELECT last_insert_rowid() AS seq").one().seq;
|
|
1162
1986
|
this.touch(input.sessionId);
|
|
1163
1987
|
return { ...comment, seq };
|
|
@@ -1169,7 +1993,7 @@ export class SqlStore {
|
|
|
1169
1993
|
return this.insertComment({
|
|
1170
1994
|
...input,
|
|
1171
1995
|
postId: post?.id ?? null,
|
|
1172
|
-
|
|
1996
|
+
designId: input.designId ?? post?.design ?? null,
|
|
1173
1997
|
postVersion: input.postVersion ?? post?.version ?? null,
|
|
1174
1998
|
});
|
|
1175
1999
|
}
|
|
@@ -1184,36 +2008,85 @@ export class SqlStore {
|
|
|
1184
2008
|
this.touch(comment.sessionId);
|
|
1185
2009
|
return comment;
|
|
1186
2010
|
}
|
|
2011
|
+
async listDecisionSources(project) {
|
|
2012
|
+
const variants = this.sql
|
|
2013
|
+
.exec("SELECT designId, state, variant, status, version, acceptedVersion FROM posts WHERE designId IN (SELECT id FROM designs WHERE project = ?) ORDER BY createdAt ASC, rowid ASC", project)
|
|
2014
|
+
.toArray()
|
|
2015
|
+
.map((r) => ({
|
|
2016
|
+
designId: r.designId,
|
|
2017
|
+
state: r.state ?? null,
|
|
2018
|
+
variant: r.variant,
|
|
2019
|
+
status: r.status,
|
|
2020
|
+
version: r.version,
|
|
2021
|
+
acceptedVersion: r.acceptedVersion ?? null,
|
|
2022
|
+
}));
|
|
2023
|
+
// A close or a move from Home is a reply too, and decides nothing.
|
|
2024
|
+
const decidedAt = {};
|
|
2025
|
+
for (const r of this.sql
|
|
2026
|
+
.exec(`SELECT designId, MAX(createdAt) AS at FROM comments
|
|
2027
|
+
WHERE kind = 'reply' AND designId IN (SELECT id FROM designs WHERE project = ?)
|
|
2028
|
+
AND json_extract(payload, '$.archived') IS NULL
|
|
2029
|
+
AND json_extract(payload, '$.edited') IS NULL
|
|
2030
|
+
GROUP BY designId`, project)
|
|
2031
|
+
.toArray()) {
|
|
2032
|
+
decidedAt[r.designId] = r.at;
|
|
2033
|
+
}
|
|
2034
|
+
return { variants, decidedAt };
|
|
2035
|
+
}
|
|
1187
2036
|
async commitReply(input) {
|
|
1188
2037
|
if (!(await this.getSession(input.sessionId)))
|
|
1189
2038
|
return null;
|
|
1190
|
-
if (!(await this.
|
|
2039
|
+
if (!(await this.getDesign(input.designId)))
|
|
1191
2040
|
return null;
|
|
1192
|
-
|
|
2041
|
+
const moved = [...input.accept.map((x) => x.id), ...input.archive, ...(input.release ?? [])];
|
|
2042
|
+
return this.writingPosts(input.designId, moved, () => this.atomic(() => {
|
|
1193
2043
|
const comment = this.insertComment({
|
|
1194
2044
|
sessionId: input.sessionId,
|
|
1195
|
-
|
|
2045
|
+
designId: input.designId,
|
|
1196
2046
|
author: "user",
|
|
1197
2047
|
text: input.text,
|
|
1198
2048
|
kind: "reply",
|
|
1199
2049
|
postVersion: input.payload.version,
|
|
1200
2050
|
payload: input.payload,
|
|
1201
2051
|
});
|
|
2052
|
+
if (input.system) {
|
|
2053
|
+
this.writeSystem(input.system.next, {
|
|
2054
|
+
sessionId: input.sessionId,
|
|
2055
|
+
summary: input.system.summary,
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1202
2058
|
this.sql.exec(input.keepDraft
|
|
1203
|
-
? "UPDATE
|
|
1204
|
-
: "UPDATE
|
|
1205
|
-
if (input.
|
|
1206
|
-
this.sql.exec("UPDATE
|
|
2059
|
+
? "UPDATE designs SET asks = ? WHERE id = ?"
|
|
2060
|
+
: "UPDATE designs SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.designId);
|
|
2061
|
+
if (input.standings) {
|
|
2062
|
+
this.sql.exec("UPDATE designs SET standings = ? WHERE id = ?", JSON.stringify(input.standings), input.designId);
|
|
2063
|
+
}
|
|
2064
|
+
if (input.owner) {
|
|
2065
|
+
this.sql.exec("UPDATE designs SET sessionId = ? WHERE id = ?", input.owner, input.designId);
|
|
1207
2066
|
}
|
|
1208
2067
|
for (const { id, version } of input.accept) {
|
|
1209
|
-
|
|
2068
|
+
// Picking an older version is passing on the newer ones. Otherwise they'd read
|
|
2069
|
+
// as unruled-on, and the viewer would move straight back to them.
|
|
2070
|
+
this.sql.exec("UPDATE posts SET status = 'accepted', acceptedVersion = ?, archiveReason = NULL, stayedVersion = CASE WHEN ? < version THEN version ELSE NULL END, archivedAt = NULL WHERE id = ? AND designId = ?", version, version, id, input.designId);
|
|
2071
|
+
// An answer's whole page stays hidden until it is chosen; now it's a version.
|
|
2072
|
+
const hiddenRow = this.sql
|
|
2073
|
+
.exec("SELECT hiddenVersions FROM posts WHERE id = ?", id)
|
|
2074
|
+
.toArray()[0];
|
|
2075
|
+
const hidden = parseJson(hiddenRow?.hiddenVersions, []);
|
|
2076
|
+
if (hidden.includes(version)) {
|
|
2077
|
+
const left = hidden.filter((v) => v !== version);
|
|
2078
|
+
this.sql.exec("UPDATE posts SET hiddenVersions = ? WHERE id = ?", left.length ? JSON.stringify(left) : null, id);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
for (const id of input.release ?? []) {
|
|
2082
|
+
this.sql.exec("UPDATE posts SET status = 'open', acceptedVersion = NULL, stayedVersion = NULL WHERE id = ? AND designId = ? AND status = 'accepted'", id, input.designId);
|
|
1210
2083
|
}
|
|
1211
2084
|
for (const id of input.archive) {
|
|
1212
|
-
this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL WHERE id = ? AND
|
|
2085
|
+
this.sql.exec("UPDATE posts SET status = 'archived', acceptedVersion = NULL, archivedAt = ? WHERE id = ? AND designId = ?", new Date().toISOString(), id, input.designId);
|
|
1213
2086
|
}
|
|
1214
2087
|
this.invalidateAssetRefs();
|
|
1215
2088
|
return comment;
|
|
1216
|
-
});
|
|
2089
|
+
}));
|
|
1217
2090
|
}
|
|
1218
2091
|
// --- assets ---
|
|
1219
2092
|
referencedAssetIds() {
|
|
@@ -1227,7 +2100,7 @@ export class SqlStore {
|
|
|
1227
2100
|
collectAssetIds(parseJson(r.surfaces, []), out);
|
|
1228
2101
|
}
|
|
1229
2102
|
// The user's attachments: in a draft, in a sent reply, on an answered ask.
|
|
1230
|
-
for (const r of this.sql.exec("SELECT asks, draft FROM
|
|
2103
|
+
for (const r of this.sql.exec("SELECT asks, draft FROM designs").toArray()) {
|
|
1231
2104
|
collectAttachedIds(parseJson(r.draft, null), out);
|
|
1232
2105
|
for (const ask of parseJson(r.asks, []))
|
|
1233
2106
|
collectAttachedIds(ask, out);
|
|
@@ -1237,12 +2110,10 @@ export class SqlStore {
|
|
|
1237
2110
|
.toArray()) {
|
|
1238
2111
|
collectAttachedIds(parseJson(r.payload, null), out);
|
|
1239
2112
|
}
|
|
1240
|
-
// An installed icon set is referenced by its project's
|
|
2113
|
+
// An installed icon set is referenced by its project's system, not by a
|
|
1241
2114
|
// surface; evicting it would blank that project's icons.
|
|
1242
|
-
for (const r of this.sql
|
|
1243
|
-
|
|
1244
|
-
.toArray()) {
|
|
1245
|
-
const sets = parseJson(r.value, null)?.iconSets;
|
|
2115
|
+
for (const r of this.sql.exec("SELECT json FROM systems").toArray()) {
|
|
2116
|
+
const sets = parseJson(r.json, null)?.iconSets;
|
|
1246
2117
|
for (const ref of Array.isArray(sets) ? sets : []) {
|
|
1247
2118
|
if (typeof ref?.assetId === "string")
|
|
1248
2119
|
out.add(ref.assetId);
|