wizards 0.2.26 → 0.2.28

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 (108) hide show
  1. package/apps/runtime/dist/agents/project-assistant.js +10 -1
  2. package/apps/runtime/dist/authoring/guide.js +7 -3
  3. package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
  4. package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
  5. package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
  6. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  7. package/apps/runtime/dist/db/schema.js +21 -0
  8. package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
  9. package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
  10. package/apps/runtime/dist/engine/input.js +10 -1
  11. package/apps/runtime/dist/mcp/tools.js +5 -2
  12. package/apps/runtime/dist/plugins/loader.js +22 -1
  13. package/apps/runtime/dist/plugins/registry.js +10 -0
  14. package/apps/runtime/dist/routes/marketplace.js +2 -1
  15. package/apps/runtime/dist/routes/runs.js +11 -3
  16. package/apps/runtime/dist/routes/studio.js +3 -1
  17. package/apps/runtime/dist/services/marketplace.js +167 -5
  18. package/apps/runtime/dist/services/runs.js +43 -15
  19. package/apps/runtime/dist/services/wizards.js +4 -2
  20. package/apps/runtime/dist/tools/plugin.js +29 -1
  21. package/apps/web/dist/assets/Account-CVMSZTCv.js +1 -0
  22. package/apps/web/dist/assets/{ChangelogDialog-CdtQWq0m.js → ChangelogDialog-3N1CgtWP.js} +1 -1
  23. package/apps/web/dist/assets/CreditsDialog-BW-RETl-.js +1 -0
  24. package/apps/web/dist/assets/EditorPage-DnuHM3nW.js +3 -0
  25. package/apps/web/dist/assets/{FlowPreview-IZcNrBJi.js → FlowPreview-Ds6ul7-N.js} +1 -1
  26. package/apps/web/dist/assets/{Harness-Cj-k02TA.js → Harness-D-QoXcSe.js} +2 -2
  27. package/apps/web/dist/assets/HomePage-Ch0HlwVX.js +1 -0
  28. package/apps/web/dist/assets/ImportWizard-DVcYcB6l.js +1 -0
  29. package/apps/web/dist/assets/LocalRuntime-D7bzT40S.js +1 -0
  30. package/apps/web/dist/assets/NewWizardPage-DvA6AX7D.js +2 -0
  31. package/apps/web/dist/assets/ProjectPage-BWAISSfX.js +2 -0
  32. package/apps/web/dist/assets/PublicRunner-D2bd95TR.js +1 -0
  33. package/apps/web/dist/assets/RunDrawer-DkOCgXAR.js +4 -0
  34. package/apps/web/dist/assets/RunnerView-DF5uW_95.js +2 -0
  35. package/apps/web/dist/assets/SettingsPage-CMTGFXit.js +1 -0
  36. package/apps/web/dist/assets/SetupPage-BaT-VqIc.js +1 -0
  37. package/apps/web/dist/assets/{SharePage-70niRKaO.js → SharePage-COKOsuvD.js} +1 -1
  38. package/apps/web/dist/assets/arrow-right-hy38IBRU.js +1 -0
  39. package/apps/web/dist/assets/changelog-C8wFW63z.js +1 -0
  40. package/apps/web/dist/assets/circle-alert-BG9P3ewL.js +1 -0
  41. package/apps/web/dist/assets/definition-BiUSuZN5.js +1 -0
  42. package/apps/web/dist/assets/esm-0rdZWuyu.js +7 -0
  43. package/apps/web/dist/assets/external-link-BwCtn1nW.js +1 -0
  44. package/apps/web/dist/assets/image-DG4q2P1H.js +1 -0
  45. package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
  46. package/apps/web/dist/assets/index-DHM9S48l.js +78 -0
  47. package/apps/web/dist/assets/lock-DH_BntP_.js +1 -0
  48. package/apps/web/dist/assets/meta-DI0ahJ_z.js +1 -0
  49. package/apps/web/dist/assets/pen-line-B1dsy3Sr.js +1 -0
  50. package/apps/web/dist/assets/refresh-cw-B9M1g3SH.js +1 -0
  51. package/apps/web/dist/assets/settings-menu-DOjgaq6J.js +1 -0
  52. package/apps/web/dist/assets/star-axQV_3EC.js +1 -0
  53. package/apps/web/dist/assets/surface-BE-3Z1DP.js +56 -0
  54. package/apps/web/dist/assets/trash-3S1_XpVa.js +1 -0
  55. package/apps/web/dist/assets/ui-BZ01zr3i.js +9 -0
  56. package/apps/web/dist/assets/upload-U0i8fAW3.js +1 -0
  57. package/apps/web/dist/assets/video-Db4o719f.js +1 -0
  58. package/apps/web/dist/assets/volume-2-6f7qN_LM.js +1 -0
  59. package/apps/web/dist/assets/wand-sparkles-MelvBtiK.js +1 -0
  60. package/apps/web/dist/index.html +4 -5
  61. package/apps/web/dist/mcp/flow.html +57 -57
  62. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
  63. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
  64. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +28 -1
  65. package/node_modules/@engenty-wizards/shared/dist/definition.js +85 -5
  66. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +4 -1
  67. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +43 -1
  68. package/node_modules/@engenty-wizards/shared/dist/marketplace.js +27 -0
  69. package/package.json +1 -1
  70. package/apps/web/dist/assets/Account-Cnksw5Ea.js +0 -1
  71. package/apps/web/dist/assets/CreditsDialog-Ti43boCN.js +0 -1
  72. package/apps/web/dist/assets/EditorPage-N7EzlRSj.js +0 -3
  73. package/apps/web/dist/assets/HomePage-bjdKkt84.js +0 -1
  74. package/apps/web/dist/assets/ImportWizard-DagFXYPM.js +0 -1
  75. package/apps/web/dist/assets/LocalRuntime-Boi_zqQJ.js +0 -1
  76. package/apps/web/dist/assets/NewWizardPage-DO5J8OpZ.js +0 -2
  77. package/apps/web/dist/assets/ProjectPage-DdG9wYsd.js +0 -2
  78. package/apps/web/dist/assets/PublicRunner-B4ts9rT6.js +0 -1
  79. package/apps/web/dist/assets/RunDrawer-LnVfCiYO.js +0 -4
  80. package/apps/web/dist/assets/RunnerView-DwffK7AA.js +0 -70
  81. package/apps/web/dist/assets/SettingsPage-Cyx_jTlp.js +0 -1
  82. package/apps/web/dist/assets/SetupPage-CBDD4rpC.js +0 -1
  83. package/apps/web/dist/assets/api-C4M6cbpW.js +0 -10
  84. package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
  85. package/apps/web/dist/assets/changelog-COyL1B5X.js +0 -1
  86. package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
  87. package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
  88. package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
  89. package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
  90. package/apps/web/dist/assets/definition-DoCLtZZL.js +0 -55
  91. package/apps/web/dist/assets/esm-CfW8Rmtq.js +0 -7
  92. package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
  93. package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
  94. package/apps/web/dist/assets/index-CJ0YJ5HC.css +0 -1
  95. package/apps/web/dist/assets/index-DMKXnSf-.js +0 -10
  96. package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
  97. package/apps/web/dist/assets/meta-6jZygxJq.js +0 -1
  98. package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
  99. package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
  100. package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
  101. package/apps/web/dist/assets/settings-menu-Dph4yXbO.js +0 -1
  102. package/apps/web/dist/assets/star-UHmZXpjA.js +0 -1
  103. package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
  104. package/apps/web/dist/assets/ui-BS348L4M.js +0 -1
  105. package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
  106. package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
  107. package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
  108. package/apps/web/dist/assets/wand-sparkles-IEd4jkyn.js +0 -1
@@ -12,7 +12,7 @@ import { brandColorSchema, ownedProject, updateProject } from "../services/proje
12
12
  import { categoryRows } from "../services/space-categories.js";
13
13
  import { currentTenant } from "../tenants/tenant.js";
14
14
  import { safeFetch } from "../tools/net-guard.js";
15
- import { assistantToolsOf } from "../tools/plugin.js";
15
+ import { assistantPageOf, assistantToolsOf } from "../tools/plugin.js";
16
16
  import { readWebsite } from "./website.js";
17
17
  const MAX_STEPS = 24;
18
18
  const SYSTEM = `You help the admin of "engenty wizards" set up a SPACE: what every wizard of the space draws on — who they are (title, about), the brand (logos, colours), assets (images, graphics, videos), documents, and facts (label → value: address, VAT id, phone, opening hours — anything).
@@ -337,7 +337,16 @@ export async function runProjectAssistant(input) {
337
337
  model: resolved.model,
338
338
  tools,
339
339
  });
340
+ const page = input.plugin
341
+ ? await assistantPageOf(currentTenant(), input.plugin, { id: project.id, name: project.name })
342
+ : null;
340
343
  const context = [
344
+ page
345
+ ? [
346
+ `The admin talks to you from the page of "${page.name}" in the studio${page.description ? `: ${page.description}` : "."} They most likely want to change what that page holds: use its tools (their names begin with "${input.plugin?.replace(/-/g, "_")}_"). Space settings stay possible when they ask for them.`,
347
+ ...page.blocks,
348
+ ].join("\n\n")
349
+ : "",
341
350
  `The project now:\n\`\`\`json\n${JSON.stringify(await projectState(input.userId, input.projectId))}\n\`\`\``,
342
351
  input.part === "knowledge"
343
352
  ? "The admin talks to you from Wissen: they want the wizards to know something."
@@ -78,7 +78,7 @@ type PageStep = { type: "page", fields: Field[] (1–5 per page), groups?: { id,
78
78
  // rule can say it; "when" stays the first choice.
79
79
  type Field = {
80
80
  id (camelCase, unique across the WHOLE wizard), label, kind, required?, placeholder?, help?,
81
- options?: string[] // select / multiselect
81
+ options?: string[] // select / multiselect / slot
82
82
  default?: string|number|boolean|string[]
83
83
  columns?: { id, label, kind: "text"|"number"|"money" }[] // items only; row amount = product of number/money columns
84
84
  vat?: { rate?: number, field?: fieldId } // items only; VAT % fixed or read from a field
@@ -99,11 +99,11 @@ type Field = {
99
99
  // the person types) or anything known before the page (an earlier answer, steps.<id>.<field>).
100
100
  // A hidden field is not required and not kept; its {{template}} is empty.
101
101
  optionsFrom?: "steps.<id>.<key>" | "steps.<id>.<key>.<column>" | "lists.<id>.<column>"
102
- // select / multiselect: the choices come from earlier data — a "list" output, one column of a
102
+ // select / multiselect / slot: the choices come from earlier data — a "list" output, one column of a
103
103
  // "table" output, or a column of a stored list. "options" stand in while that data is empty.
104
104
  }
105
105
  // kinds: text textarea number select multiselect date email url toggle color image file items connection list
106
- // location audio signature
106
+ // location audio signature slot
107
107
  // location: where the person is — the phone's position on a tap (with its accuracy), or an address they type.
108
108
  // {{field}} → "Herrengasse 16, 8010 Graz (47.07071, 15.43950, ±12 m)" · {{field.lat}} {{field.lng}}
109
109
  // {{field.accuracy}} (metres) · {{field.label}} (the address line) · {{field.map}} (a map link).
@@ -112,6 +112,10 @@ type Field = {
112
112
  // {{field}} is what the person said, as text. {{field.seconds}} is its length.
113
113
  // signature: drawn with the finger — a picture (dark ink, clear ground). A document step can place it:
114
114
  // {{field}} → asset://ID, used as <img src="asset://ID">.
115
+ // slot: the person picks one appointment time, shown by day. Options are ISO 8601 date-times with their offset
116
+ // ("2026-10-12T09:00:00+02:00"), nearly always from optionsFrom; {{field}} is the chosen one, as offered.
117
+ // Booking: agent step "free" with tools ["appointments.availability"] outputs a table "slots" with a "start" column;
118
+ // then a page { kind: "slot", optionsFrom: "steps.free.slots.start" }; then an agent step with appointments.book.
115
119
 
116
120
  type AgentStep = {
117
121
  type: "agent"
@@ -0,0 +1,248 @@
1
+ import { and, desc, eq } from "drizzle-orm";
2
+ import { nanoid } from "nanoid";
3
+ import { db, schema } from "../db/client.js";
4
+ import { buildAuthorizationUrl, createOAuth2Pkce, exchangeAuthorizationCode, refreshAccessToken, } from "../engenty/connections-sdk/oauth2.js";
5
+ import { seal, unseal } from "../secrets/crypto.js";
6
+ import { currentTenant } from "../tenants/tenant.js";
7
+ import { builtinConnector, connectorFetch, resolveEnv, usable, viaAccount } from "./builtin.js";
8
+ import { connectRedirectUri as redirectUri } from "./index.js";
9
+ import { AccountConnectError, redeemTicket, refreshThroughAccount, startThroughAccount, } from "./via-account.js";
10
+ function oauthConnector(id) {
11
+ const connector = builtinConnector(id);
12
+ if (connector?.auth.kind !== "oauth2") {
13
+ throw new Error(`"${id}" is not a built-in connector that signs in.`);
14
+ }
15
+ return connector;
16
+ }
17
+ /** The provider rights the named actions need, and the connector's own base rights. */
18
+ function scopesOf(connector, actions) {
19
+ const scopes = new Set(connector.auth.kind === "oauth2" ? connector.auth.oauth2.baseScopes : []);
20
+ for (const id of actions) {
21
+ const action = connector.actions.find((a) => a.id === id);
22
+ if (!action) {
23
+ throw new Error(`${connector.name} has no action "${id}".`);
24
+ }
25
+ for (const scope of action.providerScopes ?? []) {
26
+ scopes.add(scope);
27
+ }
28
+ }
29
+ return [...scopes];
30
+ }
31
+ const view = (row) => ({
32
+ id: row.id,
33
+ space: row.projectId,
34
+ connector: row.connector,
35
+ label: row.label,
36
+ actions: row.actions,
37
+ createdAt: row.createdAt.toISOString(),
38
+ });
39
+ /** A state the callback can tell is a plugin's: the tenant to finish it in, or null. */
40
+ export function pluginStateTenant(rawState) {
41
+ const state = unseal(rawState);
42
+ return state && typeof state.plugin === "string" ? state.tenantId : null;
43
+ }
44
+ /**
45
+ * The provider came back for a plugin's sign-in: keeps the account. A second sign-in to the
46
+ * same account for the same space replaces the first one's tokens.
47
+ */
48
+ export async function finishPluginOAuth(answer, rawState) {
49
+ const state = unseal(rawState);
50
+ if (!state?.plugin || state.exp < Date.now()) {
51
+ throw new Error("Die Anmeldung ist abgelaufen. Bitte noch einmal verbinden.");
52
+ }
53
+ const connector = oauthConnector(state.connectorId);
54
+ if (connector.auth.kind !== "oauth2") {
55
+ throw new Error("not an oauth connector");
56
+ }
57
+ const tokens = "ticket" in answer
58
+ ? await redeemTicket(answer.ticket)
59
+ : await exchangeAuthorizationCode({
60
+ code: answer.code,
61
+ codeVerifier: state.verifier,
62
+ config: connector.auth.oauth2,
63
+ redirectUri: redirectUri(),
64
+ resolveEnv,
65
+ });
66
+ const account = (await connector.auth.oauth2
67
+ .resolveAccount?.(tokens.accessToken, fetch)
68
+ .catch(() => null)) ?? { label: connector.name };
69
+ const stored = {
70
+ accessToken: tokens.accessToken,
71
+ refreshToken: tokens.refreshToken,
72
+ expiresAt: tokens.expiresAt?.toISOString() ?? null,
73
+ scopes: tokens.grantedScopes,
74
+ ...("ticket" in answer ? { via: "account" } : {}),
75
+ };
76
+ const same = await db.query.pluginConnection.findFirst({
77
+ where: and(eq(schema.pluginConnection.plugin, state.plugin), eq(schema.pluginConnection.projectId, state.projectId), eq(schema.pluginConnection.connector, connector.id), eq(schema.pluginConnection.label, account.label)),
78
+ });
79
+ if (same) {
80
+ await db
81
+ .update(schema.pluginConnection)
82
+ .set({ actions: state.actions, data: seal(stored), updatedAt: new Date() })
83
+ .where(eq(schema.pluginConnection.id, same.id));
84
+ }
85
+ else {
86
+ await db.insert(schema.pluginConnection).values({
87
+ id: nanoid(14),
88
+ plugin: state.plugin,
89
+ projectId: state.projectId,
90
+ connector: connector.id,
91
+ label: account.label,
92
+ actions: state.actions,
93
+ data: seal(stored),
94
+ });
95
+ }
96
+ return { plugin: state.plugin, projectId: state.projectId };
97
+ }
98
+ /** The tokens of a connection, refreshed when they run out within five minutes. */
99
+ async function freshTokens(row, connector) {
100
+ let data = unseal(row.data);
101
+ if (!data) {
102
+ throw new Error("The connection's keys cannot be read here. Connect the account again.");
103
+ }
104
+ const expires = data.expiresAt ? Date.parse(data.expiresAt) : null;
105
+ if (connector.auth.kind === "oauth2" &&
106
+ data.refreshToken &&
107
+ expires !== null &&
108
+ expires - Date.now() < 5 * 60_000) {
109
+ const tokens = data.via === "account"
110
+ ? await refreshThroughAccount(data.refreshToken)
111
+ : await refreshAccessToken({
112
+ config: connector.auth.oauth2,
113
+ refreshToken: data.refreshToken,
114
+ resolveEnv,
115
+ });
116
+ data = {
117
+ ...(data.via ? { via: data.via } : {}),
118
+ accessToken: tokens.accessToken,
119
+ refreshToken: tokens.refreshToken ?? data.refreshToken,
120
+ expiresAt: tokens.expiresAt?.toISOString() ?? null,
121
+ scopes: tokens.grantedScopes.length ? tokens.grantedScopes : data.scopes,
122
+ };
123
+ await db
124
+ .update(schema.pluginConnection)
125
+ .set({ data: seal(data), updatedAt: new Date() })
126
+ .where(eq(schema.pluginConnection.id, row.id));
127
+ }
128
+ return data;
129
+ }
130
+ function contextOf(row, connector, data) {
131
+ const ctx = {
132
+ accessToken: data.accessToken,
133
+ fetchImpl: connectorFetch,
134
+ log: () => undefined,
135
+ connection: {
136
+ id: row.id,
137
+ connector_id: connector.id,
138
+ auth_kind: connector.auth.kind,
139
+ autonomous_mode: "read_only",
140
+ connected_by: row.createdBy,
141
+ created_at: row.createdAt.toISOString(),
142
+ display_name: row.label,
143
+ error_message: null,
144
+ external_account: row.label,
145
+ granted_scopes: data.scopes,
146
+ owner_user_id: null,
147
+ space_id: row.projectId,
148
+ status: "active",
149
+ tenant_id: currentTenant(),
150
+ },
151
+ };
152
+ return ctx;
153
+ }
154
+ /** `server.connections` of one plugin; every call runs inside the current tenant. */
155
+ export function pluginConnections(plugin) {
156
+ const own = (id) => db.query.pluginConnection.findFirst({
157
+ where: and(eq(schema.pluginConnection.id, id), eq(schema.pluginConnection.plugin, plugin)),
158
+ });
159
+ return {
160
+ async available(ids) {
161
+ const out = [];
162
+ for (const id of ids) {
163
+ const connector = builtinConnector(id);
164
+ if (connector?.auth.kind === "oauth2" && (await usable(connector))) {
165
+ out.push({ id, name: connector.name });
166
+ }
167
+ }
168
+ return out;
169
+ },
170
+ async start({ space, connector: connectorId, actions }) {
171
+ const connector = oauthConnector(connectorId);
172
+ if (connector.auth.kind !== "oauth2") {
173
+ throw new Error("not an oauth connector");
174
+ }
175
+ const project = await db.query.project.findFirst({
176
+ where: eq(schema.project.id, space),
177
+ columns: { id: true },
178
+ });
179
+ if (!project) {
180
+ throw new Error(`There is no space "${space}".`);
181
+ }
182
+ const scopes = scopesOf(connector, actions);
183
+ const base = {
184
+ plugin,
185
+ tenantId: currentTenant(),
186
+ projectId: space,
187
+ connectorId,
188
+ actions: [...new Set(actions)],
189
+ exp: Date.now() + 15 * 60_000,
190
+ };
191
+ if (await viaAccount(connector)) {
192
+ // The Manage-App keeps the PKCE verifier; ours stays empty.
193
+ try {
194
+ const url = await startThroughAccount(scopes, seal({ ...base, verifier: "" }), redirectUri());
195
+ return { url };
196
+ }
197
+ catch (err) {
198
+ throw err instanceof AccountConnectError
199
+ ? Object.assign(new Error(err.message), { status: 409, code: "account" })
200
+ : err;
201
+ }
202
+ }
203
+ await connector.auth.oauth2.registerClient?.();
204
+ const pkce = createOAuth2Pkce();
205
+ const url = await buildAuthorizationUrl({
206
+ connector,
207
+ pkce,
208
+ redirectUri: redirectUri(),
209
+ scopes,
210
+ state: seal({ ...base, verifier: pkce.codeVerifier }),
211
+ resolveEnv,
212
+ });
213
+ return { url };
214
+ },
215
+ async list(space) {
216
+ const rows = await db.query.pluginConnection.findMany({
217
+ where: and(eq(schema.pluginConnection.plugin, plugin), eq(schema.pluginConnection.projectId, space)),
218
+ orderBy: [desc(schema.pluginConnection.createdAt)],
219
+ });
220
+ return rows.map(view);
221
+ },
222
+ async get(id) {
223
+ const row = await own(id);
224
+ return row ? view(row) : null;
225
+ },
226
+ async call(id, actionId, input) {
227
+ const row = await own(id);
228
+ if (!row) {
229
+ throw new Error(`There is no connection "${id}".`);
230
+ }
231
+ if (!row.actions.includes(actionId)) {
232
+ throw new Error(`The connection was not made for "${actionId}". Connect it again.`);
233
+ }
234
+ const connector = oauthConnector(row.connector);
235
+ const action = connector.actions.find((a) => a.id === actionId);
236
+ if (!action) {
237
+ throw new Error(`${connector.name} has no action "${actionId}".`);
238
+ }
239
+ const data = await freshTokens(row, connector);
240
+ return (await action.handler(action.inputSchema.parse(input), contextOf(row, connector, data)));
241
+ },
242
+ async remove(id) {
243
+ await db
244
+ .delete(schema.pluginConnection)
245
+ .where(and(eq(schema.pluginConnection.id, id), eq(schema.pluginConnection.plugin, plugin)));
246
+ },
247
+ };
248
+ }
@@ -0,0 +1,15 @@
1
+ CREATE TABLE `plugin_connection` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `plugin` text NOT NULL,
4
+ `project_id` text NOT NULL,
5
+ `connector` text NOT NULL,
6
+ `label` text NOT NULL,
7
+ `actions` text NOT NULL,
8
+ `data` text NOT NULL,
9
+ `created_by` text,
10
+ `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
11
+ `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
12
+ FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade
13
+ );
14
+ --> statement-breakpoint
15
+ CREATE INDEX `plugin_connection_space` ON `plugin_connection` (`plugin`,`project_id`);