@revenexx/studio-integrations 1.2.0 → 1.3.0

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 (37) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +10 -2
  4. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +5 -1
  5. package/dist/runtime/components/Integrations/NodeConfigField.vue +65 -1
  6. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +5 -1
  7. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +5 -1
  8. package/dist/runtime/components/Integrations/NodeInspector.vue +103 -25
  9. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +5 -1
  10. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.d.vue.ts +21 -0
  11. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue +133 -0
  12. package/dist/runtime/components/Integrations/NodeVersionMoveDialog.vue.d.ts +21 -0
  13. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.d.vue.ts +16 -0
  14. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue +111 -0
  15. package/dist/runtime/components/Integrations/StateNamespaceCreateDialog.vue.d.ts +16 -0
  16. package/dist/runtime/components/Integrations/WorkflowEditor.vue +39 -2
  17. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.d.vue.ts +13 -0
  18. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue +216 -0
  19. package/dist/runtime/components/Integrations/WorkflowStateNamespacesDialog.vue.d.ts +13 -0
  20. package/dist/runtime/composables/useIntegrationsTemplates.js +5 -0
  21. package/dist/runtime/composables/useIntegrationsWorkflows.js +6 -0
  22. package/dist/runtime/data/integrationNodeCatalog.d.ts +25 -0
  23. package/dist/runtime/data/integrationNodeCatalog.js +8 -0
  24. package/dist/runtime/data/nodeVersionMove.d.ts +72 -0
  25. package/dist/runtime/data/nodeVersionMove.js +65 -0
  26. package/dist/runtime/i18n/messages/de.json +53 -4
  27. package/dist/runtime/i18n/messages/en.json +53 -4
  28. package/dist/runtime/pages/integrations/workflows/[id].vue +30 -20
  29. package/dist/runtime/pages/integrations/workflows/index.vue +23 -4
  30. package/dist/runtime/types/integrations.d.ts +44 -1
  31. package/dist/runtime/utils/buildSettle.d.ts +68 -0
  32. package/dist/runtime/utils/buildSettle.js +29 -0
  33. package/dist/runtime/utils/settingCondition.d.ts +48 -0
  34. package/dist/runtime/utils/settingCondition.js +84 -0
  35. package/dist/runtime/utils/stateNamespaceName.d.ts +23 -0
  36. package/dist/runtime/utils/stateNamespaceName.js +8 -0
  37. package/package.json +1 -1
@@ -158,11 +158,28 @@ export interface WorkflowIcon {
158
158
  /** Image URL, e.g. a node-package logo (kind=image). */
159
159
  src?: string;
160
160
  }
161
+ /** The four things a workflow can remember between runs (PO-374). */
162
+ export type WorkflowStateRole = 'mapping' | 'cursor' | 'dedupe' | 'digest';
163
+ /** `private` belongs to this workflow; `shared` is tenant-wide and outlives it. */
164
+ export type WorkflowStateVisibility = 'private' | 'shared';
165
+ /**
166
+ * One state namespace a workflow declares it may reach. The engine mints the
167
+ * run's grant from exactly this list, so an undeclared namespace is not
168
+ * undocumented — it is refused at run time.
169
+ */
170
+ export interface WorkflowStateNamespace {
171
+ namespace: string;
172
+ role: WorkflowStateRole;
173
+ /** Absent means `private`, which is what the engine's schema defaults to. */
174
+ visibility?: WorkflowStateVisibility;
175
+ }
161
176
  export interface WorkflowBlob {
162
177
  nodeManifestVersion: string;
163
178
  name: LocalizedString;
164
179
  description?: LocalizedString;
165
180
  variables?: Record<string, WorkflowVariable>;
181
+ /** State namespaces this workflow may reach at run time (BlobDef `state`). */
182
+ state?: WorkflowStateNamespace[];
166
183
  triggers: WorkflowTrigger[];
167
184
  nodes: WorkflowNode[];
168
185
  edges: WorkflowEdge[];
@@ -247,11 +264,25 @@ export interface NodeManifestOutput {
247
264
  * publishes them. Author-time schema only, never live run data. */
248
265
  fields?: string[] | Record<string, unknown>;
249
266
  }
250
- export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref' | 'dynamic-schema';
267
+ export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'select' | 'multiselect' | 'object' | 'array' | 'expression' | 'secret-ref' | 'credentials-ref'
268
+ /** Names one of the workflow's declared state namespaces (PO-374). */
269
+ | 'state-ref' | 'dynamic-schema';
251
270
  export interface ConfigFieldOption {
252
271
  value: string | number | boolean;
253
272
  label: LocalizedString;
254
273
  }
274
+ /** The comparison words a settings condition may use. One vocabulary with the
275
+ * condition and filter nodes, so an author who has met these words in one
276
+ * place does not learn a second set for the other. */
277
+ export type SettingConditionOperator = 'equals' | 'notEquals' | 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'exists' | 'notExists' | 'isEmpty' | 'isNotEmpty';
278
+ /** A condition on another setting's value, deciding whether this one applies. */
279
+ export interface SettingCondition {
280
+ /** The `key` of another setting at the same level. */
281
+ key: string;
282
+ op: SettingConditionOperator;
283
+ /** Left out by the four operators that read presence rather than content. */
284
+ value?: string | number | boolean;
285
+ }
255
286
  export interface ConfigField {
256
287
  key: string;
257
288
  label: LocalizedString;
@@ -278,8 +309,20 @@ export interface ConfigField {
278
309
  * (e.g. 'revenexx:business-central'). An array lets a node accept more than
279
310
  * one credential type; the picker merges the tenant's credentials across all. */
280
311
  credentialType?: string | string[];
312
+ /**
313
+ * Only for `type: 'state-ref'` (PO-374): which kind of state namespace the
314
+ * field accepts. The picker lists the workflow's declarations of that role;
315
+ * the stored value is the namespace NAME.
316
+ */
317
+ stateRole?: WorkflowStateRole;
281
318
  /** keys of other config fields this field's options/schema depend on. */
282
319
  dependsOn?: string[];
320
+ /** When this setting applies at all (PO-410): another setting's key, an
321
+ * operator, and where the operator takes one, the value to compare against.
322
+ * Drawn only while the condition holds — see utils/settingCondition. Not
323
+ * `dependsOn`, which decides what a setting resolves to rather than whether
324
+ * it is drawn. */
325
+ showIf?: SettingCondition;
283
326
  validation?: {
284
327
  pattern?: string;
285
328
  min?: number;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Watching a queued bundle build out, on any surface that reports one.
3
+ *
4
+ * A save queues a build, and until the platform has finished it the workflow cannot
5
+ * run. Both surfaces that say so read the state off the workflow row — the editor's
6
+ * chip beside Save, the build column in the workflows list — so neither of them
7
+ * learns that a build finished unless the row is read again.
8
+ *
9
+ * Two things can make that happen. A realtime build report, which is the fast path
10
+ * and is not always there: the topics have no producer in every environment, the
11
+ * connection can be off for a tenant, and a socket can drop in the second after the
12
+ * save. And re-reading the row, which is this — the floor under the fast path, and
13
+ * the only one of the two that works everywhere today.
14
+ *
15
+ * The editor has had it since PO-398, where a chip that never cleared was reported
16
+ * as "saving does not work"; the list kept the same defect for as long as it had no
17
+ * loop of its own (PO-416). So the loop lives here rather than in either surface:
18
+ * one interval, one ceiling, and one rule about which call wins, for both.
19
+ *
20
+ * WHAT A CALLER SUPPLIES is what differs between the two surfaces, and it is only
21
+ * ever these two things: whether anything is still unsettled — one workflow in the
22
+ * editor, any row in the list — and how to read it again. `settle()` decides
23
+ * nothing about the answer; the caller's own state is what every surface renders
24
+ * from.
25
+ *
26
+ * A GENERATION RATHER THAN A BUSY FLAG. Two saves in quick succession must not
27
+ * leave the second one unwatched, and the earlier loop cannot be relied on to cover
28
+ * it: a read already in flight can land after the newer save and put the pre-save
29
+ * row back into the cache, after which the older loop reads `ready` and stops. So
30
+ * the newest call wins and every older loop retires at its next turn.
31
+ *
32
+ * THE CEILING is not a timeout in the usual sense — nothing is reported when it is
33
+ * reached. A build is seconds; a build still queued after five minutes is a state
34
+ * worth leaving on screen rather than asking about forever, and the operator's own
35
+ * reload starts a fresh watch (which is why arriving on an unsettled row starts one).
36
+ */
37
+ /** Between reads. Same cadence the editor's queued-run wait uses. */
38
+ export declare const BUILD_SETTLE_POLL_MS = 2500;
39
+ /** After this, the state stands as last read and nothing asks again. */
40
+ export declare const BUILD_SETTLE_TIMEOUT_MS: number;
41
+ /**
42
+ * Whether a build state is one that changes on its own.
43
+ *
44
+ * `ready` and `failed` are where a build lands; anything else — including a row
45
+ * whose state the platform did not send — is nothing to wait for.
46
+ */
47
+ export declare function isBuildUnsettled(status?: string | null): boolean;
48
+ export interface BuildSettleWatch {
49
+ /** Whether anything is still worth waiting for. Read fresh on every turn. */
50
+ unsettled: () => boolean;
51
+ /** Reads the state again. Its own failures are its business — see below. */
52
+ read: () => Promise<unknown>;
53
+ }
54
+ export interface BuildSettleOptions {
55
+ pollMs?: number;
56
+ timeoutMs?: number;
57
+ }
58
+ export interface BuildSettle {
59
+ /**
60
+ * Reads until nothing is unsettled, or until the ceiling. Resolves either way and
61
+ * throws nothing: a read that fails leaves the state as it stands and the next
62
+ * turn tries again, which is the same answer a surface gives its reader anyway.
63
+ */
64
+ settle: (watch: BuildSettleWatch) => Promise<void>;
65
+ /** Retires whatever is watching — for a surface being left. */
66
+ stop: () => void;
67
+ }
68
+ export declare function createBuildSettle(options?: BuildSettleOptions): BuildSettle;
@@ -0,0 +1,29 @@
1
+ export const BUILD_SETTLE_POLL_MS = 2500;
2
+ export const BUILD_SETTLE_TIMEOUT_MS = 5 * 6e4;
3
+ export function isBuildUnsettled(status) {
4
+ return status === "pending" || status === "building";
5
+ }
6
+ export function createBuildSettle(options = {}) {
7
+ const pollMs = options.pollMs ?? BUILD_SETTLE_POLL_MS;
8
+ const timeoutMs = options.timeoutMs ?? BUILD_SETTLE_TIMEOUT_MS;
9
+ let generation = 0;
10
+ return {
11
+ async settle({ unsettled, read }) {
12
+ const mine = ++generation;
13
+ const started = Date.now();
14
+ while (Date.now() - started < timeoutMs) {
15
+ if (mine !== generation) return;
16
+ if (!unsettled()) return;
17
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
18
+ if (mine !== generation || !unsettled()) return;
19
+ try {
20
+ await read();
21
+ } catch {
22
+ }
23
+ }
24
+ },
25
+ stop() {
26
+ generation += 1;
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,48 @@
1
+ import type { ConfigField, SettingConditionOperator } from '../types/integrations.js';
2
+ /**
3
+ * What an operator means. Mirrors the SDK's `evaluate` row for row, including
4
+ * the leniency of `equals` across the string/number line: a select carries `1`
5
+ * where a text box carries `'1'`, and an author means one thing by both.
6
+ */
7
+ export declare function evaluateOperator(left: unknown, op: SettingConditionOperator, right: unknown): boolean;
8
+ /**
9
+ * Whether one setting applies, given the values at its own level — inside a
10
+ * group, the siblings in that group, which is where the node wrote the
11
+ * condition and where the platform resolves it.
12
+ *
13
+ * A setting with no condition always applies, and so does one whose condition
14
+ * is malformed: whatever the node meant, an author is better off seeing a field
15
+ * than having it silently withheld.
16
+ */
17
+ export declare function settingApplies(field: Pick<ConfigField, 'showIf'>, values: Record<string, unknown>): boolean;
18
+ /**
19
+ * Whether a change to `changedKey` leaves this setting's value stale.
20
+ *
21
+ * Two reasons a setting can depend on another, and they do not go stale at the
22
+ * same moment. `dependsOn` names what a dynamic field resolves its options
23
+ * from, and any change to it invalidates what was chosen out of the old
24
+ * options. A condition names the setting that decides whether the field is
25
+ * drawn at all, and what was typed only becomes unreachable when the condition
26
+ * stops holding: while it still holds the field is on screen, and clearing it
27
+ * on every keystroke in the setting it reads would empty a field the author is
28
+ * looking at — `showIf: { key: 'url', op: 'isNotEmpty' }` stays true from the
29
+ * first character on.
30
+ */
31
+ export declare function valueGoesStale(field: Pick<ConfigField, 'dependsOn' | 'showIf'>, changedKey: string, values: Record<string, unknown>): boolean;
32
+ /**
33
+ * The settings of one level whose values are stale once `changedKey` has been
34
+ * set — the cascade, walked to a fixpoint over that level's own fields.
35
+ *
36
+ * `after` is the config as it stands *after* the write, because a condition is
37
+ * read against it: whether a setting still applies is the whole question for a
38
+ * `showIf` dependant. Each cleared key is taken out of that config as the walk
39
+ * goes, so a setting withdrawn by a clearing is found in the same pass.
40
+ *
41
+ * The setting being written is never cleared, which is also what guards a
42
+ * cyclic `dependsOn`.
43
+ */
44
+ export declare function staleKeys(fields: Pick<ConfigField, 'key' | 'dependsOn' | 'showIf'>[], changedKey: string, after: Record<string, unknown>): Set<string>;
45
+ /**
46
+ * The settings of a level that apply, in the order the node declared them.
47
+ */
48
+ export declare function applyingFields(fields: ConfigField[], values: Record<string, unknown>): ConfigField[];
@@ -0,0 +1,84 @@
1
+ const READS_PRESENCE = ["exists", "notExists", "isEmpty", "isNotEmpty"];
2
+ const OPERATORS = [
3
+ "equals",
4
+ "notEquals",
5
+ "contains",
6
+ "notContains",
7
+ "startsWith",
8
+ "endsWith",
9
+ "greaterThan",
10
+ "greaterThanOrEqual",
11
+ "lessThan",
12
+ "lessThanOrEqual",
13
+ ...READS_PRESENCE
14
+ ];
15
+ export function evaluateOperator(left, op, right) {
16
+ switch (op) {
17
+ case "exists":
18
+ return left !== void 0 && left !== null;
19
+ case "notExists":
20
+ return left === void 0 || left === null;
21
+ case "isEmpty":
22
+ return left === "" || left === null || left === void 0 || Array.isArray(left) && left.length === 0;
23
+ case "isNotEmpty":
24
+ return !evaluateOperator(left, "isEmpty", right);
25
+ case "equals":
26
+ return String(left) === String(right);
27
+ case "notEquals":
28
+ return String(left) !== String(right);
29
+ case "contains":
30
+ return typeof left === "string" && left.includes(String(right));
31
+ case "notContains":
32
+ return typeof left === "string" && !left.includes(String(right));
33
+ case "startsWith":
34
+ return typeof left === "string" && left.startsWith(String(right));
35
+ case "endsWith":
36
+ return typeof left === "string" && left.endsWith(String(right));
37
+ case "greaterThan":
38
+ return Number(left) > Number(right);
39
+ case "greaterThanOrEqual":
40
+ return Number(left) >= Number(right);
41
+ case "lessThan":
42
+ return Number(left) < Number(right);
43
+ case "lessThanOrEqual":
44
+ return Number(left) <= Number(right);
45
+ default: {
46
+ const unhandled = op;
47
+ void unhandled;
48
+ return false;
49
+ }
50
+ }
51
+ }
52
+ export function settingApplies(field, values) {
53
+ const condition = field.showIf;
54
+ if (!condition || typeof condition !== "object")
55
+ return true;
56
+ const { key, op } = condition;
57
+ if (typeof key !== "string" || !OPERATORS.includes(op))
58
+ return true;
59
+ return evaluateOperator(values[key], op, condition.value);
60
+ }
61
+ export function valueGoesStale(field, changedKey, values) {
62
+ if ((field.dependsOn ?? []).includes(changedKey))
63
+ return true;
64
+ return field.showIf?.key === changedKey && !settingApplies(field, values);
65
+ }
66
+ export function staleKeys(fields, changedKey, after) {
67
+ const remaining = { ...after };
68
+ const out = /* @__PURE__ */ new Set();
69
+ const queue = [changedKey];
70
+ while (queue.length > 0) {
71
+ const dep = queue.shift();
72
+ for (const f of fields) {
73
+ if (f.key === changedKey || out.has(f.key)) continue;
74
+ if (!valueGoesStale(f, dep, remaining)) continue;
75
+ out.add(f.key);
76
+ delete remaining[f.key];
77
+ queue.push(f.key);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ export function applyingFields(fields, values) {
83
+ return fields.filter((field) => settingApplies(field, values));
84
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The engine's grammar for a state namespace (PO-374), mirrored in the browser
3
+ * so a name it would refuse is rejected while the author types rather than as a
4
+ * 422 about `blob.state.0` on save.
5
+ *
6
+ * One copy on purpose. Two surfaces declare namespaces — the workflow's State
7
+ * dialog and the picker inside a node's state setting — and a grammar spelled
8
+ * once per surface is one that widens or narrows in a single place, leaving the
9
+ * other accepting names the engine has stopped taking.
10
+ */
11
+ export declare const STATE_NAMESPACE_PATTERN: RegExp;
12
+ /**
13
+ * Which message a typed namespace name earns, as a translation key — or `null`
14
+ * when there is nothing to say. Both callers show the same sentence for the same
15
+ * mistake, which is the other half of holding the grammar in one place.
16
+ *
17
+ * An empty field returns `null`: nobody has answered yet, and a rejection shown
18
+ * before the first keystroke reads as a rejection of the dialog.
19
+ *
20
+ * @param name what the author has typed, trimmed here rather than by the caller
21
+ * @param taken the names the workflow already declares
22
+ */
23
+ export declare function stateNamespaceErrorKey(name: string, taken: string[]): string | null;
@@ -0,0 +1,8 @@
1
+ export const STATE_NAMESPACE_PATTERN = /^[a-z][a-z0-9]*([._-][a-z0-9]+)*$/;
2
+ export function stateNamespaceErrorKey(name, taken) {
3
+ const value = name.trim();
4
+ if (!value) return null;
5
+ if (!STATE_NAMESPACE_PATTERN.test(value)) return "integration.workflows.state.error.invalidName";
6
+ if (taken.includes(value)) return "integration.workflows.state.error.nameExists";
7
+ return null;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/studio-integrations",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Integration Studio (core: workflows, node editor, runs, schedules, credentials, secrets, audit) as a standalone, publishable Nuxt module.",
6
6
  "repository": {