@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 +12 -0
- package/index.js +5 -0
- package/lib/connections.js +104 -0
- package/package.json +1 -1
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