@xeplr/actions 1.4.5 → 1.4.6

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/README.md CHANGED
@@ -233,6 +233,18 @@ should not take the other eight hundred and ninety-nine with it.
233
233
  **No aggregation.** `group by status, count` changes what a row *is*; that is
234
234
  a different shape and, when it is wanted, a different step.
235
235
 
236
+ ## Saved connections: an id in the definition, the login only in the call
237
+
238
+ A job or a workflow step stores `connectionInfoId`, never a password. `resolveConnections(inputs, resolver, who?)` swaps each id for the login just before the action runs, in a **new** object, so what the run records still holds only the id. `@xeplr/jobs` and `@xeplr/workflow-engine` both use it.
239
+
240
+ | field in the definition | filled for the action |
241
+ |---|---|
242
+ | `connectionInfoId` | `connection` (and `dbType`, when the resolver says) |
243
+ | `sourceConnectionInfoId` / `targetConnectionInfoId` | `sourceConnection` / `targetConnection` (and `…DbType`) |
244
+
245
+ - **`resolver(id, { field, inputs })`** is the host's: a login, or `{ dbType, connection }`. It sees all the inputs, for a companion id such as BI's `dbInfoId`.
246
+ - **An id that resolves to nothing is refused** (`CONNECTION_NOT_FOUND`), never replaced by a literal `connection`. An id with no resolver configured: `NO_CONNECTION_RESOLVER`.
247
+
236
248
  ## Saving files: names, and a name already taken
237
249
 
238
250
  Every action that saves a file follows one rule (`lib/fileNames.js`): `email-download-attachments`, `email-download-email` and `export-table`.
package/index.js CHANGED
@@ -22,6 +22,11 @@ module.exports = {
22
22
  // runAction(nameOrAction, input, opts?)
23
23
  runAction: runner.runAction,
24
24
 
25
+ // Saved connections: an id in the definition, the login only in the call.
26
+ // Shared by @xeplr/jobs and @xeplr/workflow-engine. See lib/connections.js.
27
+ resolveConnections: require('./lib/connections').resolveConnections,
28
+ connectionFieldFor: require('./lib/connections').connectionFieldFor,
29
+
25
30
  // Errors
26
31
  ActionNotRegisteredError: errors.ActionNotRegisteredError,
27
32
  ActionMissingDependencyError: errors.ActionMissingDependencyError,
@@ -0,0 +1,104 @@
1
+ // SAVED CONNECTIONS: AN ID IN THE STEP, THE LOGIN ONLY IN THE CALL.
2
+ //
3
+ // An action like db-fetch takes `connection: { host, port, user, password,
4
+ // database }`. A job or a workflow step cannot store that, and a person
5
+ // cannot type it:
6
+ //
7
+ // - The browser must never see it. A host seals credentials and decrypts
8
+ // them only at the point of use.
9
+ // - What a run records must never hold it. A job's inputs are snapshotted
10
+ // on every occurrence, and a workflow step's input on every step run, so
11
+ // one password in a definition becomes one password in a hundred rows.
12
+ //
13
+ // So the definition stores `connectionInfoId` (an id, safe everywhere), and
14
+ // the id is exchanged for the real login HERE, moments before the action is
15
+ // called, only in the object handed to that call. One copy, used by
16
+ // @xeplr/jobs and @xeplr/workflow-engine.
17
+ //
18
+ // ONE HOOK, not one per action: the host says once how an id becomes a
19
+ // connection (`resolver`), and every action taking a connection benefits.
20
+ // The convention is the field NAME:
21
+ //
22
+ // connectionInfoId → connection
23
+ // sourceConnectionInfoId → sourceConnection
24
+ // targetConnectionInfoId → targetConnection
25
+ //
26
+ // so an action with two connections needs no configuration to get both.
27
+
28
+ var CONNECTION_ID_SUFFIX = /ConnectionInfoId$/;
29
+
30
+ /** The field a connection id fills: connectionInfoId → connection. null for any other field. */
31
+ function connectionFieldFor(key) {
32
+ if (key === 'connectionInfoId') return 'connection';
33
+ if (!CONNECTION_ID_SUFFIX.test(key)) return null;
34
+ return key.slice(0, -'ConnectionInfoId'.length) + 'Connection';
35
+ }
36
+
37
+ /**
38
+ * Swap every *ConnectionInfoId in `inputs` for the login it names.
39
+ *
40
+ * Returns a NEW object: the caller's inputs are left untouched, which is what
41
+ * keeps a run's record free of secrets.
42
+ *
43
+ * A MISSING RESOLUTION IS FATAL, deliberately. If a definition says "use
44
+ * connection X" and X has been deleted, belongs to another tenant, or cannot
45
+ * be decrypted because the key was rotated, the only safe answer is to stop.
46
+ * Falling back to a literal `connection` sitting in the inputs would connect
47
+ * to something the author did not ask for, and it would work. A literal
48
+ * connection is honoured only when no id was given at all.
49
+ *
50
+ * Call it inside the run's tenant context: a host's saved connections are
51
+ * usually tenant-scoped, and resolving outside one finds nothing.
52
+ *
53
+ * @param {object} inputs
54
+ * @param {Function} resolver async (id, { field, inputs }) → a login, or { dbType, connection }
55
+ * @param {string} [who] how the refusal names the caller ("This job", "Step fetch")
56
+ */
57
+ async function resolveConnections(inputs, resolver, who) {
58
+ var raw = inputs || {};
59
+ var ids = Object.keys(raw).filter(function(k) { return connectionFieldFor(k) && raw[k]; });
60
+ if (!ids.length) return raw;
61
+ var name = who || 'This';
62
+
63
+ if (typeof resolver !== 'function') {
64
+ var none = new Error(name + ' names a saved connection (' + ids.join(', ') +
65
+ '), but no connection resolver is configured. The host passes resolveConnection.');
66
+ none.code = 'NO_CONNECTION_RESOLVER';
67
+ throw none;
68
+ }
69
+
70
+ var out = Object.assign({}, raw);
71
+ for (var i = 0; i < ids.length; i++) {
72
+ var key = ids[i];
73
+ var target = connectionFieldFor(key);
74
+
75
+ // THE WHOLE INPUTS go to the resolver, not just the one id: a saved
76
+ // connection is not always one id. BI's takes (connectionId, dbInfoId),
77
+ // the server and then which database on it.
78
+ var resolved = await resolver(raw[key], { field: key, inputs: raw });
79
+ if (!resolved) {
80
+ var missing = new Error('Connection "' + raw[key] + '" (' + key + ') could not be resolved: ' +
81
+ 'it may have been deleted, or belong to another company. Refusing to run ' +
82
+ 'rather than fall back to a different connection.');
83
+ missing.code = 'CONNECTION_NOT_FOUND';
84
+ throw missing;
85
+ }
86
+
87
+ // Two accepted shapes: a bare login, or { dbType, connection }. The
88
+ // second matters: the connection already knows whether it is postgres,
89
+ // mysql or mssql, so nobody has to choose that again.
90
+ if (resolved.connection) {
91
+ out[target] = resolved.connection;
92
+ var typeField = target === 'connection' ? 'dbType' : target + 'DbType';
93
+ if (resolved.dbType) out[typeField] = resolved.dbType;
94
+ } else {
95
+ out[target] = resolved;
96
+ }
97
+
98
+ // The id goes no further. The action was never given one to interpret.
99
+ delete out[key];
100
+ }
101
+ return out;
102
+ }
103
+
104
+ module.exports = { resolveConnections: resolveConnections, connectionFieldFor: connectionFieldFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/actions",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "Named-action registry with typed inputs and safe invocation — plus built-in actions for HTTP, DB, files, email, spawn",
5
5
  "main": "index.js",
6
6
  "scripts": {