wizards 0.1.9
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/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { validateWizard, wizardSchema, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { applyOps, OpError } from "../authoring/ops.js";
|
|
5
|
+
import { listConnectors } from "../connectors/external.js";
|
|
6
|
+
import { db, schema } from "../db/client.js";
|
|
7
|
+
import { env } from "../env.js";
|
|
8
|
+
import { dropLinks, putLink } from "../tenants/control.js";
|
|
9
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
10
|
+
import { changedSteps, emitDraftChanged } from "./draft-events.js";
|
|
11
|
+
import { notFound, ServiceError } from "./errors.js";
|
|
12
|
+
import { copyFiles, draftFiles, sameFiles, seedFiles } from "./files.js";
|
|
13
|
+
import { forgetWizardLinks } from "./links.js";
|
|
14
|
+
import { asLang, countInstall, marketplaceWizard } from "./marketplace.js";
|
|
15
|
+
import { defaultProject, ownedProject } from "./projects.js";
|
|
16
|
+
const BLANK = {
|
|
17
|
+
version: 1,
|
|
18
|
+
title: "Neuer Wizard",
|
|
19
|
+
description: "",
|
|
20
|
+
avatar: "round",
|
|
21
|
+
steps: [
|
|
22
|
+
{
|
|
23
|
+
id: "start",
|
|
24
|
+
type: "page",
|
|
25
|
+
title: "Los geht's",
|
|
26
|
+
fields: [{ id: "input", label: "Worum geht es?", kind: "textarea", required: true }],
|
|
27
|
+
},
|
|
28
|
+
{ id: "done", type: "result", title: "Fertig", deliverables: [] },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
export const studioUrl = (wizardId) => `${env.appUrl}/studio/edit/${wizardId}`;
|
|
32
|
+
export const shareUrl = (token) => `${env.appUrl}/w/${token}`;
|
|
33
|
+
const newShareToken = () => nanoid(14);
|
|
34
|
+
export async function ownedWizard(_userId, wizardId) {
|
|
35
|
+
const w = await db.query.wizard.findFirst({
|
|
36
|
+
where: and(eq(schema.wizard.id, wizardId), eq(schema.wizard.tenantId, currentTenant())),
|
|
37
|
+
});
|
|
38
|
+
if (!w) {
|
|
39
|
+
throw notFound();
|
|
40
|
+
}
|
|
41
|
+
return w;
|
|
42
|
+
}
|
|
43
|
+
export function wizardSummary(w) {
|
|
44
|
+
return {
|
|
45
|
+
id: w.id,
|
|
46
|
+
projectId: w.projectId,
|
|
47
|
+
title: w.title,
|
|
48
|
+
description: w.draft.description,
|
|
49
|
+
avatar: w.draft.avatar,
|
|
50
|
+
revision: w.revision,
|
|
51
|
+
published: w.publishedVersion !== null,
|
|
52
|
+
publishedVersion: w.publishedVersion,
|
|
53
|
+
shareToken: w.shareToken,
|
|
54
|
+
shareEnabled: w.shareEnabled,
|
|
55
|
+
dailyRunLimit: w.dailyRunLimit,
|
|
56
|
+
stepCount: w.draft.steps.length,
|
|
57
|
+
updatedAt: w.updatedAt.toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export async function listWizards(_userId, projectId) {
|
|
61
|
+
const rows = await db.query.wizard.findMany({
|
|
62
|
+
where: and(eq(schema.wizard.tenantId, currentTenant()), projectId ? eq(schema.wizard.projectId, projectId) : undefined),
|
|
63
|
+
orderBy: [desc(schema.wizard.updatedAt)],
|
|
64
|
+
});
|
|
65
|
+
return rows.map(wizardSummary);
|
|
66
|
+
}
|
|
67
|
+
async function publishedVersion(w) {
|
|
68
|
+
if (w.publishedVersion === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const v = await db.query.wizardVersion.findFirst({
|
|
72
|
+
where: and(eq(schema.wizardVersion.wizardId, w.id), eq(schema.wizardVersion.version, w.publishedVersion)),
|
|
73
|
+
});
|
|
74
|
+
return v ?? null;
|
|
75
|
+
}
|
|
76
|
+
/** Connections must name connectors the project has imported, and actions those connectors have. */
|
|
77
|
+
async function connectorIssues(projectId, draft) {
|
|
78
|
+
const wanted = (draft.connections ?? []).filter((c) => c.connector);
|
|
79
|
+
if (!wanted.length) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const issues = [];
|
|
83
|
+
const connectors = await listConnectors(projectId);
|
|
84
|
+
for (const connection of wanted) {
|
|
85
|
+
const connector = connectors.find((c) => c.id === connection.connector);
|
|
86
|
+
if (!connector) {
|
|
87
|
+
issues.push({
|
|
88
|
+
message: `Connection "${connection.id}" uses connector "${connection.connector}", which this project has not imported.`,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const unknown = (connection.actions ?? []).filter((a) => !connector.actions.some((x) => x.id === a));
|
|
93
|
+
if (unknown.length && !connector.toolsPending) {
|
|
94
|
+
issues.push({
|
|
95
|
+
message: `Connection "${connection.id}": ${connector.name} has no action ${unknown.map((a) => `"${a}"`).join(", ")}.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return issues;
|
|
100
|
+
}
|
|
101
|
+
/** Everything wrong with a draft: its structure, its workspace files, its project's connectors. */
|
|
102
|
+
async function checkDraft(projectId, draft, files) {
|
|
103
|
+
return [...validateWizard(draft, files), ...(await connectorIssues(projectId, draft))];
|
|
104
|
+
}
|
|
105
|
+
/** Validator issues of the draft, checked against its workspace and the project's connectors. */
|
|
106
|
+
export async function draftIssues(w) {
|
|
107
|
+
const files = await draftFiles(w.id);
|
|
108
|
+
return checkDraft(w.projectId, w.draft, files.map((f) => f.path));
|
|
109
|
+
}
|
|
110
|
+
/** The draft with its validator issues and whether it differs from the published version. */
|
|
111
|
+
export async function wizardState(w) {
|
|
112
|
+
const published = await publishedVersion(w);
|
|
113
|
+
const files = await draftFiles(w.id);
|
|
114
|
+
return {
|
|
115
|
+
...wizardSummary(w),
|
|
116
|
+
draft: w.draft,
|
|
117
|
+
files,
|
|
118
|
+
issues: await checkDraft(w.projectId, w.draft, files.map((f) => f.path)),
|
|
119
|
+
dirty: !published ||
|
|
120
|
+
JSON.stringify(published.definition) !== JSON.stringify(w.draft) ||
|
|
121
|
+
!sameFiles(published.files, files),
|
|
122
|
+
shareUrl: shareUrl(w.shareToken),
|
|
123
|
+
studioUrl: studioUrl(w.id),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function wizardMessages(wizardId) {
|
|
127
|
+
return db.query.wizardMessage.findMany({
|
|
128
|
+
where: eq(schema.wizardMessage.wizardId, wizardId),
|
|
129
|
+
orderBy: [schema.wizardMessage.createdAt],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export async function addMessage(wizardId, message, writer = { source: "studio" }) {
|
|
133
|
+
const id = nanoid(12);
|
|
134
|
+
await db.insert(schema.wizardMessage).values({
|
|
135
|
+
id,
|
|
136
|
+
wizardId,
|
|
137
|
+
role: message.role,
|
|
138
|
+
content: message.content,
|
|
139
|
+
changed: message.changed ?? false,
|
|
140
|
+
source: writer.source,
|
|
141
|
+
client: writer.source === "mcp" ? writer.client : null,
|
|
142
|
+
});
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
145
|
+
function stepLabel(raw, index) {
|
|
146
|
+
const steps = raw?.steps;
|
|
147
|
+
const id = steps?.[index]?.id;
|
|
148
|
+
return typeof id === "string" ? `steps[${index}] "${id}"` : `steps[${index}]`;
|
|
149
|
+
}
|
|
150
|
+
/** The schema check every write passes. Validator issues are allowed in a draft; a wrong shape is not. */
|
|
151
|
+
export function parseDraft(raw, files) {
|
|
152
|
+
const parsed = wizardSchema.safeParse(raw);
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
throw new ServiceError("invalid", "The wizard does not match the schema.", {
|
|
155
|
+
issues: shapeIssues(parsed.error, raw),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return { draft: parsed.data, issues: validateWizard(parsed.data, files) };
|
|
159
|
+
}
|
|
160
|
+
export function shapeIssues(error, raw) {
|
|
161
|
+
return error.issues.map((i) => {
|
|
162
|
+
const [head, index, ...rest] = i.path;
|
|
163
|
+
const where = head === "steps" && typeof index === "number"
|
|
164
|
+
? [stepLabel(raw, index), ...rest].join(".")
|
|
165
|
+
: i.path.join(".");
|
|
166
|
+
return { message: `${where || "wizard"}: ${i.message}` };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export async function createWizard(userId, input, writer = { source: "studio" }) {
|
|
170
|
+
const project = input.projectId
|
|
171
|
+
? await ownedProject(userId, input.projectId)
|
|
172
|
+
: await defaultProject(userId);
|
|
173
|
+
const starter = input.starterId
|
|
174
|
+
? await marketplaceWizard(input.starterId, asLang(input.lang))
|
|
175
|
+
: undefined;
|
|
176
|
+
if (input.starterId && !starter) {
|
|
177
|
+
throw new ServiceError("invalid", `There is no starter "${input.starterId}".`);
|
|
178
|
+
}
|
|
179
|
+
const { draft, issues } = input.definition !== undefined
|
|
180
|
+
? parseDraft(input.definition)
|
|
181
|
+
: { draft: starter?.definition ?? structuredClone(BLANK), issues: [] };
|
|
182
|
+
const fromStarter = input.definition === undefined ? starter : undefined;
|
|
183
|
+
const id = nanoid(12);
|
|
184
|
+
const shareToken = newShareToken();
|
|
185
|
+
await db.insert(schema.wizard).values({
|
|
186
|
+
id,
|
|
187
|
+
projectId: project.id,
|
|
188
|
+
tenantId: currentTenant(),
|
|
189
|
+
title: draft.title,
|
|
190
|
+
draft,
|
|
191
|
+
shareToken,
|
|
192
|
+
dailyRunLimit: env.limits.defaultDailyRuns,
|
|
193
|
+
starter: fromStarter?.id ?? null,
|
|
194
|
+
starterRevision: fromStarter?.revision ?? null,
|
|
195
|
+
});
|
|
196
|
+
await putLink(shareToken, "wizard", id);
|
|
197
|
+
if (fromStarter) {
|
|
198
|
+
await seedFiles(id, fromStarter.files);
|
|
199
|
+
// Counted at the marketplace, beside the request.
|
|
200
|
+
void countInstall(fromStarter.id);
|
|
201
|
+
}
|
|
202
|
+
if (input.note) {
|
|
203
|
+
await addMessage(id, { role: "assistant", content: input.note, changed: true }, writer);
|
|
204
|
+
}
|
|
205
|
+
else if (fromStarter) {
|
|
206
|
+
await addMessage(id, {
|
|
207
|
+
role: "assistant",
|
|
208
|
+
content: fromStarter.language === "en"
|
|
209
|
+
? `I have set up the template "${fromStarter.title}" for you. Test it at the top right – or tell me what should be different.`
|
|
210
|
+
: `Ich habe die Vorlage „${fromStarter.title}“ für dich angelegt. Teste sie rechts oben – oder sag mir, was anders sein soll.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return { id, revision: 0, issues, studioUrl: studioUrl(id) };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Replaces the draft if nobody wrote since `baseRevision`. Every draft write — studio inspector,
|
|
217
|
+
* architect, MCP — goes through here, so the revision counts all of them.
|
|
218
|
+
*/
|
|
219
|
+
export async function writeDraft(userId, wizardId, input, writer = { source: "studio" }) {
|
|
220
|
+
const w = await ownedWizard(userId, wizardId);
|
|
221
|
+
const files = await draftFiles(w.id);
|
|
222
|
+
const paths = files.map((f) => f.path);
|
|
223
|
+
const { draft } = parseDraft(input.definition, paths);
|
|
224
|
+
const issues = await checkDraft(w.projectId, draft, paths);
|
|
225
|
+
const [row] = await db
|
|
226
|
+
.update(schema.wizard)
|
|
227
|
+
.set({
|
|
228
|
+
draft,
|
|
229
|
+
title: draft.title,
|
|
230
|
+
revision: sql `${schema.wizard.revision} + 1`,
|
|
231
|
+
updatedAt: new Date(),
|
|
232
|
+
})
|
|
233
|
+
.where(and(eq(schema.wizard.id, w.id), eq(schema.wizard.revision, input.baseRevision)))
|
|
234
|
+
.returning({ revision: schema.wizard.revision });
|
|
235
|
+
if (!row) {
|
|
236
|
+
const current = await ownedWizard(userId, wizardId);
|
|
237
|
+
throw new ServiceError("revision_conflict", "Der Wizard wurde inzwischen geändert.", {
|
|
238
|
+
revision: current.revision,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (input.note) {
|
|
242
|
+
await addMessage(w.id, { role: "assistant", content: input.note, changed: true }, writer);
|
|
243
|
+
}
|
|
244
|
+
emitDraftChanged({
|
|
245
|
+
wizardId: w.id,
|
|
246
|
+
kind: "draft",
|
|
247
|
+
revision: row.revision,
|
|
248
|
+
source: writer.source,
|
|
249
|
+
client: writer.source === "mcp" ? writer.client : null,
|
|
250
|
+
note: input.note ?? null,
|
|
251
|
+
touched: changedSteps(w.draft, draft),
|
|
252
|
+
});
|
|
253
|
+
return { revision: row.revision, issues, draft };
|
|
254
|
+
}
|
|
255
|
+
export async function editWizard(userId, wizardId, input, writer) {
|
|
256
|
+
const w = await ownedWizard(userId, wizardId);
|
|
257
|
+
if (w.revision !== input.baseRevision) {
|
|
258
|
+
throw new ServiceError("revision_conflict", "Der Wizard wurde inzwischen geändert.", {
|
|
259
|
+
revision: w.revision,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
let definition;
|
|
263
|
+
try {
|
|
264
|
+
definition = applyOps(w.draft, input.ops);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
if (err instanceof OpError) {
|
|
268
|
+
throw new ServiceError("invalid", err.message);
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
return writeDraft(userId, wizardId, { definition, baseRevision: input.baseRevision, note: input.note }, writer);
|
|
273
|
+
}
|
|
274
|
+
export async function updateWizardSettings(userId, wizardId, patch) {
|
|
275
|
+
const w = await ownedWizard(userId, wizardId);
|
|
276
|
+
if (patch.projectId) {
|
|
277
|
+
await ownedProject(userId, patch.projectId);
|
|
278
|
+
}
|
|
279
|
+
await db
|
|
280
|
+
.update(schema.wizard)
|
|
281
|
+
.set({ ...patch, updatedAt: new Date() })
|
|
282
|
+
.where(eq(schema.wizard.id, w.id));
|
|
283
|
+
return wizardSummary(await ownedWizard(userId, wizardId));
|
|
284
|
+
}
|
|
285
|
+
export async function rotateShareLink(userId, wizardId) {
|
|
286
|
+
const w = await ownedWizard(userId, wizardId);
|
|
287
|
+
const token = newShareToken();
|
|
288
|
+
await db
|
|
289
|
+
.update(schema.wizard)
|
|
290
|
+
.set({ shareToken: token, updatedAt: new Date() })
|
|
291
|
+
.where(eq(schema.wizard.id, w.id));
|
|
292
|
+
await dropLinks([w.shareToken]);
|
|
293
|
+
await putLink(token, "wizard", w.id);
|
|
294
|
+
return { shareToken: token, shareUrl: shareUrl(token) };
|
|
295
|
+
}
|
|
296
|
+
export async function duplicateWizard(userId, wizardId) {
|
|
297
|
+
const w = await ownedWizard(userId, wizardId);
|
|
298
|
+
const id = nanoid(12);
|
|
299
|
+
const draft = { ...w.draft, title: `${w.draft.title} (Kopie)` };
|
|
300
|
+
const shareToken = newShareToken();
|
|
301
|
+
await db.insert(schema.wizard).values({
|
|
302
|
+
id,
|
|
303
|
+
projectId: w.projectId,
|
|
304
|
+
tenantId: currentTenant(),
|
|
305
|
+
title: draft.title,
|
|
306
|
+
draft,
|
|
307
|
+
shareToken,
|
|
308
|
+
dailyRunLimit: w.dailyRunLimit,
|
|
309
|
+
starter: w.starter,
|
|
310
|
+
});
|
|
311
|
+
await putLink(shareToken, "wizard", id);
|
|
312
|
+
await copyFiles(w.id, id);
|
|
313
|
+
return { id };
|
|
314
|
+
}
|
|
315
|
+
export async function deleteWizard(_userId, wizardId) {
|
|
316
|
+
await forgetWizardLinks(wizardId);
|
|
317
|
+
await db
|
|
318
|
+
.delete(schema.wizard)
|
|
319
|
+
.where(and(eq(schema.wizard.id, wizardId), eq(schema.wizard.tenantId, currentTenant())));
|
|
320
|
+
}
|
|
321
|
+
/** A wizard with validator issues is never published — same rule for studio and MCP. */
|
|
322
|
+
export async function requireClean(w) {
|
|
323
|
+
const files = await draftFiles(w.id);
|
|
324
|
+
const issues = await checkDraft(w.projectId, w.draft, files.map((f) => f.path));
|
|
325
|
+
if (issues.length) {
|
|
326
|
+
throw new ServiceError("has_issues", "Der Wizard hat noch Fehler.", { issues });
|
|
327
|
+
}
|
|
328
|
+
return { definition: w.draft, files };
|
|
329
|
+
}
|
|
330
|
+
export async function publishWizard(userId, wizardId) {
|
|
331
|
+
const w = await ownedWizard(userId, wizardId);
|
|
332
|
+
const { definition, files } = await requireClean(w);
|
|
333
|
+
const version = (w.publishedVersion ?? 0) + 1;
|
|
334
|
+
await db
|
|
335
|
+
.insert(schema.wizardVersion)
|
|
336
|
+
.values({ id: nanoid(12), wizardId: w.id, version, definition, files });
|
|
337
|
+
await db
|
|
338
|
+
.update(schema.wizard)
|
|
339
|
+
.set({ publishedVersion: version, updatedAt: new Date() })
|
|
340
|
+
.where(eq(schema.wizard.id, w.id));
|
|
341
|
+
return { version, shareUrl: shareUrl(w.shareToken), shareEnabled: w.shareEnabled };
|
|
342
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { control, controlDb } from "./db/client.js";
|
|
3
|
+
/** Settings of a runtime that runs alone, kept in the control database. Never secrets. */
|
|
4
|
+
export async function readSetting(key) {
|
|
5
|
+
const row = await controlDb.query.setting.findFirst({ where: eq(control.setting.key, key) });
|
|
6
|
+
return row?.value ?? null;
|
|
7
|
+
}
|
|
8
|
+
export async function writeSetting(key, value) {
|
|
9
|
+
await controlDb
|
|
10
|
+
.insert(control.setting)
|
|
11
|
+
.values({ key, value })
|
|
12
|
+
.onConflictDoUpdate({ target: control.setting.key, set: { value } });
|
|
13
|
+
}
|
|
14
|
+
export async function deleteSetting(key) {
|
|
15
|
+
await controlDb.delete(control.setting).where(eq(control.setting.key, key));
|
|
16
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { newCells, patchCells, rowKey, STORE_LIMITS, } from "@engenty-wizards/shared/store";
|
|
2
|
+
import { cleanPath, mimeForPath } from "@engenty-wizards/shared/workspace";
|
|
3
|
+
import { and, asc, eq, inArray, like, lt, sql } from "drizzle-orm";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import { db, schema } from "../db/client.js";
|
|
6
|
+
import { env } from "../env.js";
|
|
7
|
+
import { getBlob, putBlob } from "../files/blobs.js";
|
|
8
|
+
import { seal, unseal } from "../secrets/crypto.js";
|
|
9
|
+
export class StoreError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/** The person a run belongs to: the signed-in admin, else the visitor of the shared link. */
|
|
12
|
+
export function holderOf(run) {
|
|
13
|
+
if (run.userId) {
|
|
14
|
+
return `u:${run.userId}`;
|
|
15
|
+
}
|
|
16
|
+
return run.visitorId ? `v:${run.visitorId}` : `r:${run.id}`;
|
|
17
|
+
}
|
|
18
|
+
export function scopeOf(run) {
|
|
19
|
+
return { wizardId: run.wizardId, holder: holderOf(run) };
|
|
20
|
+
}
|
|
21
|
+
/** Marks the store as in use; stores nobody came back to are cleaned up. */
|
|
22
|
+
export async function touchStore(scope) {
|
|
23
|
+
await db
|
|
24
|
+
.insert(schema.storeHolder)
|
|
25
|
+
.values({ ...scope, usedAt: new Date() })
|
|
26
|
+
.onConflictDoUpdate({
|
|
27
|
+
target: [schema.storeHolder.wizardId, schema.storeHolder.holder],
|
|
28
|
+
set: { usedAt: new Date() },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// --- lists -------------------------------------------------------------------
|
|
32
|
+
const inList = (scope, list) => and(eq(schema.storeRow.wizardId, scope.wizardId), eq(schema.storeRow.holder, scope.holder), eq(schema.storeRow.list, list));
|
|
33
|
+
function toRow(r) {
|
|
34
|
+
return { id: r.id, cells: r.cells, updatedAt: r.updatedAt.toISOString() };
|
|
35
|
+
}
|
|
36
|
+
export async function listRows(scope, list) {
|
|
37
|
+
const rows = await db.query.storeRow.findMany({
|
|
38
|
+
where: inList(scope, list),
|
|
39
|
+
orderBy: [asc(schema.storeRow.createdAt), asc(schema.storeRow.id)],
|
|
40
|
+
});
|
|
41
|
+
return rows.map(toRow);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Saves rows into a list. With a key column, a row whose key is already there is updated — only
|
|
45
|
+
* the columns given change, so a step can add "how to get the invoice" without wiping the rest.
|
|
46
|
+
* A row with a bad value is refused as a whole (`TableColumnValueError` names the column).
|
|
47
|
+
*/
|
|
48
|
+
export async function saveRows(scope, def, input) {
|
|
49
|
+
let added = 0;
|
|
50
|
+
let updated = 0;
|
|
51
|
+
const existing = await db.query.storeRow.findMany({ where: inList(scope, def.id) });
|
|
52
|
+
const byKey = new Map(existing.filter((r) => r.key).map((r) => [r.key, r]));
|
|
53
|
+
let count = existing.length;
|
|
54
|
+
for (const raw of input) {
|
|
55
|
+
const key = rowKey(def, raw);
|
|
56
|
+
const hit = key ? byKey.get(key) : undefined;
|
|
57
|
+
if (hit) {
|
|
58
|
+
const cells = patchCells(def, hit.cells, raw);
|
|
59
|
+
await db
|
|
60
|
+
.update(schema.storeRow)
|
|
61
|
+
.set({ cells, updatedAt: new Date() })
|
|
62
|
+
.where(eq(schema.storeRow.id, hit.id));
|
|
63
|
+
hit.cells = cells;
|
|
64
|
+
updated++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const cells = newCells(def, raw);
|
|
68
|
+
if (Object.values(cells).every((v) => v === null)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (count >= STORE_LIMITS.rowsPerList) {
|
|
72
|
+
throw new StoreError(`List "${def.id}" is full (${STORE_LIMITS.rowsPerList} rows).`);
|
|
73
|
+
}
|
|
74
|
+
const row = { id: nanoid(14), ...scope, list: def.id, key, cells };
|
|
75
|
+
await db.insert(schema.storeRow).values(row);
|
|
76
|
+
if (key) {
|
|
77
|
+
byKey.set(key, { ...row, createdAt: new Date(), updatedAt: new Date() });
|
|
78
|
+
}
|
|
79
|
+
count++;
|
|
80
|
+
added++;
|
|
81
|
+
}
|
|
82
|
+
await touchStore(scope);
|
|
83
|
+
return { added, updated };
|
|
84
|
+
}
|
|
85
|
+
/** Changes cells of one row (the person edited it). */
|
|
86
|
+
export async function updateRow(scope, def, rowId, patch) {
|
|
87
|
+
const row = await db.query.storeRow.findFirst({
|
|
88
|
+
where: and(inList(scope, def.id), eq(schema.storeRow.id, rowId)),
|
|
89
|
+
});
|
|
90
|
+
if (!row) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const cells = patchCells(def, row.cells, patch);
|
|
94
|
+
const key = rowKey(def, cells);
|
|
95
|
+
const clash = key && key !== row.key
|
|
96
|
+
? await db.query.storeRow.findFirst({
|
|
97
|
+
where: and(inList(scope, def.id), eq(schema.storeRow.key, key)),
|
|
98
|
+
})
|
|
99
|
+
: null;
|
|
100
|
+
if (clash) {
|
|
101
|
+
throw new StoreError("Diesen Eintrag gibt es schon.");
|
|
102
|
+
}
|
|
103
|
+
await db
|
|
104
|
+
.update(schema.storeRow)
|
|
105
|
+
.set({ cells, key, updatedAt: new Date() })
|
|
106
|
+
.where(eq(schema.storeRow.id, rowId));
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/** Deletes rows by id, or by the value of the key column. */
|
|
110
|
+
export async function deleteRows(scope, def, idsOrKeys) {
|
|
111
|
+
if (!idsOrKeys.length) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const keys = idsOrKeys.map((k) => k.trim().toLowerCase());
|
|
115
|
+
const byId = await db
|
|
116
|
+
.delete(schema.storeRow)
|
|
117
|
+
.where(and(inList(scope, def.id), inArray(schema.storeRow.id, idsOrKeys)))
|
|
118
|
+
.returning({ id: schema.storeRow.id });
|
|
119
|
+
const byKey = await db
|
|
120
|
+
.delete(schema.storeRow)
|
|
121
|
+
.where(and(inList(scope, def.id), inArray(schema.storeRow.key, keys)))
|
|
122
|
+
.returning({ id: schema.storeRow.id });
|
|
123
|
+
return byId.length + byKey.length;
|
|
124
|
+
}
|
|
125
|
+
// --- files -------------------------------------------------------------------
|
|
126
|
+
const ownFiles = (scope) => and(eq(schema.storeFile.wizardId, scope.wizardId), eq(schema.storeFile.holder, scope.holder));
|
|
127
|
+
function toFile(r) {
|
|
128
|
+
return {
|
|
129
|
+
path: r.path,
|
|
130
|
+
mime: r.mime,
|
|
131
|
+
size: r.size,
|
|
132
|
+
source: r.source,
|
|
133
|
+
updatedAt: r.updatedAt.toISOString(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Any name a model or a mail server comes up with, as a path the store accepts. */
|
|
137
|
+
export function storePath(raw) {
|
|
138
|
+
const path = raw
|
|
139
|
+
.normalize("NFKD")
|
|
140
|
+
.replace(/[̀-ͯ]/g, "")
|
|
141
|
+
.replace(/ß/g, "ss")
|
|
142
|
+
.split("/")
|
|
143
|
+
.map((part) => part
|
|
144
|
+
.trim()
|
|
145
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "_")
|
|
146
|
+
.replace(/^[._]+/, "")
|
|
147
|
+
.slice(0, 80))
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.join("/");
|
|
150
|
+
const clean = cleanPath(path);
|
|
151
|
+
if (!clean) {
|
|
152
|
+
throw new StoreError(`"${raw}" is not a usable file name.`);
|
|
153
|
+
}
|
|
154
|
+
return clean;
|
|
155
|
+
}
|
|
156
|
+
export async function storeFiles(scope, folder) {
|
|
157
|
+
const rows = await db.query.storeFile.findMany({
|
|
158
|
+
where: folder
|
|
159
|
+
? and(ownFiles(scope), like(schema.storeFile.path, `${folder.replace(/\/+$/, "")}/%`))
|
|
160
|
+
: ownFiles(scope),
|
|
161
|
+
orderBy: [asc(schema.storeFile.path)],
|
|
162
|
+
});
|
|
163
|
+
return rows.map(toFile);
|
|
164
|
+
}
|
|
165
|
+
export async function readStoreFile(scope, rawPath) {
|
|
166
|
+
const path = cleanPath(rawPath);
|
|
167
|
+
const row = path
|
|
168
|
+
? await db.query.storeFile.findFirst({
|
|
169
|
+
where: and(ownFiles(scope), eq(schema.storeFile.path, path)),
|
|
170
|
+
})
|
|
171
|
+
: undefined;
|
|
172
|
+
return row ? { file: toFile(row), data: await getBlob(row.hash) } : null;
|
|
173
|
+
}
|
|
174
|
+
export async function writeStoreFile(scope, rawPath, content, meta = {}) {
|
|
175
|
+
const path = storePath(rawPath);
|
|
176
|
+
const data = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
177
|
+
if (data.byteLength > STORE_LIMITS.fileBytes) {
|
|
178
|
+
throw new StoreError(`${path} is larger than 15 MB.`);
|
|
179
|
+
}
|
|
180
|
+
const [usage] = await db
|
|
181
|
+
.select({
|
|
182
|
+
files: sql `count(*)`,
|
|
183
|
+
bytes: sql `coalesce(sum(${schema.storeFile.size}), 0)`,
|
|
184
|
+
})
|
|
185
|
+
.from(schema.storeFile)
|
|
186
|
+
.where(ownFiles(scope));
|
|
187
|
+
if (usage.files >= STORE_LIMITS.files ||
|
|
188
|
+
usage.bytes + data.byteLength > STORE_LIMITS.totalBytes) {
|
|
189
|
+
throw new StoreError("The wizard's storage is full.");
|
|
190
|
+
}
|
|
191
|
+
const hash = await putBlob(data);
|
|
192
|
+
const mime = meta.mime && meta.mime !== "application/octet-stream" ? meta.mime : mimeForPath(path);
|
|
193
|
+
const updatedAt = new Date();
|
|
194
|
+
await db
|
|
195
|
+
.insert(schema.storeFile)
|
|
196
|
+
.values({ ...scope, path, hash, mime, size: data.byteLength, source: meta.source ?? null })
|
|
197
|
+
.onConflictDoUpdate({
|
|
198
|
+
target: [schema.storeFile.wizardId, schema.storeFile.holder, schema.storeFile.path],
|
|
199
|
+
set: { hash, mime, size: data.byteLength, source: meta.source ?? null, updatedAt },
|
|
200
|
+
});
|
|
201
|
+
await touchStore(scope);
|
|
202
|
+
return {
|
|
203
|
+
path,
|
|
204
|
+
mime,
|
|
205
|
+
size: data.byteLength,
|
|
206
|
+
source: meta.source ?? null,
|
|
207
|
+
updatedAt: updatedAt.toISOString(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export async function deleteStoreFile(scope, rawPath) {
|
|
211
|
+
const path = cleanPath(rawPath);
|
|
212
|
+
if (!path) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const gone = await db
|
|
216
|
+
.delete(schema.storeFile)
|
|
217
|
+
.where(and(ownFiles(scope), eq(schema.storeFile.path, path)))
|
|
218
|
+
.returning({ path: schema.storeFile.path });
|
|
219
|
+
return gone.length > 0;
|
|
220
|
+
}
|
|
221
|
+
// --- secrets -----------------------------------------------------------------
|
|
222
|
+
const ownSecrets = (scope) => and(eq(schema.storeSecret.wizardId, scope.wizardId), eq(schema.storeSecret.holder, scope.holder));
|
|
223
|
+
export async function getSecret(scope, slot) {
|
|
224
|
+
const row = await db.query.storeSecret.findFirst({
|
|
225
|
+
where: and(ownSecrets(scope), eq(schema.storeSecret.slot, slot)),
|
|
226
|
+
});
|
|
227
|
+
const data = row ? unseal(row.data) : null;
|
|
228
|
+
return row && data
|
|
229
|
+
? { provider: row.provider, label: row.label, data, createdAt: row.createdAt }
|
|
230
|
+
: null;
|
|
231
|
+
}
|
|
232
|
+
export async function putSecret(scope, slot, provider, label, data) {
|
|
233
|
+
const sealed = seal(data);
|
|
234
|
+
await db
|
|
235
|
+
.insert(schema.storeSecret)
|
|
236
|
+
.values({ id: nanoid(14), ...scope, slot, provider, label, data: sealed })
|
|
237
|
+
.onConflictDoUpdate({
|
|
238
|
+
target: [schema.storeSecret.wizardId, schema.storeSecret.holder, schema.storeSecret.slot],
|
|
239
|
+
set: { provider, label, data: sealed, updatedAt: new Date() },
|
|
240
|
+
});
|
|
241
|
+
await touchStore(scope);
|
|
242
|
+
}
|
|
243
|
+
export async function deleteSecret(scope, slot) {
|
|
244
|
+
await db
|
|
245
|
+
.delete(schema.storeSecret)
|
|
246
|
+
.where(and(ownSecrets(scope), eq(schema.storeSecret.slot, slot)));
|
|
247
|
+
}
|
|
248
|
+
export async function listSecrets(scope) {
|
|
249
|
+
const rows = await db.query.storeSecret.findMany({ where: ownSecrets(scope) });
|
|
250
|
+
return rows.map((r) => ({
|
|
251
|
+
slot: r.slot,
|
|
252
|
+
provider: r.provider,
|
|
253
|
+
label: r.label,
|
|
254
|
+
createdAt: r.createdAt.toISOString(),
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
// --- everything --------------------------------------------------------------
|
|
258
|
+
/** Deletes everything the wizard keeps for this person. Blobs stay: other files may share them. */
|
|
259
|
+
export async function clearStore(scope) {
|
|
260
|
+
await db
|
|
261
|
+
.delete(schema.storeRow)
|
|
262
|
+
.where(and(eq(schema.storeRow.wizardId, scope.wizardId), eq(schema.storeRow.holder, scope.holder)));
|
|
263
|
+
await db.delete(schema.storeFile).where(ownFiles(scope));
|
|
264
|
+
await db.delete(schema.storeSecret).where(ownSecrets(scope));
|
|
265
|
+
await db
|
|
266
|
+
.delete(schema.storeHolder)
|
|
267
|
+
.where(and(eq(schema.storeHolder.wizardId, scope.wizardId), eq(schema.storeHolder.holder, scope.holder)));
|
|
268
|
+
}
|
|
269
|
+
/** Stores nobody used for STORE_TTL_DAYS are deleted — a visitor's cookie is long gone by then. */
|
|
270
|
+
export async function purgeUnusedStores() {
|
|
271
|
+
const cutoff = new Date(Date.now() - env.limits.storeTtlDays * 24 * 60 * 60 * 1000);
|
|
272
|
+
const stale = await db.query.storeHolder.findMany({
|
|
273
|
+
where: lt(schema.storeHolder.usedAt, cutoff),
|
|
274
|
+
});
|
|
275
|
+
for (const scope of stale) {
|
|
276
|
+
await clearStore(scope);
|
|
277
|
+
}
|
|
278
|
+
return stale.length;
|
|
279
|
+
}
|