wizards 0.2.11 → 0.2.13

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 (86) hide show
  1. package/apps/runtime/dist/authoring/guide.js +8 -2
  2. package/apps/runtime/dist/db/migrations/tenant/0007_space_data.sql +43 -0
  3. package/apps/runtime/dist/db/migrations/tenant/meta/0007_snapshot.json +2012 -0
  4. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  5. package/apps/runtime/dist/db/schema.js +55 -0
  6. package/apps/runtime/dist/engine/runner.js +1 -1
  7. package/apps/runtime/dist/routes/api.js +14 -1
  8. package/apps/runtime/dist/routes/runs.js +13 -7
  9. package/apps/runtime/dist/routes/studio.js +89 -1
  10. package/apps/runtime/dist/services/cloud.js +62 -2
  11. package/apps/runtime/dist/services/runs.js +69 -1
  12. package/apps/runtime/dist/services/space-data.js +361 -0
  13. package/apps/runtime/dist/services/spaces.js +95 -1
  14. package/apps/runtime/dist/store/errors.js +3 -0
  15. package/apps/runtime/dist/store/index.js +19 -5
  16. package/apps/runtime/dist/store/shared-lists.js +124 -0
  17. package/apps/runtime/dist/tools/index.js +4 -0
  18. package/apps/runtime/dist/tools/pages.js +51 -0
  19. package/apps/runtime/dist/tools/store.js +3 -3
  20. package/apps/web/dist/assets/Account-ulB5WeOT.js +1 -0
  21. package/apps/web/dist/assets/ChangelogDialog-BVR5814e.js +1 -0
  22. package/apps/web/dist/assets/ChatPanel-C7uql4_d.js +1 -0
  23. package/apps/web/dist/assets/{CreditsDialog-DSjsgLoo.js → CreditsDialog-o9h0Nrui.js} +1 -1
  24. package/apps/web/dist/assets/EditorPage-CskikVRK.js +2 -0
  25. package/apps/web/dist/assets/FlowPreview-YaqUirty.js +1 -0
  26. package/apps/web/dist/assets/{Harness-OlRaoHGd.js → Harness-ut5xGR_o.js} +2 -2
  27. package/apps/web/dist/assets/HomePage-Drys3BPU.js +1 -0
  28. package/apps/web/dist/assets/{ImportWizard-BGs5dv94.js → ImportWizard-BGqkJFeZ.js} +1 -1
  29. package/apps/web/dist/assets/LocalRuntime-DwPJjv3H.js +1 -0
  30. package/apps/web/dist/assets/NewWizardPage-CrBSpeIw.js +2 -0
  31. package/apps/web/dist/assets/ProjectPage-CAdSHd0Y.js +4 -0
  32. package/apps/web/dist/assets/PublicRunner-JleX2stQ.js +1 -0
  33. package/apps/web/dist/assets/RunnerView-BWG4Odbt.js +70 -0
  34. package/apps/web/dist/assets/SettingsPage-CYOdJ5FL.js +1 -0
  35. package/apps/web/dist/assets/SetupPage-CMVwnLQm.js +1 -0
  36. package/apps/web/dist/assets/SharePage-CKppWAtr.js +1 -0
  37. package/apps/web/dist/assets/{api-u3nydnnp.js → api-DTLkC0ge.js} +2 -2
  38. package/apps/web/dist/assets/changelog-dI1t4l0T.js +1 -0
  39. package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +1 -0
  40. package/apps/web/dist/assets/{definition-Co2Mg1qo.js → definition-Dp0DR2D6.js} +1 -1
  41. package/apps/web/dist/assets/{esm-CyCiCaFy.js → esm-B6hLaDRL.js} +1 -1
  42. package/apps/web/dist/assets/index-BWxUOTc4.js +10 -0
  43. package/apps/web/dist/assets/index-DMwk_afK.css +1 -0
  44. package/apps/web/dist/assets/lock-DZL2N67v.js +1 -0
  45. package/apps/web/dist/assets/meta-CZWUthoQ.js +1 -0
  46. package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +1 -0
  47. package/apps/web/dist/assets/settings-menu-Iz6kyoDe.js +1 -0
  48. package/apps/web/dist/assets/{type-BqbppSsp.js → table-2-BdDuBcF2.js} +1 -1
  49. package/apps/web/dist/assets/ui-CHX2gEQl.js +1 -0
  50. package/apps/web/dist/assets/video-9fDPRPCv.js +1 -0
  51. package/apps/web/dist/index.html +4 -4
  52. package/apps/web/dist/mcp/flow.html +5 -5
  53. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +12 -4
  54. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -1
  55. package/node_modules/@engenty-wizards/shared/dist/definition.js +36 -2
  56. package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +69 -0
  57. package/node_modules/@engenty-wizards/shared/dist/space-data.js +46 -0
  58. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +2 -0
  59. package/package.json +1 -1
  60. package/apps/web/dist/assets/Account-BJPPu___.js +0 -1
  61. package/apps/web/dist/assets/ChangelogDialog-D5Jc3poI.js +0 -1
  62. package/apps/web/dist/assets/ChatPanel-n7GEZxTX.js +0 -1
  63. package/apps/web/dist/assets/EditorPage-teTTDbnX.js +0 -2
  64. package/apps/web/dist/assets/FlowPreview-4cRsWwO8.js +0 -1
  65. package/apps/web/dist/assets/HomePage-BGQMVqEr.js +0 -1
  66. package/apps/web/dist/assets/LocalRuntime-WHeeZvB4.js +0 -1
  67. package/apps/web/dist/assets/NewWizardPage-h3dWMUgw.js +0 -2
  68. package/apps/web/dist/assets/ProjectPage-Cfnwi-pK.js +0 -2
  69. package/apps/web/dist/assets/PublicRunner-Oa4uTkvr.js +0 -1
  70. package/apps/web/dist/assets/ReadOnly-B0-ReU_0.js +0 -1
  71. package/apps/web/dist/assets/RunnerView-njFeqF1A.js +0 -2
  72. package/apps/web/dist/assets/SettingsPage-D4xJ8tEu.js +0 -1
  73. package/apps/web/dist/assets/SetupPage-DUyvWSdi.js +0 -1
  74. package/apps/web/dist/assets/SharePage-Bikt8bn4.js +0 -1
  75. package/apps/web/dist/assets/arrow-left-hTN1VoH1.js +0 -1
  76. package/apps/web/dist/assets/changelog-CT8yK95a.js +0 -1
  77. package/apps/web/dist/assets/circle-alert-B76z9TWZ.js +0 -1
  78. package/apps/web/dist/assets/index-CQQD8QNR.css +0 -1
  79. package/apps/web/dist/assets/index-Z0fbfnW7.js +0 -10
  80. package/apps/web/dist/assets/lock-CzNXAYdi.js +0 -1
  81. package/apps/web/dist/assets/meta-DsCJykuf.js +0 -1
  82. package/apps/web/dist/assets/mic-Bvz4mA2j.js +0 -1
  83. package/apps/web/dist/assets/outputs-Ba-Tblsj.js +0 -69
  84. package/apps/web/dist/assets/refresh-cw-Cmk7VCe0.js +0 -1
  85. package/apps/web/dist/assets/ui-gGAdXx-P.js +0 -1
  86. package/apps/web/dist/assets/video-DBmwF1Jm.js +0 -1
@@ -50,6 +50,13 @@
50
50
  "when": 1791283092075,
51
51
  "tag": "0006_hard_war_machine",
52
52
  "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1791287441521,
58
+ "tag": "0007_space_data",
59
+ "breakpoints": true
53
60
  }
54
61
  ]
55
62
  }
@@ -225,6 +225,61 @@ export const projectChunk = sqliteTable("project_chunk", {
225
225
  index("project_chunk_file").on(t.fileId),
226
226
  index("project_chunk_ref").on(t.projectId, t.plugin, t.ref),
227
227
  ]);
228
+ // --- The space's data -------------------------------------------------------
229
+ // Tables and pages a space keeps, its own or one of its wizards'. Shared by everyone who works
230
+ // in the space, unlike the wizard's store below, which belongs to one person.
231
+ /** A table: its columns are engenty data-table columns, its rows in `space_table_row`. */
232
+ export const spaceTable = sqliteTable("space_table", {
233
+ id: text("id").primaryKey(),
234
+ tenantId: text("tenant_id").notNull(),
235
+ projectId: text("project_id")
236
+ .notNull()
237
+ .references(() => project.id, { onDelete: "cascade" }),
238
+ /** The wizard it belongs to; null: the space's own. */
239
+ wizardId: text("wizard_id").references(() => wizard.id, { onDelete: "cascade" }),
240
+ /**
241
+ * The wizard's shared list it keeps: its title, columns and key come from the wizard's
242
+ * definition, its rows from every run. Null: a table made in the studio.
243
+ */
244
+ list: text("list"),
245
+ /** The column a row is matched on, as the list says. */
246
+ keyColumn: text("key_column"),
247
+ title: text("title").notNull(),
248
+ columns: text("columns", { mode: "json" }).$type().notNull(),
249
+ createdAt: createdAt(),
250
+ updatedAt: updatedAt(),
251
+ }, (t) => [
252
+ index("space_table_project").on(t.projectId, t.wizardId),
253
+ uniqueIndex("space_table_list").on(t.wizardId, t.list),
254
+ ]);
255
+ /** A row of a table: cells by column id. Rows stand in the order they were added. */
256
+ export const spaceTableRow = sqliteTable("space_table_row", {
257
+ id: text("id").primaryKey(),
258
+ tableId: text("table_id")
259
+ .notNull()
260
+ .references(() => spaceTable.id, { onDelete: "cascade" }),
261
+ /** The key column's value, normalised: saving a row with a known key updates that row. */
262
+ key: text("key"),
263
+ cells: text("cells", { mode: "json" }).$type().notNull(),
264
+ createdAt: createdAt(),
265
+ updatedAt: updatedAt(),
266
+ }, (t) => [
267
+ index("space_table_row_table").on(t.tableId, t.createdAt),
268
+ index("space_table_row_key").on(t.tableId, t.key),
269
+ ]);
270
+ /** A page: a title and markdown, what the editor shows and an agent reads and writes. */
271
+ export const spacePage = sqliteTable("space_page", {
272
+ id: text("id").primaryKey(),
273
+ tenantId: text("tenant_id").notNull(),
274
+ projectId: text("project_id")
275
+ .notNull()
276
+ .references(() => project.id, { onDelete: "cascade" }),
277
+ wizardId: text("wizard_id").references(() => wizard.id, { onDelete: "cascade" }),
278
+ title: text("title").notNull(),
279
+ markdown: text("markdown").notNull().default(""),
280
+ createdAt: createdAt(),
281
+ updatedAt: updatedAt(),
282
+ }, (t) => [index("space_page_project").on(t.projectId, t.wizardId)]);
228
283
  // --- The wizard's store ------------------------------------------------------
229
284
  // What a wizard keeps between runs for one person. `holder` is that person: "u:<userId>" for a
230
285
  // signed-in admin, "v:<visitorId>" for an end user on a shared link.
@@ -226,7 +226,7 @@ async function storedLists(run) {
226
226
  const scope = scopeOf(run);
227
227
  const out = {};
228
228
  for (const def of run.definition.lists ?? []) {
229
- out[def.id] = { def, rows: await listRows(scope, def.id) };
229
+ out[def.id] = { def, rows: await listRows(scope, def) };
230
230
  }
231
231
  return out;
232
232
  }
@@ -6,7 +6,7 @@ import { allowed } from "../limits.js";
6
6
  import { authenticate, principalOf } from "../mcp/auth.js";
7
7
  import { ServiceError } from "../services/errors.js";
8
8
  import { deleteFile, draftFiles, writeFile } from "../services/files.js";
9
- import { checkOnServer, checkSchema, listSyncedSpaces, patchSyncedWizard, removeSyncedSpace, removeSyncedWizard, rotateSyncedLink, syncSettingsSchema, syncWizard, syncWizardSchema, } from "../services/spaces.js";
9
+ import { checkOnServer, checkSchema, listSyncedSpaces, patchSyncedWizard, removeSyncedSpace, removeSyncedWizard, rotateSyncedLink, syncDataSchema, syncSettingsSchema, syncSpaceData, syncWizard, syncWizardSchema, } from "../services/spaces.js";
10
10
  import { createWizard, ownedWizard, publishWizard, writeDraft } from "../services/wizards.js";
11
11
  import { codeOf, tenantStatus } from "../tenants/control.js";
12
12
  const importSchema = z.object({
@@ -83,6 +83,19 @@ export const apiRoutes = new Hono()
83
83
  }
84
84
  const body = syncWizardSchema.parse(await c.req.json());
85
85
  return withTenant(who.tenantId, async () => c.json(await syncWizard(c.req.param("spaceId"), c.req.param("wizardId"), body)));
86
+ })
87
+ // The space's own tables and pages, sent with a wizard when they changed.
88
+ .put("/spaces/:spaceId/data", tooLarge, async (c) => {
89
+ const who = await caller(c, ["wizards:write"]);
90
+ if (who instanceof Response) {
91
+ return who;
92
+ }
93
+ const limited = withinSyncLimit(c, who);
94
+ if (limited) {
95
+ return limited;
96
+ }
97
+ const body = syncDataSchema.parse(await c.req.json());
98
+ return withTenant(who.tenantId, async () => c.json(await syncSpaceData(c.req.param("spaceId"), body)));
86
99
  })
87
100
  .patch("/spaces/:spaceId/wizards/:wizardId", async (c) => {
88
101
  const who = await caller(c, ["wizards:publish"]);
@@ -594,7 +594,8 @@ export const runRoutes = new Hono()
594
594
  // --- the wizard's lists and files for this person -------------------------------
595
595
  .post("/:id/lists/:list/rows", async (c) => {
596
596
  const run = await accessibleRun(c, c.req.param("id"));
597
- const def = run?.definition.lists?.find((l) => l.id === c.req.param("list"));
597
+ // A shared list is every run's: the person never sees it.
598
+ const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
598
599
  if (!run || !def) {
599
600
  return c.json({ error: "not found" }, 404);
600
601
  }
@@ -612,7 +613,8 @@ export const runRoutes = new Hono()
612
613
  })
613
614
  .patch("/:id/lists/:list/rows/:rowId", async (c) => {
614
615
  const run = await accessibleRun(c, c.req.param("id"));
615
- const def = run?.definition.lists?.find((l) => l.id === c.req.param("list"));
616
+ // A shared list is every run's: the person never sees it.
617
+ const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
616
618
  if (!run || !def) {
617
619
  return c.json({ error: "not found" }, 404);
618
620
  }
@@ -632,7 +634,8 @@ export const runRoutes = new Hono()
632
634
  })
633
635
  .delete("/:id/lists/:list/rows/:rowId", async (c) => {
634
636
  const run = await accessibleRun(c, c.req.param("id"));
635
- const def = run?.definition.lists?.find((l) => l.id === c.req.param("list"));
637
+ // A shared list is every run's: the person never sees it.
638
+ const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
636
639
  if (!run || !def) {
637
640
  return c.json({ error: "not found" }, 404);
638
641
  }
@@ -642,13 +645,14 @@ export const runRoutes = new Hono()
642
645
  })
643
646
  .get("/:id/lists/:list/download", async (c) => {
644
647
  const run = await accessibleRun(c, c.req.param("id"));
645
- const def = run?.definition.lists?.find((l) => l.id === c.req.param("list"));
648
+ // A shared list is every run's: the person never sees it.
649
+ const def = run?.definition.lists?.find((l) => l.id === c.req.param("list") && !l.shared);
646
650
  const format = z.enum(FORMATS).safeParse(c.req.query("format"));
647
651
  if (!run || !def || !format.success || !LIST_FORMATS.includes(format.data)) {
648
652
  return c.notFound();
649
653
  }
650
654
  const scope = scopeOf(run);
651
- const download = await listDownload(def, await listRows(scope, def.id), format.data, `${run.definition.title} ${def.title}`, async (path) => {
655
+ const download = await listDownload(def, await listRows(scope, def), format.data, `${run.definition.title} ${def.title}`, async (path) => {
652
656
  const found = await readStoreFile(scope, path).catch(() => null);
653
657
  return found ? new Uint8Array(found.data) : null;
654
658
  });
@@ -660,10 +664,12 @@ export const runRoutes = new Hono()
660
664
  return c.json({ error: "not found" }, 404);
661
665
  }
662
666
  const scope = scopeOf(run);
663
- const lists = await Promise.all((run.definition.lists ?? []).map(async (def) => ({
667
+ const lists = await Promise.all((run.definition.lists ?? [])
668
+ .filter((def) => !def.shared)
669
+ .map(async (def) => ({
664
670
  id: def.id,
665
671
  title: def.title,
666
- rows: (await listRows(scope, def.id)).length,
672
+ rows: (await listRows(scope, def)).length,
667
673
  })));
668
674
  const secrets = await listSecrets(scope);
669
675
  return c.json({
@@ -37,7 +37,8 @@ import { profileSchema, saveProfile, userProfile } from "../services/profile.js"
37
37
  import { addProjectFile, fileView, orderProjectFiles, projectFile, projectFileContent, projectFiles, reindexProjectFile, removeProjectFile, updateProjectFile, } from "../services/project-files.js";
38
38
  import { documentText, searchProject } from "../services/project-index.js";
39
39
  import { createProject, deleteProject, listProjects, maskedServers, ownedProject, projectLimit, projectPatchSchema, updateProject, } from "../services/projects.js";
40
- import { listRuns, startTestRun } from "../services/runs.js";
40
+ import { listResults, listRuns, startTestRun } from "../services/runs.js";
41
+ import { addTableRow, createPage, createTable, deletePage, deleteTable, deleteTableRows, getPage, getTable, listSpaceData, updatePage, updateTable, updateTableRow, } from "../services/space-data.js";
41
42
  import { draftWidgetPreview } from "../services/widgets.js";
42
43
  import { createWizard, deleteWizard, duplicateWizard, listWizards, ownedWizard, rotateShareLink, updateWizardSettings, wizardMessages, wizardState, writableWizard, writeDraft, } from "../services/wizards.js";
43
44
  import { readSetting, writeSetting } from "../settings.js";
@@ -447,6 +448,93 @@ export const studio = new Hono()
447
448
  const project = await ownedProject(c.get("user").id, c.req.param("id"));
448
449
  await removeConnector(project.id, c.req.param("connectorId"));
449
450
  return c.json({ ok: true });
451
+ })
452
+ // The runs of the space's wizards that reached their result: of one wizard, or of all.
453
+ .get("/projects/:id/results", async (c) => {
454
+ const project = await ownedProject(c.get("user").id, c.req.param("id"));
455
+ const query = z
456
+ .object({
457
+ wizard: z.string().optional(),
458
+ q: z.string().max(200).optional(),
459
+ mode: z.enum(["live", "test"]).optional(),
460
+ days: z.coerce.number().int().min(1).max(3650).optional(),
461
+ })
462
+ .parse(c.req.query());
463
+ return c.json(await listResults(project.id, {
464
+ wizardId: query.wizard || undefined,
465
+ q: query.q,
466
+ mode: query.mode,
467
+ days: query.days,
468
+ }));
469
+ })
470
+ // --- the space's tables and pages --------------------------------------------
471
+ .get("/projects/:id/data", async (c) => c.json(await listSpaceData(c.get("user").id, c.req.param("id"))))
472
+ .post("/projects/:id/tables", async (c) => {
473
+ const body = z
474
+ .object({
475
+ title: z.string().max(120),
476
+ wizardId: z.string().nullish(),
477
+ columns: z.unknown().optional(),
478
+ })
479
+ .parse(await c.req.json());
480
+ return c.json(await createTable(c.get("user").id, c.req.param("id"), body));
481
+ })
482
+ .get("/tables/:id", async (c) => c.json(await getTable(c.get("user").id, c.req.param("id"))))
483
+ .patch("/tables/:id", async (c) => {
484
+ const body = z
485
+ .object({
486
+ title: z.string().max(120).optional(),
487
+ wizardId: z.string().nullish(),
488
+ columns: z.unknown().optional(),
489
+ })
490
+ .parse(await c.req.json());
491
+ await updateTable(c.get("user").id, c.req.param("id"), body);
492
+ return c.json({ ok: true });
493
+ })
494
+ .delete("/tables/:id", async (c) => {
495
+ await deleteTable(c.get("user").id, c.req.param("id"));
496
+ return c.json({ ok: true });
497
+ })
498
+ .post("/tables/:id/rows", async (c) => {
499
+ const body = z
500
+ .object({ cells: z.record(z.string(), z.unknown()).optional() })
501
+ .parse(await c.req.json());
502
+ return c.json(await addTableRow(c.get("user").id, c.req.param("id"), body.cells));
503
+ })
504
+ .patch("/tables/:id/rows/:rowId", async (c) => {
505
+ const body = z.object({ cells: z.record(z.string(), z.unknown()) }).parse(await c.req.json());
506
+ return c.json(await updateTableRow(c.get("user").id, c.req.param("id"), c.req.param("rowId"), body.cells));
507
+ })
508
+ .post("/tables/:id/rows/delete", async (c) => {
509
+ const body = z.object({ ids: z.array(z.string()).max(5000) }).parse(await c.req.json());
510
+ await deleteTableRows(c.get("user").id, c.req.param("id"), body.ids);
511
+ return c.json({ ok: true });
512
+ })
513
+ .post("/projects/:id/pages", async (c) => {
514
+ const body = z
515
+ .object({
516
+ title: z.string().max(200),
517
+ wizardId: z.string().nullish(),
518
+ markdown: z.string().optional(),
519
+ })
520
+ .parse(await c.req.json());
521
+ return c.json(await createPage(c.get("user").id, c.req.param("id"), body));
522
+ })
523
+ .get("/pages/:id", async (c) => c.json(await getPage(c.get("user").id, c.req.param("id"))))
524
+ .patch("/pages/:id", async (c) => {
525
+ const body = z
526
+ .object({
527
+ title: z.string().max(200).optional(),
528
+ wizardId: z.string().nullish(),
529
+ markdown: z.string().optional(),
530
+ })
531
+ .parse(await c.req.json());
532
+ await updatePage(c.get("user").id, c.req.param("id"), body);
533
+ return c.json({ ok: true });
534
+ })
535
+ .delete("/pages/:id", async (c) => {
536
+ await deletePage(c.get("user").id, c.req.param("id"));
537
+ return c.json({ ok: true });
450
538
  })
451
539
  // --- wizards ---------------------------------------------------------------
452
540
  .get("/projects/:id/wizards", async (c) => {
@@ -1,4 +1,5 @@
1
- import { and, eq, isNotNull, like } from "drizzle-orm";
1
+ import { createHash } from "node:crypto";
2
+ import { and, asc, eq, inArray, isNotNull, isNull, like } from "drizzle-orm";
2
3
  import { accountToken, linkedAccount } from "../auth/account.js";
3
4
  import { control, controlDb, db, schema } from "../db/client.js";
4
5
  import { env } from "../env.js";
@@ -94,10 +95,61 @@ async function spaceOf(userId, projectId) {
94
95
  : null,
95
96
  };
96
97
  }
98
+ /** The space's own tables and pages, as they are sent: not what a wizard's runs keep. */
99
+ async function spaceDataOf(projectId) {
100
+ const own = (table) => and(eq(table.projectId, projectId), isNull(table.wizardId));
101
+ const tables = await db.query.spaceTable.findMany({
102
+ where: own(schema.spaceTable),
103
+ orderBy: [asc(schema.spaceTable.id)],
104
+ });
105
+ const rows = tables.length
106
+ ? await db.query.spaceTableRow.findMany({
107
+ where: inArray(schema.spaceTableRow.tableId, tables.map((t) => t.id)),
108
+ orderBy: [asc(schema.spaceTableRow.createdAt), asc(schema.spaceTableRow.id)],
109
+ })
110
+ : [];
111
+ const pages = await db.query.spacePage.findMany({
112
+ where: own(schema.spacePage),
113
+ orderBy: [asc(schema.spacePage.id)],
114
+ });
115
+ return {
116
+ tables: tables.map((t) => ({
117
+ id: t.id,
118
+ title: t.title,
119
+ columns: t.columns,
120
+ rows: rows.filter((r) => r.tableId === t.id).map((r) => ({ id: r.id, cells: r.cells })),
121
+ })),
122
+ pages: pages.map((p) => ({ id: p.id, title: p.title, markdown: p.markdown })),
123
+ };
124
+ }
125
+ /**
126
+ * Sends the space's own tables and pages along with a wizard, when they changed since they last
127
+ * arrived. A sending that fails is tried with the next wizard; the wizard is there either way.
128
+ */
129
+ async function syncSpaceDataToCloud(projectId) {
130
+ const data = await spaceDataOf(projectId);
131
+ const hash = createHash("sha256").update(JSON.stringify(data)).digest("hex");
132
+ const sent = `cloud-data:${projectId}`;
133
+ if ((await readSetting(sent)) === hash) {
134
+ return;
135
+ }
136
+ try {
137
+ const answer = await call("PUT", `/spaces/${projectId}/data`, data, 120_000);
138
+ if (answer.ok) {
139
+ await writeSetting(sent, hash);
140
+ }
141
+ else {
142
+ console.error("[cloud] the space's data was not taken:", answer.status, answer.body);
143
+ }
144
+ }
145
+ catch (err) {
146
+ console.error("[cloud] sending the space's data failed:", err);
147
+ }
148
+ }
97
149
  /**
98
150
  * Sends a wizard's published version to the cloud: the same wizard again updates its copy
99
151
  * there, whose link stays. What came back is kept; a try that failed is kept as such beside
100
- * the copy from before.
152
+ * the copy from before. The space's own tables and pages follow it.
101
153
  */
102
154
  export async function syncToCloud(userId, wizardId) {
103
155
  const w = await ownedWizard(userId, wizardId);
@@ -136,6 +188,7 @@ export async function syncToCloud(userId, wizardId) {
136
188
  error: null,
137
189
  };
138
190
  await writeSetting(key(w.id), state);
191
+ await syncSpaceDataToCloud(w.projectId);
139
192
  return state;
140
193
  }
141
194
  catch (err) {
@@ -253,9 +306,16 @@ export async function retryFailedSends(userId) {
253
306
  /** The account was unlinked: what this runtime kept of its cloud belongs to no account now. */
254
307
  export async function forgetCloud() {
255
308
  await controlDb.delete(control.setting).where(like(control.setting.key, "cloud:%"));
309
+ await forgetSentData();
310
+ }
311
+ /** The space's data counts as not sent: the next wizard takes it along again. */
312
+ async function forgetSentData() {
313
+ await controlDb.delete(control.setting).where(like(control.setting.key, "cloud-data:%"));
256
314
  }
257
315
  /** Sends every published wizard: once after an account was linked. */
258
316
  export async function syncAllToCloud(userId) {
317
+ // A cloud that was linked anew, or whose spaces were replaced, has none of it.
318
+ await forgetSentData();
259
319
  const wizards = await db
260
320
  .select({ id: schema.wizard.id })
261
321
  .from(schema.wizard)
@@ -1,5 +1,5 @@
1
1
  import { formatsFor } from "@engenty-wizards/shared/definition";
2
- import { and, count, desc, eq, inArray, or } from "drizzle-orm";
2
+ import { and, count, desc, eq, gte, inArray, max, or, sql } from "drizzle-orm";
3
3
  import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
4
4
  import { db, schema } from "../db/client.js";
5
5
  import { answerAsk, unattended } from "../engine/asks.js";
@@ -384,3 +384,71 @@ export async function listRuns(userId, wizardId) {
384
384
  stepTitle: r.definition.steps.find((s) => s.id === r.cursor)?.title ?? null,
385
385
  }));
386
386
  }
387
+ /**
388
+ * The results of a space: its wizards' runs that reached their result, newest first, narrowed by
389
+ * `filter`; and each wizard with how many it has and when its last one came, the latest first.
390
+ */
391
+ export async function listResults(projectId, filter = {}) {
392
+ const wizards = await db
393
+ .select({
394
+ id: schema.wizard.id,
395
+ title: schema.wizard.title,
396
+ results: count(schema.run.id),
397
+ lastAt: max(schema.run.updatedAt),
398
+ })
399
+ .from(schema.wizard)
400
+ .leftJoin(schema.run, and(eq(schema.run.wizardId, schema.wizard.id), eq(schema.run.status, "done")))
401
+ .where(and(eq(schema.wizard.projectId, projectId), eq(schema.wizard.tenantId, currentTenant())))
402
+ .groupBy(schema.wizard.id)
403
+ // Wizards without a result last: SQLite sorts null below every time.
404
+ .orderBy(desc(max(schema.run.updatedAt)), desc(schema.wizard.updatedAt));
405
+ const q = filter.q?.trim().toLowerCase();
406
+ const like = q ? `%${q.replace(/[\\%_]/g, (c) => `\\${c}`)}%` : null;
407
+ const titled = like
408
+ ? new Set(wizards.filter((w) => w.title.toLowerCase().includes(q)).map((w) => w.id))
409
+ : null;
410
+ const ids = wizards.map((w) => w.id).filter((id) => !filter.wizardId || id === filter.wizardId);
411
+ const rows = ids.length
412
+ ? await db
413
+ .select({
414
+ id: schema.run.id,
415
+ wizardId: schema.run.wizardId,
416
+ mode: schema.run.mode,
417
+ version: schema.run.version,
418
+ costMicros: schema.run.costMicros,
419
+ updatedAt: schema.run.updatedAt,
420
+ })
421
+ .from(schema.run)
422
+ .where(and(inArray(schema.run.wizardId, ids), eq(schema.run.status, "done"), filter.mode ? eq(schema.run.mode, filter.mode) : undefined, filter.days
423
+ ? gte(schema.run.updatedAt, new Date(Date.now() - filter.days * 86_400_000))
424
+ : undefined, like
425
+ ? or(titled?.size ? inArray(schema.run.wizardId, [...titled]) : undefined, sql `lower(${schema.run.state}) like ${like} escape '\\'`)
426
+ : undefined))
427
+ .orderBy(desc(schema.run.updatedAt))
428
+ .limit(100)
429
+ : [];
430
+ const assetCounts = rows.length
431
+ ? await db
432
+ .select({ runId: schema.asset.runId, n: count() })
433
+ .from(schema.asset)
434
+ .where(inArray(schema.asset.runId, rows.map((r) => r.id)))
435
+ .groupBy(schema.asset.runId)
436
+ : [];
437
+ return {
438
+ wizards: wizards.map((w) => ({
439
+ id: w.id,
440
+ title: w.title,
441
+ results: w.results,
442
+ lastAt: w.lastAt ? new Date(w.lastAt).toISOString() : null,
443
+ })),
444
+ runs: rows.map((r) => ({
445
+ id: r.id,
446
+ wizardId: r.wizardId,
447
+ mode: r.mode,
448
+ version: r.version,
449
+ doneAt: r.updatedAt.toISOString(),
450
+ credits: Math.ceil(r.costMicros / MICROS_PER_CREDIT),
451
+ assets: assetCounts.find((a) => a.runId === r.id)?.n ?? 0,
452
+ })),
453
+ };
454
+ }