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.
- package/apps/runtime/dist/agents/project-assistant.js +20 -2
- package/apps/runtime/dist/authoring/guide.js +6 -3
- package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +160 -8
- package/apps/runtime/dist/engine/steps.js +3 -11
- package/apps/runtime/dist/env.js +2 -0
- package/apps/runtime/dist/files/storage.js +15 -0
- package/apps/runtime/dist/models.js +38 -0
- package/apps/runtime/dist/plugins/loader.js +2 -0
- package/apps/runtime/dist/routes/studio.js +147 -6
- package/apps/runtime/dist/services/cloud.js +59 -1
- package/apps/runtime/dist/services/knowledge-model.js +558 -0
- package/apps/runtime/dist/services/knowledge.js +721 -0
- package/apps/runtime/dist/services/project-files.js +114 -6
- package/apps/runtime/dist/services/project-index.js +632 -94
- package/apps/runtime/dist/services/relevance.js +90 -0
- package/apps/runtime/dist/services/space-categories.js +882 -0
- package/apps/runtime/dist/services/space-data.js +466 -38
- package/apps/runtime/dist/services/space-origin.js +164 -0
- package/apps/runtime/dist/services/spaces.js +155 -1
- package/apps/runtime/dist/tools/pages.js +27 -22
- package/apps/runtime/dist/tools/project.js +130 -40
- package/apps/web/dist/assets/{Account-LVXhagaS.js → Account-Cp1cgqjV.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-DM0KAoy6.js → ChangelogDialog-Chke1qUe.js} +1 -1
- package/apps/web/dist/assets/{ChatPanel-CxOQmtv-.js → ChatPanel-DWrwewr6.js} +1 -1
- package/apps/web/dist/assets/{CreditsDialog-DkW3YT44.js → CreditsDialog-Ckema8Ss.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-hbisE1Wf.js → EditorPage-XX6pfHk5.js} +1 -1
- package/apps/web/dist/assets/{FlowPreview-v7or29ku.js → FlowPreview-DS545n78.js} +1 -1
- package/apps/web/dist/assets/{Harness-RZF-bBmb.js → Harness-H3yXP-2L.js} +1 -1
- package/apps/web/dist/assets/{HomePage-mxG-a_KU.js → HomePage-Bds84ShK.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-CiPyUt3E.js → ImportWizard-0_NkA8aS.js} +1 -1
- package/apps/web/dist/assets/{LocalRuntime-Dqp9wkN3.js → LocalRuntime-DnABzTOe.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-DHwRsi0_.js → NewWizardPage-BqP75V9K.js} +2 -2
- package/apps/web/dist/assets/ProjectPage-DbOvwOdZ.js +5 -0
- package/apps/web/dist/assets/{PublicRunner-TGkOwbAP.js → PublicRunner-CtZ7vxl_.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-DzHGshZj.js → RunnerView-xV5bWclu.js} +1 -1
- package/apps/web/dist/assets/{SettingsPage-CG3Xtfej.js → SettingsPage-DKt55zFh.js} +1 -1
- package/apps/web/dist/assets/{SetupPage-O1GfPlqX.js → SetupPage-c6mMD9zN.js} +1 -1
- package/apps/web/dist/assets/{SharePage-CCNUhtry.js → SharePage-DTKIbMj-.js} +1 -1
- package/apps/web/dist/assets/{api-DB22_B7G.js → api-CVG-xDEn.js} +1 -1
- package/apps/web/dist/assets/changelog-CXXT0zsp.js +1 -0
- package/apps/web/dist/assets/{esm-3TT9i_rv.js → esm-DLM3jdbm.js} +1 -1
- package/apps/web/dist/assets/{index-CLe9Wrpu.js → index-1gmaxDSk.js} +3 -3
- package/apps/web/dist/assets/index-Bwyehu0k.css +1 -0
- package/apps/web/dist/assets/{meta-CEsBCPfA.js → meta-m947x6uG.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-pOcjznGJ.js → settings-menu-DUY6__C3.js} +1 -1
- package/apps/web/dist/assets/ui-BPY2pIh0.js +1 -0
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +164 -0
- package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
- package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +1 -16
- package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +8 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +5 -0
- package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/space-data.js +2 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/ProjectPage-Bb7jN_6n.js +0 -4
- package/apps/web/dist/assets/changelog-BkIJdZ7M.js +0 -1
- package/apps/web/dist/assets/index-BPpKz65x.css +0 -1
- 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
|
-
|
|
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,
|
|
96
|
+
.set({ updatedAt: new Date(), ...(table.origin && !table.kept ? { kept: true } : {}) })
|
|
97
|
+
.where(eq(schema.spaceTable.id, table.id));
|
|
93
98
|
}
|
|
94
|
-
|
|
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,
|
|
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
|
-
:
|
|
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
|
|
113
|
-
title
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
262
|
-
if (n
|
|
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
|
|
271
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
313
|
-
/** The pages a run
|
|
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
|
-
|
|
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
|
+
}
|