wizards 0.2.17 → 0.2.18

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.
Files changed (65) hide show
  1. package/apps/runtime/dist/agents/project-assistant.js +20 -2
  2. package/apps/runtime/dist/authoring/guide.js +6 -3
  3. package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
  4. package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
  5. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  6. package/apps/runtime/dist/db/schema.js +160 -8
  7. package/apps/runtime/dist/engine/steps.js +3 -11
  8. package/apps/runtime/dist/env.js +2 -0
  9. package/apps/runtime/dist/files/storage.js +15 -0
  10. package/apps/runtime/dist/models.js +38 -0
  11. package/apps/runtime/dist/plugins/loader.js +2 -0
  12. package/apps/runtime/dist/routes/studio.js +147 -6
  13. package/apps/runtime/dist/services/cloud.js +59 -1
  14. package/apps/runtime/dist/services/knowledge-model.js +558 -0
  15. package/apps/runtime/dist/services/knowledge.js +721 -0
  16. package/apps/runtime/dist/services/project-files.js +114 -6
  17. package/apps/runtime/dist/services/project-index.js +632 -94
  18. package/apps/runtime/dist/services/relevance.js +90 -0
  19. package/apps/runtime/dist/services/space-categories.js +882 -0
  20. package/apps/runtime/dist/services/space-data.js +466 -38
  21. package/apps/runtime/dist/services/space-origin.js +164 -0
  22. package/apps/runtime/dist/services/spaces.js +155 -1
  23. package/apps/runtime/dist/tools/pages.js +27 -22
  24. package/apps/runtime/dist/tools/project.js +130 -40
  25. package/apps/web/dist/assets/{Account-LVXhagaS.js → Account-Cp1cgqjV.js} +1 -1
  26. package/apps/web/dist/assets/{ChangelogDialog-DM0KAoy6.js → ChangelogDialog-Chke1qUe.js} +1 -1
  27. package/apps/web/dist/assets/{ChatPanel-CxOQmtv-.js → ChatPanel-DWrwewr6.js} +1 -1
  28. package/apps/web/dist/assets/{CreditsDialog-DkW3YT44.js → CreditsDialog-Ckema8Ss.js} +1 -1
  29. package/apps/web/dist/assets/{EditorPage-hbisE1Wf.js → EditorPage-XX6pfHk5.js} +1 -1
  30. package/apps/web/dist/assets/{FlowPreview-v7or29ku.js → FlowPreview-DS545n78.js} +1 -1
  31. package/apps/web/dist/assets/{Harness-RZF-bBmb.js → Harness-H3yXP-2L.js} +1 -1
  32. package/apps/web/dist/assets/{HomePage-mxG-a_KU.js → HomePage-Bds84ShK.js} +1 -1
  33. package/apps/web/dist/assets/{ImportWizard-CiPyUt3E.js → ImportWizard-0_NkA8aS.js} +1 -1
  34. package/apps/web/dist/assets/{LocalRuntime-Dqp9wkN3.js → LocalRuntime-DnABzTOe.js} +1 -1
  35. package/apps/web/dist/assets/{NewWizardPage-DHwRsi0_.js → NewWizardPage-BqP75V9K.js} +2 -2
  36. package/apps/web/dist/assets/ProjectPage-DbOvwOdZ.js +5 -0
  37. package/apps/web/dist/assets/{PublicRunner-TGkOwbAP.js → PublicRunner-CtZ7vxl_.js} +1 -1
  38. package/apps/web/dist/assets/{RunnerView-DzHGshZj.js → RunnerView-xV5bWclu.js} +1 -1
  39. package/apps/web/dist/assets/{SettingsPage-CG3Xtfej.js → SettingsPage-DKt55zFh.js} +1 -1
  40. package/apps/web/dist/assets/{SetupPage-O1GfPlqX.js → SetupPage-c6mMD9zN.js} +1 -1
  41. package/apps/web/dist/assets/{SharePage-CCNUhtry.js → SharePage-DTKIbMj-.js} +1 -1
  42. package/apps/web/dist/assets/{api-DB22_B7G.js → api-CVG-xDEn.js} +1 -1
  43. package/apps/web/dist/assets/changelog-CXXT0zsp.js +1 -0
  44. package/apps/web/dist/assets/{esm-3TT9i_rv.js → esm-DLM3jdbm.js} +1 -1
  45. package/apps/web/dist/assets/{index-CLe9Wrpu.js → index-1gmaxDSk.js} +3 -3
  46. package/apps/web/dist/assets/index-Bwyehu0k.css +1 -0
  47. package/apps/web/dist/assets/{meta-CEsBCPfA.js → meta-m947x6uG.js} +1 -1
  48. package/apps/web/dist/assets/{settings-menu-pOcjznGJ.js → settings-menu-DUY6__C3.js} +1 -1
  49. package/apps/web/dist/assets/ui-BPY2pIh0.js +1 -0
  50. package/apps/web/dist/index.html +4 -4
  51. package/apps/web/dist/mcp/flow.html +2 -2
  52. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +164 -0
  53. package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
  54. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
  55. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +1 -16
  56. package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
  57. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +8 -0
  58. package/node_modules/@engenty-wizards/shared/dist/run.js +5 -0
  59. package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +20 -0
  60. package/node_modules/@engenty-wizards/shared/dist/space-data.js +2 -1
  61. package/package.json +1 -1
  62. package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +0 -4
  63. package/apps/web/dist/assets/changelog-BkIJdZ7M.js +0 -1
  64. package/apps/web/dist/assets/index-BPpKz65x.css +0 -1
  65. package/apps/web/dist/assets/ui-I7QYMX0F.js +0 -1
@@ -1,13 +1,16 @@
1
1
  import { TableColumnValueError, tableColumnsSchema, } from "@engenty-wizards/shared/engenty/data-tables";
2
2
  import { editCells, SPACE_DATA_LIMITS, } from "@engenty-wizards/shared/space-data";
3
3
  import { rowKey } from "@engenty-wizards/shared/store";
4
- import { and, asc, count, desc, eq, inArray } from "drizzle-orm";
4
+ import { and, asc, count, desc, eq, inArray, isNotNull, isNull, max } from "drizzle-orm";
5
5
  import { nanoid } from "nanoid";
6
6
  import { db, schema } from "../db/client.js";
7
7
  import { currentTenant } from "../tenants/tenant.js";
8
8
  import { projectWritable, requireWritableProject } from "./access.js";
9
9
  import { notFound, ServiceError } from "./errors.js";
10
+ import { filePaths, freeSlug, knowledgePage, pageTree, tablePaths } from "./knowledge.js";
11
+ import { queueIndex } from "./project-index.js";
10
12
  import { ownedProject } from "./projects.js";
13
+ import { itemCategoriesOf, setItemCategories, syncTableRowCategories } from "./space-categories.js";
11
14
  /** A wizard of the space, or null for the space's own. */
12
15
  async function ownerOf(projectId, wizardId) {
13
16
  if (!wizardId) {
@@ -28,18 +31,19 @@ function cellError(err) {
28
31
  }
29
32
  throw err;
30
33
  }
31
- function checkColumns(columns) {
34
+ export function checkColumns(columns) {
32
35
  const parsed = tableColumnsSchema.safeParse(columns);
33
36
  if (!parsed.success) {
34
37
  throw new ServiceError("invalid", parsed.error.issues[0]?.message ?? "Ungültige Spalten.");
35
38
  }
36
39
  return parsed.data;
37
40
  }
41
+ /** Daten: the tables and pages the space's wizards keep, and the wizards to name whose they are. */
38
42
  export async function listSpaceData(userId, projectId) {
39
43
  const project = await ownedProject(userId, projectId);
40
44
  const [tables, rows, pages, wizards] = await Promise.all([
41
45
  db.query.spaceTable.findMany({
42
- where: eq(schema.spaceTable.projectId, project.id),
46
+ where: and(eq(schema.spaceTable.projectId, project.id), isNotNull(schema.spaceTable.wizardId)),
43
47
  orderBy: [asc(schema.spaceTable.title)],
44
48
  columns: { id: true, wizardId: true, list: true, title: true, updatedAt: true },
45
49
  }),
@@ -50,7 +54,7 @@ export async function listSpaceData(userId, projectId) {
50
54
  .where(eq(schema.spaceTable.projectId, project.id))
51
55
  .groupBy(schema.spaceTableRow.tableId),
52
56
  db.query.spacePage.findMany({
53
- where: eq(schema.spacePage.projectId, project.id),
57
+ where: and(eq(schema.spacePage.projectId, project.id), isNotNull(schema.spacePage.wizardId)),
54
58
  orderBy: [asc(schema.spacePage.title)],
55
59
  columns: { id: true, wizardId: true, title: true, updatedAt: true },
56
60
  }),
@@ -85,36 +89,74 @@ async function writableTable(tableId) {
85
89
  await requireWritableProject(table.projectId);
86
90
  return table;
87
91
  }
88
- async function touchTable(tableId) {
92
+ /** A person changed a table: when a plugin wrote it, the plugin no longer does. */
93
+ async function touchTable(table) {
89
94
  await db
90
95
  .update(schema.spaceTable)
91
- .set({ updatedAt: new Date() })
92
- .where(eq(schema.spaceTable.id, tableId));
96
+ .set({ updatedAt: new Date(), ...(table.origin && !table.kept ? { kept: true } : {}) })
97
+ .where(eq(schema.spaceTable.id, table.id));
93
98
  }
94
- export async function createTable(userId, projectId, input) {
95
- const project = await ownedProject(userId, projectId);
96
- await requireWritableProject(project.id);
99
+ async function tableCount(projectId) {
97
100
  const [{ n }] = await db
98
101
  .select({ n: count() })
99
102
  .from(schema.spaceTable)
100
- .where(eq(schema.spaceTable.projectId, project.id));
103
+ .where(eq(schema.spaceTable.projectId, projectId));
101
104
  if (n >= SPACE_DATA_LIMITS.tables) {
102
105
  throw new ServiceError("refused", `Ein Space hat höchstens ${SPACE_DATA_LIMITS.tables} Tabellen.`);
103
106
  }
107
+ }
108
+ export async function createTable(userId, projectId, input) {
109
+ const project = await ownedProject(userId, projectId);
110
+ await requireWritableProject(project.id);
111
+ await tableCount(project.id);
104
112
  const columns = input.columns
105
113
  ? checkColumns(input.columns)
106
- : [{ id: "name", name: "Name", type: "text" }];
114
+ : input.format === "faq"
115
+ ? [
116
+ { id: "frage", name: "Frage", type: "text" },
117
+ {
118
+ id: "antwort",
119
+ name: "Antwort",
120
+ type: "text",
121
+ format: { style: "multiline" },
122
+ },
123
+ ]
124
+ : [{ id: "name", name: "Name", type: "text" }];
107
125
  const id = nanoid(12);
126
+ const wizardId = await ownerOf(project.id, input.wizardId);
127
+ const title = input.title.trim() || "Tabelle";
108
128
  await db.insert(schema.spaceTable).values({
109
129
  id,
110
130
  tenantId: currentTenant(),
111
131
  projectId: project.id,
112
- wizardId: await ownerOf(project.id, input.wizardId),
113
- title: input.title.trim() || "Tabelle",
132
+ wizardId,
133
+ title,
134
+ slug: wizardId ? null : await freeSlug(project.id, title, "table"),
135
+ format: input.format ?? null,
114
136
  columns,
115
137
  });
116
138
  return { id };
117
139
  }
140
+ async function tableKnowledge(table) {
141
+ if (table.wizardId) {
142
+ return null;
143
+ }
144
+ const file = table.fileId
145
+ ? await db.query.projectFile.findFirst({
146
+ where: eq(schema.projectFile.id, table.fileId),
147
+ columns: { id: true, name: true, mime: true },
148
+ })
149
+ : undefined;
150
+ return {
151
+ path: (await tablePaths(table.projectId)).get(table.id) ?? "",
152
+ categories: (await itemCategoriesOf([{ tableId: table.id }])).get(`t:${table.id}`) ?? [],
153
+ origin: table.origin,
154
+ originLabel: table.originLabel,
155
+ kept: table.kept,
156
+ review: table.review,
157
+ file: file ?? null,
158
+ };
159
+ }
118
160
  export async function getTable(userId, tableId) {
119
161
  const table = await tableRow(tableId);
120
162
  const project = await ownedProject(userId, table.projectId);
@@ -128,35 +170,66 @@ export async function getTable(userId, tableId) {
128
170
  wizardId: table.wizardId,
129
171
  list: table.list,
130
172
  title: table.title,
173
+ format: table.format,
131
174
  columns: table.columns,
132
175
  rows: rows.map((r) => ({ id: r.id, cells: r.cells, updatedAt: r.updatedAt.toISOString() })),
133
176
  readOnly: !(await projectWritable(project)),
177
+ knowledge: await tableKnowledge(table),
134
178
  };
135
179
  }
136
180
  /**
137
181
  * A new title, owner or columns. Rows keep their cells: a removed column's cells are dropped
138
- * the next time the row changes, and are not shown before.
182
+ * the next time the row changes, and are not shown before. `review: null`: a person looked.
139
183
  */
140
184
  export async function updateTable(_userId, tableId, patch) {
141
185
  const table = await writableTable(tableId);
142
186
  if (table.list && (patch.columns !== undefined || patch.wizardId !== undefined)) {
143
187
  throw new ServiceError("refused", "Diese Tabelle gehört zu einer Liste des Wizards: ihre Spalten kommen aus dem Wizard.");
144
188
  }
189
+ const title = patch.title !== undefined ? patch.title.trim() || table.title : table.title;
190
+ const wizardId = patch.wizardId !== undefined ? await ownerOf(table.projectId, patch.wizardId) : table.wizardId;
191
+ const changed = title !== table.title || patch.columns !== undefined || patch.format !== undefined;
145
192
  await db
146
193
  .update(schema.spaceTable)
147
194
  .set({
148
- ...(patch.title !== undefined ? { title: patch.title.trim() || table.title } : {}),
149
- ...(patch.wizardId !== undefined
150
- ? { wizardId: await ownerOf(table.projectId, patch.wizardId) }
195
+ title,
196
+ wizardId,
197
+ ...(title !== table.title || wizardId !== table.wizardId
198
+ ? { slug: wizardId ? null : await freeSlug(table.projectId, title, "table", table.id) }
151
199
  : {}),
152
200
  ...(patch.columns !== undefined ? { columns: checkColumns(patch.columns) } : {}),
201
+ ...(patch.format !== undefined ? { format: patch.format } : {}),
202
+ ...(patch.review === null ? { review: null } : {}),
203
+ ...(changed && table.origin ? { kept: true } : {}),
153
204
  updatedAt: new Date(),
154
205
  })
155
206
  .where(eq(schema.spaceTable.id, table.id));
207
+ if (changed || wizardId !== table.wizardId) {
208
+ queueIndex(`t:${table.id}`);
209
+ }
210
+ }
211
+ /** The file an item was read from goes with it, when it was uploaded and nothing else uses it. */
212
+ async function dropOriginal(item) {
213
+ if (!item.fileId || item.originKey) {
214
+ return;
215
+ }
216
+ const used = (await db.query.spacePage.findFirst({
217
+ where: eq(schema.spacePage.fileId, item.fileId),
218
+ columns: { id: true },
219
+ })) ??
220
+ (await db.query.spaceTable.findFirst({
221
+ where: eq(schema.spaceTable.fileId, item.fileId),
222
+ columns: { id: true },
223
+ }));
224
+ if (!used) {
225
+ const { removeProjectFile } = await import("./project-files.js");
226
+ await removeProjectFile(item.projectId, item.fileId).catch(() => undefined);
227
+ }
156
228
  }
157
229
  export async function deleteTable(_userId, tableId) {
158
230
  const table = await writableTable(tableId);
159
231
  await db.delete(schema.spaceTable).where(eq(schema.spaceTable.id, table.id));
232
+ await dropOriginal(table);
160
233
  }
161
234
  /** The key a row of a shared list is matched on; tables made in the studio have none. */
162
235
  function keyOf(table, cells) {
@@ -177,6 +250,14 @@ async function refuseClash(tableId, key, rowId) {
177
250
  throw new ServiceError("invalid", "Diesen Eintrag gibt es schon.");
178
251
  }
179
252
  }
253
+ /** After a row of a table of Wissen changed: its Kategorien from its columns, its unit. */
254
+ async function rowChanged(table, rowId) {
255
+ if (table.wizardId) {
256
+ return;
257
+ }
258
+ await syncTableRowCategories(table.id, [rowId]);
259
+ queueIndex(`r:${rowId}`);
260
+ }
180
261
  /** Adds a row; `cells` may leave columns empty, even required ones, to be filled later. */
181
262
  export async function addTableRow(_userId, tableId, cells = {}) {
182
263
  const table = await writableTable(tableId);
@@ -198,7 +279,8 @@ export async function addTableRow(_userId, tableId, cells = {}) {
198
279
  await refuseClash(table.id, key, null);
199
280
  const row = { id: nanoid(14), tableId: table.id, key, cells: value, createdAt: new Date() };
200
281
  await db.insert(schema.spaceTableRow).values(row);
201
- await touchTable(table.id);
282
+ await touchTable(table);
283
+ await rowChanged(table, row.id);
202
284
  return { id: row.id, cells: value };
203
285
  }
204
286
  /** Changes the cells named in `cells`; the others stay. */
@@ -223,7 +305,8 @@ export async function updateTableRow(_userId, tableId, rowId, cells) {
223
305
  .update(schema.spaceTableRow)
224
306
  .set({ cells: value, key, updatedAt: new Date() })
225
307
  .where(eq(schema.spaceTableRow.id, row.id));
226
- await touchTable(table.id);
308
+ await touchTable(table);
309
+ await rowChanged(table, row.id);
227
310
  return { id: row.id, cells: value };
228
311
  }
229
312
  export async function deleteTableRows(_userId, tableId, rowIds) {
@@ -234,7 +317,7 @@ export async function deleteTableRows(_userId, tableId, rowIds) {
234
317
  await db
235
318
  .delete(schema.spaceTableRow)
236
319
  .where(and(eq(schema.spaceTableRow.tableId, table.id), inArray(schema.spaceTableRow.id, rowIds)));
237
- await touchTable(table.id);
320
+ await touchTable(table);
238
321
  }
239
322
  // --- pages ---------------------------------------------------------------------
240
323
  async function pageRow(pageId) {
@@ -252,25 +335,68 @@ function checkMarkdown(markdown) {
252
335
  }
253
336
  return markdown;
254
337
  }
255
- export async function createPage(userId, projectId, input) {
256
- const project = await ownedProject(userId, projectId);
257
- await requireWritableProject(project.id);
338
+ async function pageCount(projectId, more = 1) {
258
339
  const [{ n }] = await db
259
340
  .select({ n: count() })
260
341
  .from(schema.spacePage)
261
- .where(eq(schema.spacePage.projectId, project.id));
262
- if (n >= SPACE_DATA_LIMITS.pages) {
342
+ .where(eq(schema.spacePage.projectId, projectId));
343
+ if (n + more > SPACE_DATA_LIMITS.pages) {
263
344
  throw new ServiceError("refused", `Ein Space hat höchstens ${SPACE_DATA_LIMITS.pages} Seiten.`);
264
345
  }
346
+ }
347
+ /** A page of the space's own that a sub-page may stand under: never one of its own sub-pages. */
348
+ async function parentOf(projectId, parentId, pageId) {
349
+ if (!parentId) {
350
+ return null;
351
+ }
352
+ const parent = await db.query.spacePage.findFirst({
353
+ where: and(eq(schema.spacePage.id, parentId), eq(schema.spacePage.projectId, projectId)),
354
+ columns: { id: true, wizardId: true, parentId: true },
355
+ });
356
+ if (!parent || parent.wizardId) {
357
+ throw new ServiceError("invalid", "Unter diese Seite kann keine Seite.");
358
+ }
359
+ for (let at = parent.id; at && pageId;) {
360
+ if (at === pageId) {
361
+ throw new ServiceError("invalid", "Eine Seite kann nicht unter sich selbst stehen.");
362
+ }
363
+ const up = await db.query.spacePage.findFirst({
364
+ where: eq(schema.spacePage.id, at),
365
+ columns: { parentId: true },
366
+ });
367
+ at = up?.parentId ?? null;
368
+ }
369
+ return parent.id;
370
+ }
371
+ async function nextPosition(projectId, parentId) {
372
+ const [{ last }] = await db
373
+ .select({ last: max(schema.spacePage.position) })
374
+ .from(schema.spacePage)
375
+ .where(and(eq(schema.spacePage.projectId, projectId), isNull(schema.spacePage.wizardId), parentId ? eq(schema.spacePage.parentId, parentId) : isNull(schema.spacePage.parentId)));
376
+ return (last ?? -1) + 1;
377
+ }
378
+ export async function createPage(userId, projectId, input) {
379
+ const project = await ownedProject(userId, projectId);
380
+ await requireWritableProject(project.id);
381
+ await pageCount(project.id);
265
382
  const id = nanoid(12);
383
+ const wizardId = await ownerOf(project.id, input.wizardId);
384
+ const parentId = wizardId ? null : await parentOf(project.id, input.parentId);
385
+ const title = input.title.trim() || "Seite";
266
386
  await db.insert(schema.spacePage).values({
267
387
  id,
268
388
  tenantId: currentTenant(),
269
389
  projectId: project.id,
270
- wizardId: await ownerOf(project.id, input.wizardId),
271
- title: input.title.trim() || "Seite",
390
+ wizardId,
391
+ parentId,
392
+ position: wizardId ? 0 : await nextPosition(project.id, parentId),
393
+ title,
394
+ slug: wizardId ? null : await freeSlug(project.id, title, { parentId }),
272
395
  markdown: checkMarkdown(input.markdown ?? ""),
273
396
  });
397
+ if (!wizardId) {
398
+ queueIndex(`p:${id}`);
399
+ }
274
400
  return { id };
275
401
  }
276
402
  export async function getPage(userId, pageId) {
@@ -284,42 +410,80 @@ export async function getPage(userId, pageId) {
284
410
  markdown: page.markdown,
285
411
  updatedAt: page.updatedAt.toISOString(),
286
412
  readOnly: !(await projectWritable(project)),
413
+ knowledge: page.wizardId ? null : await knowledgePage(page),
287
414
  };
288
415
  }
416
+ /**
417
+ * A new title, text, owner or place. A person who changes what a plugin wrote keeps the page:
418
+ * the plugin no longer writes it. `review: null`: a person looked at it.
419
+ */
289
420
  export async function updatePage(_userId, pageId, patch) {
290
421
  const page = await pageRow(pageId);
291
422
  await requireWritableProject(page.projectId);
423
+ const title = patch.title !== undefined ? patch.title.trim() || page.title : page.title;
424
+ const wizardId = patch.wizardId !== undefined ? await ownerOf(page.projectId, patch.wizardId) : page.wizardId;
425
+ const parentId = wizardId !== null
426
+ ? null
427
+ : patch.parentId !== undefined
428
+ ? await parentOf(page.projectId, patch.parentId, page.id)
429
+ : page.parentId;
430
+ const moved = title !== page.title || wizardId !== page.wizardId || parentId !== page.parentId;
431
+ const edited = moved || (patch.markdown !== undefined && patch.markdown !== page.markdown);
292
432
  await db
293
433
  .update(schema.spacePage)
294
434
  .set({
295
- ...(patch.title !== undefined ? { title: patch.title.trim() || page.title } : {}),
296
- ...(patch.wizardId !== undefined
297
- ? { wizardId: await ownerOf(page.projectId, patch.wizardId) }
435
+ title,
436
+ wizardId,
437
+ parentId,
438
+ ...(moved
439
+ ? {
440
+ slug: wizardId ? null : await freeSlug(page.projectId, title, { parentId }, page.id),
441
+ ...(parentId !== page.parentId
442
+ ? { position: await nextPosition(page.projectId, parentId) }
443
+ : {}),
444
+ }
298
445
  : {}),
299
446
  ...(patch.markdown !== undefined ? { markdown: checkMarkdown(patch.markdown) } : {}),
447
+ ...(patch.review === null ? { review: null } : {}),
448
+ ...(edited && page.origin ? { kept: true } : {}),
300
449
  updatedAt: new Date(),
301
450
  })
302
451
  .where(eq(schema.spacePage.id, page.id));
452
+ if (moved) {
453
+ queueIndex(`P:${page.id}`);
454
+ }
455
+ else if (edited) {
456
+ queueIndex(`p:${page.id}`);
457
+ }
303
458
  }
304
459
  export async function deletePage(_userId, pageId) {
305
460
  const page = await pageRow(pageId);
306
461
  await requireWritableProject(page.projectId);
307
462
  await db.delete(schema.spacePage).where(eq(schema.spacePage.id, page.id));
463
+ await dropOriginal(page);
464
+ }
465
+ /** Puts a page's sub-pages into the order of `ids`. */
466
+ export async function orderSubPages(_userId, pageId, ids) {
467
+ const page = await pageRow(pageId);
468
+ await requireWritableProject(page.projectId);
469
+ for (const [position, id] of ids.entries()) {
470
+ await db
471
+ .update(schema.spacePage)
472
+ .set({ position })
473
+ .where(and(eq(schema.spacePage.id, id), eq(schema.spacePage.parentId, page.id)));
474
+ }
308
475
  }
309
476
  // --- what runs read and write --------------------------------------------------
310
477
  // A run writes the pages of its own wizard, on the runtime it runs on: a run on the server writes
311
478
  // the server's, also in a space a local install synced, which the studio there only shows. It
312
- // reads its wizard's pages and the space's own.
313
- /** The pages a run can read: its wizard's, then the space's own. */
479
+ // reads its wizard's pages; the space's own are Wissen, read by path.
480
+ /** The pages of a run's wizard. */
314
481
  export async function runPages(projectId, wizardId) {
315
- const pages = await db.query.spacePage.findMany({
316
- where: eq(schema.spacePage.projectId, projectId),
482
+ return db.query.spacePage.findMany({
483
+ where: and(eq(schema.spacePage.projectId, projectId), eq(schema.spacePage.wizardId, wizardId)),
317
484
  columns: { id: true, wizardId: true, title: true, markdown: true, updatedAt: true },
318
485
  orderBy: [asc(schema.spacePage.title)],
319
486
  });
320
- return pages
321
- .filter((p) => p.wizardId === wizardId || p.wizardId === null)
322
- .sort((a, b) => Number(b.wizardId === wizardId) - Number(a.wizardId === wizardId));
323
487
  }
324
488
  /** Writes a page of the run's wizard: a new one, or the one with this title, replaced or added to. */
325
489
  export async function writeRunPage(projectId, wizardId, input) {
@@ -359,3 +523,267 @@ export async function writeRunPage(projectId, wizardId, input) {
359
523
  });
360
524
  return { id, title, chars: markdown.length, made: true };
361
525
  }
526
+ async function originSpace(space) {
527
+ const project = await db.query.project.findFirst({
528
+ where: and(eq(schema.project.id, space), eq(schema.project.tenantId, currentTenant())),
529
+ columns: { id: true },
530
+ });
531
+ if (!project) {
532
+ throw new ServiceError("not_found", `There is no space "${space}".`);
533
+ }
534
+ await requireWritableProject(project.id);
535
+ return project.id;
536
+ }
537
+ /** Keeps the original of what a plugin wrote: a document of the space under the same key. */
538
+ async function originalFile(origin, projectId, key, label, original, text) {
539
+ const { putOriginFile } = await import("./project-files.js");
540
+ return putOriginFile(origin, {
541
+ space: projectId,
542
+ key,
543
+ label,
544
+ name: original.name,
545
+ mime: original.mime,
546
+ data: original.data,
547
+ text,
548
+ });
549
+ }
550
+ export async function putOriginPage(origin, input) {
551
+ const projectId = await originSpace(input.space);
552
+ const key = input.key.trim().slice(0, 300);
553
+ const existing = await db.query.spacePage.findFirst({
554
+ where: and(eq(schema.spacePage.projectId, projectId), eq(schema.spacePage.origin, origin), eq(schema.spacePage.originKey, key)),
555
+ });
556
+ if (existing?.kept) {
557
+ return { id: existing.id, state: "kept" };
558
+ }
559
+ const parent = input.parent
560
+ ? await db.query.spacePage.findFirst({
561
+ where: and(eq(schema.spacePage.projectId, projectId), eq(schema.spacePage.origin, origin), eq(schema.spacePage.originKey, input.parent)),
562
+ columns: { id: true },
563
+ })
564
+ : null;
565
+ if (input.parent && !parent) {
566
+ throw new ServiceError("invalid", `There is no page with the key "${input.parent}" to stand under.`);
567
+ }
568
+ const title = input.title.replace(/\s+/g, " ").trim().slice(0, 200) || "Seite";
569
+ const markdown = checkMarkdown(input.markdown);
570
+ const label = input.label?.trim().slice(0, 120) || null;
571
+ const parentId = parent?.id ?? null;
572
+ const fileId = input.original
573
+ ? await originalFile(origin, projectId, key, label, input.original, markdown)
574
+ : (existing?.fileId ?? null);
575
+ const position = input.position ?? existing?.position ?? (await nextPosition(projectId, parentId));
576
+ let id = existing?.id;
577
+ const moved = !existing || existing.title !== title || existing.parentId !== parentId;
578
+ if (existing) {
579
+ await db
580
+ .update(schema.spacePage)
581
+ .set({
582
+ title,
583
+ markdown,
584
+ parentId,
585
+ position,
586
+ originLabel: label,
587
+ fileId,
588
+ review: input.review ?? null,
589
+ ...(moved ? { slug: await freeSlug(projectId, title, { parentId }, existing.id) } : {}),
590
+ updatedAt: new Date(),
591
+ })
592
+ .where(eq(schema.spacePage.id, existing.id));
593
+ }
594
+ else {
595
+ await pageCount(projectId);
596
+ id = nanoid(12);
597
+ await db.insert(schema.spacePage).values({
598
+ id,
599
+ tenantId: currentTenant(),
600
+ projectId,
601
+ parentId,
602
+ position,
603
+ title,
604
+ slug: await freeSlug(projectId, title, { parentId }),
605
+ markdown,
606
+ origin,
607
+ originKey: key,
608
+ originLabel: label,
609
+ fileId,
610
+ review: input.review ?? null,
611
+ });
612
+ }
613
+ const set = input.categories
614
+ ? await setItemCategories(projectId, { pageId: id }, input.categories, "origin")
615
+ : { changed: false };
616
+ queueIndex(`${moved || set.changed ? "P" : "p"}:${id}`);
617
+ return { id: id, state: "written" };
618
+ }
619
+ export async function putOriginTable(origin, input) {
620
+ const projectId = await originSpace(input.space);
621
+ const key = input.key.trim().slice(0, 300);
622
+ const existing = await db.query.spaceTable.findFirst({
623
+ where: and(eq(schema.spaceTable.projectId, projectId), eq(schema.spaceTable.origin, origin), eq(schema.spaceTable.originKey, key)),
624
+ });
625
+ if (existing?.kept) {
626
+ return { id: existing.id, state: "kept" };
627
+ }
628
+ const columns = checkColumns(input.columns);
629
+ if (input.rows.length > SPACE_DATA_LIMITS.rowsPerTable) {
630
+ throw new ServiceError("refused", `A table keeps at most ${SPACE_DATA_LIMITS.rowsPerTable} rows.`);
631
+ }
632
+ let cells;
633
+ try {
634
+ cells = input.rows.map((row) => editCells(columns, {}, row));
635
+ }
636
+ catch (err) {
637
+ cellError(err);
638
+ }
639
+ const title = input.title.replace(/\s+/g, " ").trim().slice(0, 120) || "Tabelle";
640
+ const label = input.label?.trim().slice(0, 120) || null;
641
+ const csv = [
642
+ columns.map((c) => c.name).join(","),
643
+ ...cells.map((r) => columns.map((c) => String(r[c.id] ?? "")).join(",")),
644
+ ].join("\n");
645
+ const fileId = input.original
646
+ ? await originalFile(origin, projectId, key, label, input.original, csv)
647
+ : (existing?.fileId ?? null);
648
+ let id = existing?.id;
649
+ if (existing) {
650
+ await db
651
+ .update(schema.spaceTable)
652
+ .set({
653
+ title,
654
+ columns,
655
+ format: input.format ?? null,
656
+ originLabel: label,
657
+ fileId,
658
+ review: input.review ?? null,
659
+ ...(existing.title !== title
660
+ ? { slug: await freeSlug(projectId, title, "table", existing.id) }
661
+ : {}),
662
+ updatedAt: new Date(),
663
+ })
664
+ .where(eq(schema.spaceTable.id, existing.id));
665
+ await db.delete(schema.spaceTableRow).where(eq(schema.spaceTableRow.tableId, existing.id));
666
+ }
667
+ else {
668
+ await tableCount(projectId);
669
+ id = nanoid(12);
670
+ await db.insert(schema.spaceTable).values({
671
+ id,
672
+ tenantId: currentTenant(),
673
+ projectId,
674
+ title,
675
+ slug: await freeSlug(projectId, title, "table"),
676
+ format: input.format ?? null,
677
+ columns,
678
+ origin,
679
+ originKey: key,
680
+ originLabel: label,
681
+ fileId,
682
+ review: input.review ?? null,
683
+ });
684
+ }
685
+ const now = Date.now();
686
+ for (let i = 0; i < cells.length; i += 200) {
687
+ await db.insert(schema.spaceTableRow).values(cells.slice(i, i + 200).map((c, j) => ({
688
+ id: nanoid(14),
689
+ tableId: id,
690
+ cells: c,
691
+ // The order they came in: one millisecond apart.
692
+ createdAt: new Date(now - cells.length + i + j),
693
+ })));
694
+ }
695
+ if (input.categories) {
696
+ await setItemCategories(projectId, { tableId: id }, input.categories, "origin");
697
+ }
698
+ await syncTableRowCategories(id);
699
+ queueIndex(`t:${id}`);
700
+ return { id: id, state: "written" };
701
+ }
702
+ /** What a plugin wrote into a space, by its keys. */
703
+ export async function listOrigin(origin, space) {
704
+ const projectId = await originSpace(space);
705
+ const [pages, tables, files] = await Promise.all([
706
+ // What a document of the plugin was read into stands behind the document, not on its own.
707
+ db.query.spacePage.findMany({
708
+ where: and(eq(schema.spacePage.projectId, projectId), eq(schema.spacePage.origin, origin), isNotNull(schema.spacePage.originKey)),
709
+ columns: {
710
+ id: true,
711
+ originKey: true,
712
+ title: true,
713
+ kept: true,
714
+ review: true,
715
+ updatedAt: true,
716
+ },
717
+ }),
718
+ db.query.spaceTable.findMany({
719
+ where: and(eq(schema.spaceTable.projectId, projectId), eq(schema.spaceTable.origin, origin), isNotNull(schema.spaceTable.originKey)),
720
+ columns: {
721
+ id: true,
722
+ originKey: true,
723
+ title: true,
724
+ kept: true,
725
+ review: true,
726
+ updatedAt: true,
727
+ },
728
+ }),
729
+ db.query.projectFile.findMany({
730
+ where: and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.origin, origin)),
731
+ columns: { id: true, originKey: true, name: true, updatedAt: true },
732
+ }),
733
+ ]);
734
+ const [tree, tPaths, fPaths] = await Promise.all([
735
+ pageTree(projectId),
736
+ tablePaths(projectId),
737
+ filePaths(projectId),
738
+ ]);
739
+ return [
740
+ ...pages.map((p) => ({
741
+ key: p.originKey,
742
+ kind: "page",
743
+ id: p.id,
744
+ title: p.title,
745
+ path: tree.get(p.id)?.path ?? "",
746
+ state: (p.kept ? "kept" : "written"),
747
+ review: p.review,
748
+ updatedAt: p.updatedAt.toISOString(),
749
+ })),
750
+ ...tables.map((t) => ({
751
+ key: t.originKey,
752
+ kind: "table",
753
+ id: t.id,
754
+ title: t.title,
755
+ path: tPaths.get(t.id) ?? "",
756
+ state: (t.kept ? "kept" : "written"),
757
+ review: t.review,
758
+ updatedAt: t.updatedAt.toISOString(),
759
+ })),
760
+ ...files.map((f) => ({
761
+ key: f.originKey,
762
+ kind: "file",
763
+ id: f.id,
764
+ title: f.name,
765
+ path: fPaths.get(f.id) ?? "",
766
+ state: "written",
767
+ review: null,
768
+ updatedAt: f.updatedAt.toISOString(),
769
+ })),
770
+ ];
771
+ }
772
+ /**
773
+ * Takes out what a plugin wrote under a key, or without a key everything it wrote into the
774
+ * space. What a person changed since stays. Returns how many items went.
775
+ */
776
+ export async function removeOrigin(origin, space, key) {
777
+ const projectId = await originSpace(space);
778
+ const pages = await db
779
+ .delete(schema.spacePage)
780
+ .where(and(eq(schema.spacePage.projectId, projectId), eq(schema.spacePage.origin, origin), eq(schema.spacePage.kept, false), key === undefined ? undefined : eq(schema.spacePage.originKey, key)))
781
+ .returning({ id: schema.spacePage.id });
782
+ const tables = await db
783
+ .delete(schema.spaceTable)
784
+ .where(and(eq(schema.spaceTable.projectId, projectId), eq(schema.spaceTable.origin, origin), eq(schema.spaceTable.kept, false), key === undefined ? undefined : eq(schema.spaceTable.originKey, key)))
785
+ .returning({ id: schema.spaceTable.id });
786
+ const { removeOriginFiles } = await import("./project-files.js");
787
+ const files = await removeOriginFiles(origin, projectId, key);
788
+ return pages.length + tables.length + files;
789
+ }