@xeplr/actions 2.1.0 → 2.2.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.
- package/README.md +25 -1
- package/index.js +4 -0
- package/lib/builtins/data-operation.js +7 -0
- package/lib/builtins/db/fetch.js +25 -0
- package/lib/builtins/db/list-columns.js +3 -0
- package/lib/builtins/db/list-procedures.js +3 -0
- package/lib/builtins/db/list-tables.js +3 -0
- package/lib/builtins/db/list-views.js +3 -0
- package/lib/builtins/db/procedure.js +2 -0
- package/lib/builtins/email/delete.js +2 -0
- package/lib/builtins/email/download-attachments.js +2 -0
- package/lib/builtins/email/download-email.js +2 -0
- package/lib/builtins/email/move.js +2 -0
- package/lib/builtins/email/read.js +8 -0
- package/lib/builtins/export/pages.js +2 -0
- package/lib/builtins/export/table.js +2 -0
- package/lib/builtins/file/delete.js +2 -0
- package/lib/builtins/file/list.js +6 -0
- package/lib/builtins/file/move.js +2 -0
- package/lib/builtins/file/read.js +6 -0
- package/lib/builtins/send-email.js +2 -0
- package/lib/drivers/email/imap.js +5 -3
- package/lib/registry.js +16 -1
- package/lib/runner.js +35 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,10 +64,12 @@ var moved = await actions.runAction(actions.builtins.dbMove, {
|
|
|
64
64
|
|---|---|
|
|
65
65
|
| `register(def)` / `register(name, def)` | Add an action. Throws without `name` or `execute`, or with a `requires` module that is not installed (`ActionMissingDependencyError`). Re-registering a name replaces it. |
|
|
66
66
|
| `get(name)` / `has(name)` | Look one up. |
|
|
67
|
-
| `list()` | `[{ name, description, inputSchema, outputSchema, requires, event }]` — serializable, for an actions endpoint. `event` is `null` for an ordinary action. |
|
|
67
|
+
| `list()` | `[{ name, description, inputSchema, outputSchema, requires, event, preview, example }]` — serializable, for an actions endpoint. `event` is `null` for an ordinary action; `preview` is `true` when the action can be previewed (the function itself never leaves the process). |
|
|
68
68
|
| `clear()` | Empty the registry. |
|
|
69
69
|
| `runAction({ name, input, system?, timeoutMs? })` | Run a registered action. Never throws. |
|
|
70
70
|
| `runAction(nameOrDef, input, { system, timeoutMs, metaStore }?)` | Same, by name or with an action module directly. |
|
|
71
|
+
| `previewAction({ name, input, system? })` / `previewAction(nameOrDef, input, opts?)` | What the step **would** hand on, without running it — see [Previews](#previews-what-a-step-would-hand-on). Never calls `execute`. Never throws. |
|
|
72
|
+
| `PREVIEW_ROWS` | `5`: how many rows or items a preview reads. |
|
|
71
73
|
| `ActionNotRegisteredError` | Returned (in `error`) for an unknown name. |
|
|
72
74
|
| `ActionMissingDependencyError` | Thrown by `register()` for a missing `requires` module. |
|
|
73
75
|
| `TransientError` | For an action to signal "retry me". Nothing in this package (or `@xeplr/jobs`) acts on it yet. |
|
|
@@ -89,6 +91,8 @@ var moved = await actions.runAction(actions.builtins.dbMove, {
|
|
|
89
91
|
| `description` | Text for pickers. |
|
|
90
92
|
| `inputSchema` | `[{ name, type, required, default, description, order, ... }]`, validated by `@xeplr/schema-handler` `applySchema`. Built-ins also use `group`, `showWhen`, `system`, `options`, `optionsFrom`, `dependsOn`, `label`, `hidden` (left out of a form unless a value is already set), `suggestFrom` / `suggestWith` (a host option source that suggests values, asked with the named sibling inputs; `db-procedure`'s parameters ask `procedureParams` with the connection, database and procedure) and `control` (`'parameters'`: `[{ name, value }]` shown as rows, a value fixed or bound to the run's window: `{window.from}` / `{window.to}`, see `@xeplr/jobs`) — hints for a form; validation treats every field the same. |
|
|
91
93
|
| `outputSchema` | Optional documentation of the return value. |
|
|
94
|
+
| `preview({ input, system })` | Optional. Resolves to `{ output, rows? }`: what `execute` would return, read **without a side effect** — at most `PREVIEW_ROWS` rows or items, never a write, a send, a move or a delete. Only for an action that can read safely. |
|
|
95
|
+
| `example` | Optional. For an action that writes or sends: its output with example values, so a designer can show the shape without running it. |
|
|
92
96
|
| `requires` | Module names checked with `require.resolve` at `register()`. |
|
|
93
97
|
| `execute({ input, system, metaStore })` | Returns (or resolves to) the output. |
|
|
94
98
|
| `event` | Optional. Makes this a **screen function** rather than a step: the event it is written for — `beforeLoad`, `beforeSave`, `afterSave`, `beforeDelete`, `onError` (run by `@xeplr/factory` around a screen's records) or `decide` (run by `@xeplr/workflow-engine` to choose a flow's next step). A screen names one per event and pickers offer only the functions for that event. It is called with the event's context — `execute(ctx)`, not `{ input, system }` — and never as a workflow step. |
|
|
@@ -101,6 +105,26 @@ var moved = await actions.runAction(actions.builtins.dbMove, {
|
|
|
101
105
|
|
|
102
106
|
An unknown action, invalid input, a thrown or rejected `execute`, and a timeout (`timeoutMs`, message `Action timed out after Nms`) all come back as `status: 'failed'`. A timeout does not cancel the running work.
|
|
103
107
|
|
|
108
|
+
### Previews: what a step would hand on
|
|
109
|
+
|
|
110
|
+
A workflow designer shows a step's output before anything has run, so later steps can pick from it. Typing an example by hand was the only way, even where the output can simply be read. `previewAction` reads it instead, and **only ever calls an action's `preview`, never its `execute`**:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
{ status: 'success' | 'example' | 'failed', output, rows, error, durationMs }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `success`: `output` is what the step would hand on (its shape, sample values); `rows` the first few rows or items when it reads some.
|
|
117
|
+
- `example`: the action has no preview (it writes or sends); `output` is its declared `example`, or `null`. **Nothing ran.**
|
|
118
|
+
- `failed`: the input is incomplete or the read failed; `error` says which.
|
|
119
|
+
|
|
120
|
+
The input is checked exactly as a run checks it.
|
|
121
|
+
|
|
122
|
+
| previewed (reads, up to 5) | example only (writes or sends; never run) |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `db-fetch` (through the run's own cursor, then stops; nothing spooled), `db-list-tables` / `-views` / `-procedures` / `-columns`, `file-list`, `file-read`, `data-operation` (the run itself: it only computes), `email-read` (the mailbox opened **read-only**, IMAP `EXAMINE`, so nothing is marked read) | `file-move`, `file-delete`, `email-send`, `email-move`, `email-delete`, `email-download-email`, `email-download-attachments`, `export-table`, `export-pages`, `db-procedure` (a procedure may write) |
|
|
125
|
+
|
|
126
|
+
Neither: `db-push`, `db-move`, `http-request` (an outside system, which may change something), `spawn-program`, `file-upload`; a designer asks for an example by hand. `test/previews.test.js` checks it on a real folder, file and Postgres: five rows read and no file spooled, the real count kept, a deleting step's file still there.
|
|
127
|
+
|
|
104
128
|
## Built-in actions
|
|
105
129
|
|
|
106
130
|
Register the ones you want (`register(actions.builtins.dbFetch)`) or pass a module straight to `runAction`. `@xeplr/jobs` registers all implemented ones except `spawnProgram` and `dbMove`.
|
package/index.js
CHANGED
|
@@ -21,6 +21,10 @@ module.exports = {
|
|
|
21
21
|
// runAction({ name, input, system?, timeoutMs? })
|
|
22
22
|
// runAction(nameOrAction, input, opts?)
|
|
23
23
|
runAction: runner.runAction,
|
|
24
|
+
// What a step would hand on, read without a side effect (an action's
|
|
25
|
+
// `preview`), or its declared example. Never runs `execute`.
|
|
26
|
+
previewAction: runner.previewAction,
|
|
27
|
+
PREVIEW_ROWS: runner.PREVIEW_ROWS,
|
|
24
28
|
|
|
25
29
|
// Saved connections: an id in the definition, the login only in the call.
|
|
26
30
|
// Shared by @xeplr/jobs and @xeplr/workflow-engine. See lib/connections.js.
|
|
@@ -114,6 +114,13 @@ module.exports = {
|
|
|
114
114
|
// The same call every action gets from runAction: { input, system }. It
|
|
115
115
|
// once took (input, context) — the runner's object landed in `input`,
|
|
116
116
|
// `from` was undefined, and every workflow step returned no rows.
|
|
117
|
+
// Works only on the rows it is given — no database, no file, no send — so
|
|
118
|
+
// its preview is the run, over the sample rows the step before hands on.
|
|
119
|
+
preview: async function(call) {
|
|
120
|
+
var out = await module.exports.execute(call);
|
|
121
|
+
return { output: out, rows: (out && out.rows || []).slice(0, require('../runner').PREVIEW_ROWS) };
|
|
122
|
+
},
|
|
123
|
+
|
|
117
124
|
execute: async function(call) {
|
|
118
125
|
var input = (call && call.input) || {};
|
|
119
126
|
var system = (call && call.system) || {};
|
package/lib/builtins/db/fetch.js
CHANGED
|
@@ -79,6 +79,31 @@ module.exports = {
|
|
|
79
79
|
description: 'Rows pulled per cursor round-trip / stream highWaterMark.' }
|
|
80
80
|
],
|
|
81
81
|
|
|
82
|
+
// THE FIRST ROWS, through the same statement and cursor a run uses, then
|
|
83
|
+
// stop: nothing is spooled. The output is what a run hands on, with the
|
|
84
|
+
// file it would write named but not written.
|
|
85
|
+
preview: async function(ctx) {
|
|
86
|
+
var input = ctx.input || {};
|
|
87
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
88
|
+
dbDrivers.checkDriverRequires('db-fetch', input.dbType, driver);
|
|
89
|
+
var built = buildFetchSql(driver, input);
|
|
90
|
+
var pool = await driver.connect(input.connection);
|
|
91
|
+
var rows = [];
|
|
92
|
+
try {
|
|
93
|
+
var stream = driver.fetchStream(pool, { sql: built.sql, params: built.params, batchSize: require('../../runner').PREVIEW_ROWS });
|
|
94
|
+
for await (var row of stream) {
|
|
95
|
+
rows.push(row);
|
|
96
|
+
if (rows.length >= require('../../runner').PREVIEW_ROWS) break;
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await driver.close(pool);
|
|
100
|
+
}
|
|
101
|
+
var output = input.streaming_mode === false
|
|
102
|
+
? { rows: rows, rowCount: rows.length, streaming: false }
|
|
103
|
+
: { filePath: '(a file in this company\'s tmp/, written when it runs)', format: 'jsonl', bytes: 0, rows: rows.length, streaming: true };
|
|
104
|
+
return { output: output, rows: rows };
|
|
105
|
+
},
|
|
106
|
+
|
|
82
107
|
execute: async function(ctx) {
|
|
83
108
|
var input = ctx.input || {};
|
|
84
109
|
var system = ctx.system || {};
|
|
@@ -20,6 +20,9 @@ module.exports = {
|
|
|
20
20
|
description: 'Schema the table lives in (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
21
21
|
],
|
|
22
22
|
|
|
23
|
+
// Only reads the database's own catalogue, so the preview is the run.
|
|
24
|
+
preview: async function(ctx) { return { output: await module.exports.execute(ctx) }; },
|
|
25
|
+
|
|
23
26
|
execute: async function(ctx) {
|
|
24
27
|
var input = ctx.input || {};
|
|
25
28
|
var driver = dbDrivers.getDriver(input.dbType);
|
|
@@ -17,6 +17,9 @@ module.exports = {
|
|
|
17
17
|
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
18
18
|
],
|
|
19
19
|
|
|
20
|
+
// Only reads the database's own catalogue, so the preview is the run.
|
|
21
|
+
preview: async function(ctx) { return { output: await module.exports.execute(ctx) }; },
|
|
22
|
+
|
|
20
23
|
execute: async function(ctx) {
|
|
21
24
|
var input = ctx.input || {};
|
|
22
25
|
var driver = dbDrivers.getDriver(input.dbType);
|
|
@@ -19,6 +19,9 @@ module.exports = {
|
|
|
19
19
|
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
20
20
|
],
|
|
21
21
|
|
|
22
|
+
// Only reads the database's own catalogue, so the preview is the run.
|
|
23
|
+
preview: async function(ctx) { return { output: await module.exports.execute(ctx) }; },
|
|
24
|
+
|
|
22
25
|
execute: async function(ctx) {
|
|
23
26
|
var input = ctx.input || {};
|
|
24
27
|
var driver = dbDrivers.getDriver(input.dbType);
|
|
@@ -16,6 +16,9 @@ module.exports = {
|
|
|
16
16
|
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
17
17
|
],
|
|
18
18
|
|
|
19
|
+
// Only reads the database's own catalogue, so the preview is the run.
|
|
20
|
+
preview: async function(ctx) { return { output: await module.exports.execute(ctx) }; },
|
|
21
|
+
|
|
19
22
|
execute: async function(ctx) {
|
|
20
23
|
var input = ctx.input || {};
|
|
21
24
|
var driver = dbDrivers.getDriver(input.dbType);
|
|
@@ -32,6 +32,8 @@ var { buildProcedureCall } = require('../../drivers/db/procedure');
|
|
|
32
32
|
|
|
33
33
|
module.exports = {
|
|
34
34
|
name: 'db-procedure',
|
|
35
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
36
|
+
example: { rows: [{ id: 1, status: 'done' }], rowCount: 1, procedure: 'close_period', sql: 'CALL close_period(?)' },
|
|
35
37
|
description: 'Call a stored procedure with parameters. Returns its rows when it produces any — ' +
|
|
36
38
|
'no target table, no movement. For a procedure whose rows you want written to a ' +
|
|
37
39
|
'table, use db-move with mode="procedure".',
|
|
@@ -13,6 +13,8 @@ var emailConfig = require('../../drivers/email/config');
|
|
|
13
13
|
|
|
14
14
|
module.exports = {
|
|
15
15
|
name: 'email-delete',
|
|
16
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
17
|
+
example: { deleted: true, messageId: '4182', folder: 'INBOX' },
|
|
16
18
|
description: 'Permanently delete a message (flags \\Deleted and expunges — NOT a move to Trash). ' +
|
|
17
19
|
'The message id is folder-scoped, so input.folder must name the folder it is in.',
|
|
18
20
|
requires: [],
|
|
@@ -17,6 +17,8 @@ var fileNames = require('../../fileNames');
|
|
|
17
17
|
|
|
18
18
|
module.exports = {
|
|
19
19
|
name: 'email-download-attachments',
|
|
20
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
21
|
+
example: { count: 1, saved: [{ filename: 'invoice.pdf', path: 'mail/invoice.pdf', size: 91022, contentType: 'application/pdf' }], path: 'mail/invoice.pdf', existing: [], subject: 'Invoice 450103', messageId: '4182' },
|
|
20
22
|
description: 'Save a message\'s attachments to disk, optionally filtered by extension or ' +
|
|
21
23
|
'filename substring, named as you say. Returns { count, saved[], path, skipped, existing[] }.',
|
|
22
24
|
requires: [],
|
|
@@ -15,6 +15,8 @@ var fileNames = require('../../fileNames');
|
|
|
15
15
|
|
|
16
16
|
module.exports = {
|
|
17
17
|
name: 'email-download-email',
|
|
18
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
19
|
+
example: { count: 1, saved: [{ filename: 'Invoice 450103.eml', path: 'mail/Invoice 450103.eml', size: 48213, contentType: 'message/rfc822' }], path: 'mail/Invoice 450103.eml', existing: [], subject: 'Invoice 450103', messageId: '4182' },
|
|
18
20
|
description: 'Save an entire message to disk as a .eml (RFC-822) file, streamed. ' +
|
|
19
21
|
'Returns { count, saved[], path } like email-download-attachments.',
|
|
20
22
|
requires: [],
|
|
@@ -14,6 +14,8 @@ var emailConfig = require('../../drivers/email/config');
|
|
|
14
14
|
|
|
15
15
|
module.exports = {
|
|
16
16
|
name: 'email-move',
|
|
17
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
18
|
+
example: { moved: true, messageId: '4182', fromFolder: 'INBOX', toFolder: 'Processed' },
|
|
17
19
|
description: 'Move a message to another mailbox folder. The message id is folder-scoped, ' +
|
|
18
20
|
'so input.folder must name the folder the message is currently in.',
|
|
19
21
|
requires: [],
|
|
@@ -59,6 +59,14 @@ module.exports = {
|
|
|
59
59
|
description: 'true → spool messages to an NDJSON file (output.filePath). false → inline output.messages.' }
|
|
60
60
|
],
|
|
61
61
|
|
|
62
|
+
// The first messages, with the mailbox opened READ-ONLY (IMAP EXAMINE), so
|
|
63
|
+
// a preview never marks anything read — whatever a full download does.
|
|
64
|
+
preview: async function(ctx) {
|
|
65
|
+
var input = Object.assign({}, ctx.input, { limit: require('../../runner').PREVIEW_ROWS, readOnly: true, streaming_mode: false });
|
|
66
|
+
var out = await module.exports.execute({ input: input, system: ctx.system });
|
|
67
|
+
return { output: out, rows: out.messages };
|
|
68
|
+
},
|
|
69
|
+
|
|
62
70
|
execute: async function(ctx) {
|
|
63
71
|
var input = ctx.input || {};
|
|
64
72
|
var system = ctx.system || {};
|
|
@@ -66,6 +66,8 @@ function validate(spec) {
|
|
|
66
66
|
|
|
67
67
|
module.exports = {
|
|
68
68
|
name: 'export-pages',
|
|
69
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
70
|
+
example: { filePath: 'exports/board.pdf', fileName: 'board.pdf', contentType: 'application/pdf', bytes: 220514, pages: 3 },
|
|
69
71
|
description: 'Lay out pages of placed blocks (text, tables, images, shapes) as a PDF — a dashboard as it looked.',
|
|
70
72
|
requires: [],
|
|
71
73
|
LIMITS: LIMITS,
|
|
@@ -57,6 +57,8 @@ function columnsOf(rows) {
|
|
|
57
57
|
|
|
58
58
|
module.exports = {
|
|
59
59
|
name: 'export-table',
|
|
60
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
61
|
+
example: { filePath: 'exports/sales.xlsx', fileName: 'sales.xlsx', contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bytes: 18432, rows: 1420, truncated: false, skipped: false },
|
|
60
62
|
description: 'Write rows to a CSV, Excel (.xlsx) or PDF file. Excel cells stay numbers with their number format; ' +
|
|
61
63
|
'a PDF shows each cell as it was displayed.',
|
|
62
64
|
requires: [],
|
|
@@ -11,6 +11,8 @@ var dataDir = require('../../dataDir');
|
|
|
11
11
|
|
|
12
12
|
module.exports = {
|
|
13
13
|
name: 'file-delete',
|
|
14
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
15
|
+
example: { path: 'bank/inbox/sept.csv', deleted: true, skipped: false },
|
|
14
16
|
description: 'Delete one file, permanently (file-move to a "processed" folder is the reversible one). Returns { path, deleted, skipped }.',
|
|
15
17
|
requires: [],
|
|
16
18
|
inputSchema: [
|
|
@@ -36,6 +36,12 @@ module.exports = {
|
|
|
36
36
|
{ name: 'count', type: 'number', description: 'How many, so the next step can branch on none.' }
|
|
37
37
|
],
|
|
38
38
|
|
|
39
|
+
// Listing only reads the folder; the preview is the run, its first files.
|
|
40
|
+
preview: async function(ctx) {
|
|
41
|
+
var out = await module.exports.execute(ctx);
|
|
42
|
+
return { output: { files: out.files.slice(0, require('../../runner').PREVIEW_ROWS), count: out.count }, rows: out.files.slice(0, require('../../runner').PREVIEW_ROWS) };
|
|
43
|
+
},
|
|
44
|
+
|
|
39
45
|
execute: async function(ctx) {
|
|
40
46
|
var input = ctx.input || {};
|
|
41
47
|
var dir = dataDir.resolve(input.folder, 'The folder');
|
|
@@ -12,6 +12,8 @@ var fileNames = require('../../fileNames');
|
|
|
12
12
|
|
|
13
13
|
module.exports = {
|
|
14
14
|
name: 'file-move',
|
|
15
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
16
|
+
example: { path: 'bank/processed/sept.csv', name: 'sept.csv', moved: true, skipped: false },
|
|
15
17
|
description: 'Move or copy a file into a folder, or to a new name. Returns { path, name, moved, skipped }.',
|
|
16
18
|
requires: [],
|
|
17
19
|
inputSchema: [
|
|
@@ -38,6 +38,12 @@ module.exports = {
|
|
|
38
38
|
{ name: 'truncated', type: 'boolean' }
|
|
39
39
|
],
|
|
40
40
|
|
|
41
|
+
// The file's first rows: reading never changes it.
|
|
42
|
+
preview: async function(ctx) {
|
|
43
|
+
var out = await module.exports.execute({ input: Object.assign({}, ctx.input, { maxRows: require('../../runner').PREVIEW_ROWS }), system: ctx.system });
|
|
44
|
+
return { output: out, rows: out.rows };
|
|
45
|
+
},
|
|
46
|
+
|
|
41
47
|
execute: async function(ctx) {
|
|
42
48
|
var input = ctx.input || {};
|
|
43
49
|
var file = dataDir.resolve(input.path, 'The file');
|
|
@@ -48,6 +48,8 @@ var emailConfig = require('../drivers/email/config');
|
|
|
48
48
|
|
|
49
49
|
module.exports = {
|
|
50
50
|
name: 'email-send',
|
|
51
|
+
// What it hands on, with example values: it writes, so it is never previewed.
|
|
52
|
+
example: { sent: true, messageId: '<4182@mail.acme.com>', accepted: ['asha@acme.com'], rejected: [], response: '250 OK' },
|
|
51
53
|
description: 'Send an email over SMTP. Attachments accept { filename, path } descriptors, ' +
|
|
52
54
|
'so email-download-attachments output can be forwarded directly.',
|
|
53
55
|
requires: [], // nodemailer checked at runtime
|
|
@@ -95,7 +95,7 @@ function buildClientConfig(action, connection) {
|
|
|
95
95
|
// through this so the release/logout pair can never be forgotten in one of
|
|
96
96
|
// them. `logout()` is best-effort in its own catch: a failure there must not
|
|
97
97
|
// mask the real error from the work, and must not fail an op that succeeded.
|
|
98
|
-
async function withMailbox(action, connection, folder, fn) {
|
|
98
|
+
async function withMailbox(action, connection, folder, fn, readOnly) {
|
|
99
99
|
// Config FIRST, deps second. Validating the connection needs no peer dep, so
|
|
100
100
|
// a misconfigured connection reports the missing field even on a host that
|
|
101
101
|
// hasn't installed imapflow — otherwise the dep error masks the real one.
|
|
@@ -105,7 +105,9 @@ async function withMailbox(action, connection, folder, fn) {
|
|
|
105
105
|
|
|
106
106
|
await client.connect();
|
|
107
107
|
try {
|
|
108
|
-
|
|
108
|
+
// Read-only (IMAP EXAMINE) for a preview: nothing it fetches can mark a
|
|
109
|
+
// message read, whatever the server does on a full download.
|
|
110
|
+
var lock = await client.getMailboxLock(folder || 'INBOX', readOnly ? { readOnly: true } : undefined);
|
|
109
111
|
try {
|
|
110
112
|
return await fn(client);
|
|
111
113
|
} finally {
|
|
@@ -257,7 +259,7 @@ async function read(connection, input) {
|
|
|
257
259
|
});
|
|
258
260
|
}
|
|
259
261
|
return out;
|
|
260
|
-
});
|
|
262
|
+
}, input.readOnly === true);
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
/** Move one message to another folder. Returns the shape the action wraps. */
|
package/lib/registry.js
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
// outputSchema : array? — optional; documents what the action returns
|
|
10
10
|
// requires : string[]? — peer-dep module names checked at register time
|
|
11
11
|
// execute : function — async ({ input, system }) => output
|
|
12
|
+
// preview : function? — async ({ input, system }) => { output, rows? }:
|
|
13
|
+
// what execute WOULD hand on, read without a
|
|
14
|
+
// side effect — a few rows, never a write, a
|
|
15
|
+
// send, a move or a delete. A designer shows it
|
|
16
|
+
// before anything has run (runner.previewAction).
|
|
17
|
+
// example : object? — for an action that cannot be previewed
|
|
18
|
+
// safely (it writes or sends): its output with
|
|
19
|
+
// example values, so a designer still shows
|
|
20
|
+
// the shape.
|
|
12
21
|
// event : string? — a SCREEN FUNCTION, not a step: the event it
|
|
13
22
|
// is written for ('beforeSave', 'decide', …).
|
|
14
23
|
// Called with that event's own context instead
|
|
@@ -61,6 +70,8 @@ function register(nameOrDef, def) {
|
|
|
61
70
|
outputSchema: action.outputSchema || null,
|
|
62
71
|
requires: Array.isArray(action.requires) ? action.requires.slice() : [],
|
|
63
72
|
event: action.event || null,
|
|
73
|
+
preview: typeof action.preview === 'function' ? action.preview : null,
|
|
74
|
+
example: action.example && typeof action.example === 'object' ? action.example : null,
|
|
64
75
|
execute: action.execute
|
|
65
76
|
});
|
|
66
77
|
}
|
|
@@ -78,7 +89,11 @@ function list() {
|
|
|
78
89
|
inputSchema: def.inputSchema,
|
|
79
90
|
outputSchema: def.outputSchema,
|
|
80
91
|
requires: def.requires,
|
|
81
|
-
event: def.event
|
|
92
|
+
event: def.event,
|
|
93
|
+
// Whether a designer can ask for a preview — the function itself never
|
|
94
|
+
// leaves the process.
|
|
95
|
+
preview: Boolean(def.preview),
|
|
96
|
+
example: def.example
|
|
82
97
|
});
|
|
83
98
|
});
|
|
84
99
|
return out;
|
package/lib/runner.js
CHANGED
|
@@ -43,6 +43,40 @@ async function runAction(first, second, third) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* WHAT A STEP WOULD HAND ON, without running it. Calls the action's `preview`
|
|
48
|
+
* — never `execute` — with the input checked exactly as a run checks it.
|
|
49
|
+
*
|
|
50
|
+
* Returns { status, output, rows, error, durationMs }:
|
|
51
|
+
* status 'success' output is what execute would return (its shape and
|
|
52
|
+
* sample values); rows, when the action reads rows, the
|
|
53
|
+
* first few of them.
|
|
54
|
+
* status 'example' the action cannot be previewed safely; output is its
|
|
55
|
+
* declared example (or null).
|
|
56
|
+
* status 'failed' the input is incomplete or the read failed — error says.
|
|
57
|
+
* Never throws.
|
|
58
|
+
*/
|
|
59
|
+
async function previewAction(first, second, third) {
|
|
60
|
+
var start = Date.now();
|
|
61
|
+
var opts = normalizeArgs(first, second, third);
|
|
62
|
+
var def = opts.action || get(opts.name);
|
|
63
|
+
if (!def) return Object.assign(finish(start, 'failed', null, new ActionNotRegisteredError(opts.name)), { rows: null });
|
|
64
|
+
if (typeof def.preview !== 'function') {
|
|
65
|
+
return Object.assign(finish(start, 'example', def.example || null, null), { rows: null });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
var input = applySchema(def.inputSchema, coerceScalars(def.inputSchema, opts.input || {}), 'input');
|
|
69
|
+
var seen = await def.preview({ input: input, system: opts.system || {}, metaStore: opts.metaStore });
|
|
70
|
+
seen = seen || {};
|
|
71
|
+
return Object.assign(finish(start, 'success', seen.output === undefined ? null : seen.output, null), { rows: seen.rows || null });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return Object.assign(finish(start, 'failed', null, err), { rows: null });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** How many rows a preview reads: enough to see, never the data. */
|
|
78
|
+
var PREVIEW_ROWS = 5;
|
|
79
|
+
|
|
46
80
|
/**
|
|
47
81
|
* ONE VALUE WHERE A LIST WAS DECLARED MEANS A LIST OF ONE.
|
|
48
82
|
*
|
|
@@ -125,4 +159,4 @@ function withTimeout(promise, ms) {
|
|
|
125
159
|
});
|
|
126
160
|
}
|
|
127
161
|
|
|
128
|
-
module.exports = { runAction: runAction };
|
|
162
|
+
module.exports = { runAction: runAction, previewAction: previewAction, PREVIEW_ROWS: PREVIEW_ROWS };
|
package/package.json
CHANGED