@xeplr/actions 1.1.1
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/LICENSE +21 -0
- package/index.js +51 -0
- package/lib/builtins/db/fetch.js +142 -0
- package/lib/builtins/db/list-columns.js +39 -0
- package/lib/builtins/db/list-procedures.js +36 -0
- package/lib/builtins/db/list-tables.js +38 -0
- package/lib/builtins/db/list-views.js +35 -0
- package/lib/builtins/db/push.js +107 -0
- package/lib/builtins/db/query.js +5 -0
- package/lib/builtins/file/move.js +8 -0
- package/lib/builtins/file/upload.js +123 -0
- package/lib/builtins/http-request.js +3 -0
- package/lib/builtins/index.js +40 -0
- package/lib/builtins/send-email.js +8 -0
- package/lib/builtins/spawn-program.js +4 -0
- package/lib/drivers/db/index.js +41 -0
- package/lib/drivers/db/mongo.js +8 -0
- package/lib/drivers/db/mssql.js +464 -0
- package/lib/drivers/db/mysql.js +425 -0
- package/lib/drivers/db/postgres.js +372 -0
- package/lib/drivers/file/google.js +8 -0
- package/lib/drivers/file/index.js +42 -0
- package/lib/drivers/file/local.js +68 -0
- package/lib/drivers/file/sftp.js +9 -0
- package/lib/drivers/file/sharepoint.js +9 -0
- package/lib/errors.js +37 -0
- package/lib/formats/_xlsx-stream.js +355 -0
- package/lib/formats/csv.js +54 -0
- package/lib/formats/excel.js +87 -0
- package/lib/formats/index.js +38 -0
- package/lib/formats/json.js +42 -0
- package/lib/formats/txt.js +6 -0
- package/lib/registry.js +75 -0
- package/lib/runner.js +81 -0
- package/lib/streaming/checkpoint.js +46 -0
- package/lib/streaming/index.js +10 -0
- package/lib/streaming/resume.js +35 -0
- package/lib/streaming/spool.js +253 -0
- package/lib/streaming/utils.js +82 -0
- package/lib/uploader/attach.js +69 -0
- package/lib/uploader/index.js +298 -0
- package/lib/uploader/infer.js +76 -0
- package/lib/uploader/meta-store-knex.js +136 -0
- package/lib/uploader/meta-store-noop.js +11 -0
- package/lib/uploader/reconcile.js +88 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xeplr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
var registry = require('./lib/registry');
|
|
2
|
+
var runner = require('./lib/runner');
|
|
3
|
+
var errors = require('./lib/errors');
|
|
4
|
+
var builtins = require('./lib/builtins');
|
|
5
|
+
var uploader = require('./lib/uploader');
|
|
6
|
+
var streaming = require('./lib/streaming');
|
|
7
|
+
var dbDrivers = require('./lib/drivers/db');
|
|
8
|
+
var fileDrivers = require('./lib/drivers/file');
|
|
9
|
+
var formats = require('./lib/formats');
|
|
10
|
+
var configDb = require('./lib/uploader/attach');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
// Registry
|
|
14
|
+
register: registry.register,
|
|
15
|
+
get: registry.get,
|
|
16
|
+
has: registry.has,
|
|
17
|
+
list: registry.list,
|
|
18
|
+
clear: registry.clear,
|
|
19
|
+
|
|
20
|
+
// Safe invocation — two forms:
|
|
21
|
+
// runAction({ name, input, system?, timeoutMs? })
|
|
22
|
+
// runAction(nameOrAction, input, opts?)
|
|
23
|
+
runAction: runner.runAction,
|
|
24
|
+
|
|
25
|
+
// Errors
|
|
26
|
+
ActionNotRegisteredError: errors.ActionNotRegisteredError,
|
|
27
|
+
ActionMissingDependencyError: errors.ActionMissingDependencyError,
|
|
28
|
+
TransientError: errors.TransientError,
|
|
29
|
+
|
|
30
|
+
// Built-in actions
|
|
31
|
+
// var { register, builtins } = require('@xeplr/actions');
|
|
32
|
+
// register(builtins.sendEmail); ← simple
|
|
33
|
+
// register(builtins.fileUpload({ metaKnex })); ← factory-shaped when config is needed
|
|
34
|
+
builtins: builtins,
|
|
35
|
+
|
|
36
|
+
// Primitives (used internally by actions and exposed for consumers writing
|
|
37
|
+
// their own — e.g. a UI wizard showing inferred columns from a CSV sample
|
|
38
|
+
// before actually committing to an import).
|
|
39
|
+
uploader: uploader,
|
|
40
|
+
inferColumns: uploader.inferColumns,
|
|
41
|
+
streaming: streaming,
|
|
42
|
+
drivers: { db: dbDrivers, file: fileDrivers },
|
|
43
|
+
formats: formats,
|
|
44
|
+
|
|
45
|
+
// xeplr_configs — shared control-plane DB (import/movement metadata).
|
|
46
|
+
// One call per app: attachConfig({ service }); await xcfg.ready(). See
|
|
47
|
+
// lib/uploader/attach.js. configRequiredEnv spreads into the app's own
|
|
48
|
+
// env.required.js, same convention as @xeplr/auth's requiredEnv.
|
|
49
|
+
attachConfig: configDb.attachConfig,
|
|
50
|
+
configRequiredEnv: configDb.requiredEnv
|
|
51
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// db-fetch action — routes to getDriver(input.dbType), then streams rows out
|
|
2
|
+
// of the source via driver.fetchStream (a server-side cursor / native stream,
|
|
3
|
+
// never buffering the whole result set).
|
|
4
|
+
//
|
|
5
|
+
// STREAMING BY DEFAULT. Unlike the older builtins convention (streaming_mode
|
|
6
|
+
// defaults false), db-fetch defaults streaming_mode=TRUE: rows are spooled to
|
|
7
|
+
// an NDJSON temp file and the action returns { filePath, format, bytes, rows }.
|
|
8
|
+
// That output plugs straight into db-push's `filePath` input for cross-engine
|
|
9
|
+
// replication with a bounded memory footprint at both ends.
|
|
10
|
+
//
|
|
11
|
+
// Set streaming_mode:false ONLY for known-small reads — it buffers the full
|
|
12
|
+
// result set into output.rows (still read via the streaming cursor internally).
|
|
13
|
+
//
|
|
14
|
+
// modes:
|
|
15
|
+
// 'table' → SELECT * FROM <table> (input.table required)
|
|
16
|
+
// 'query' → raw input.sql (+ input.params) (input.sql required)
|
|
17
|
+
// Temp file: XEPLR_ACTIONS_TMP_DIR (default os.tmpdir()/xeplr-actions),
|
|
18
|
+
// named <occurrenceId>_db-fetch_<seq>.jsonl. Cleanup: none in-action — rely
|
|
19
|
+
// on the sweep-temp-files action (or db-push after it consumes the file).
|
|
20
|
+
|
|
21
|
+
var dbDrivers = require('../../drivers/db');
|
|
22
|
+
var { generateId } = require('@xeplr/utils/lib/helpers');
|
|
23
|
+
var os = require('os');
|
|
24
|
+
var path = require('path');
|
|
25
|
+
var fs = require('fs');
|
|
26
|
+
var fsp = require('fs/promises');
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
name: 'db-fetch',
|
|
30
|
+
description: 'Stream rows out of a source DB table or query. Streaming by default: ' +
|
|
31
|
+
'spools to an NDJSON file (output.filePath) consumable by db-push. ' +
|
|
32
|
+
'Routes to the driver named by input.dbType.',
|
|
33
|
+
requires: [], // per-driver requires checked at runtime once dbType is known
|
|
34
|
+
|
|
35
|
+
inputSchema: [
|
|
36
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
37
|
+
description: 'postgres | mysql | mssql' },
|
|
38
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
39
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
40
|
+
{ name: 'mode', type: 'string', default: 'table', order: 3,
|
|
41
|
+
description: '"table" (SELECT * FROM input.table) or "query" (raw input.sql)' },
|
|
42
|
+
{ name: 'table', type: 'string', order: 4,
|
|
43
|
+
description: 'Source table name (mode="table").' },
|
|
44
|
+
{ name: 'sql', type: 'string', order: 5,
|
|
45
|
+
description: 'Raw SQL to stream (mode="query").' },
|
|
46
|
+
{ name: 'params', type: 'array', order: 6,
|
|
47
|
+
description: 'Bind params for mode="query".' },
|
|
48
|
+
{ name: 'streaming_mode', type: 'boolean', default: true, order: 7,
|
|
49
|
+
description: 'Default TRUE → spool to NDJSON file. false → inline output.rows (small reads only).' },
|
|
50
|
+
{ name: 'batchSize', type: 'number', default: 1000, order: 8,
|
|
51
|
+
description: 'Rows pulled per cursor round-trip / stream highWaterMark.' }
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
execute: async function(ctx) {
|
|
55
|
+
var input = ctx.input || {};
|
|
56
|
+
var system = ctx.system || {};
|
|
57
|
+
|
|
58
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
59
|
+
dbDrivers.checkDriverRequires('db-fetch', input.dbType, driver);
|
|
60
|
+
if (typeof driver.fetchStream !== 'function') {
|
|
61
|
+
throw new Error('db-fetch: driver "' + input.dbType + '" does not implement fetchStream yet');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var built = buildFetchSql(driver, input);
|
|
65
|
+
var streaming = input.streaming_mode !== false; // default TRUE
|
|
66
|
+
var pool = await driver.connect(input.connection);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
var rowStream = driver.fetchStream(pool, {
|
|
70
|
+
sql: built.sql, params: built.params, batchSize: input.batchSize || 1000
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!streaming) {
|
|
74
|
+
var rows = [];
|
|
75
|
+
for await (var row of rowStream) rows.push(row);
|
|
76
|
+
return { rows: rows, rowCount: rows.length, streaming: false };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var filePath = await resolveOutputPath(system);
|
|
80
|
+
var stats = await writeNDJSON(filePath, rowStream);
|
|
81
|
+
return {
|
|
82
|
+
filePath: filePath,
|
|
83
|
+
format: 'jsonl',
|
|
84
|
+
bytes: stats.bytes,
|
|
85
|
+
rows: stats.rows,
|
|
86
|
+
streaming: true
|
|
87
|
+
};
|
|
88
|
+
} finally {
|
|
89
|
+
await driver.close(pool);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ─── helpers ────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function buildFetchSql(driver, input) {
|
|
97
|
+
var mode = input.mode || (input.table ? 'table' : 'query');
|
|
98
|
+
if (mode === 'table') {
|
|
99
|
+
if (!input.table) throw new Error('db-fetch: mode "table" requires input.table');
|
|
100
|
+
return { sql: 'SELECT * FROM ' + driver.quoteIdent(input.table), params: [] };
|
|
101
|
+
}
|
|
102
|
+
if (mode === 'query') {
|
|
103
|
+
if (!input.sql) throw new Error('db-fetch: mode "query" requires input.sql');
|
|
104
|
+
return { sql: input.sql, params: input.params || [] };
|
|
105
|
+
}
|
|
106
|
+
throw new Error('db-fetch: unsupported mode "' + mode + '" (use "table" or "query")');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async function resolveOutputPath(system) {
|
|
111
|
+
var dir = process.env.XEPLR_ACTIONS_TMP_DIR || path.join(os.tmpdir(), 'xeplr-actions');
|
|
112
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
113
|
+
var base = (system.occurrenceId || generateId()) + '_db-fetch_' + generateId() + '.jsonl';
|
|
114
|
+
return path.join(dir, base);
|
|
115
|
+
}
|
|
116
|
+
// Write an async iterable of rows to an NDJSON file, honoring write
|
|
117
|
+
// backpressure (await 'drain' when the OS buffer is full). One JSON object
|
|
118
|
+
// per line — the exact shape db-push's filePath reader expects.
|
|
119
|
+
async function writeNDJSON(filePath, rowIterable) {
|
|
120
|
+
var ws = fs.createWriteStream(filePath, { encoding: 'utf8' });
|
|
121
|
+
var rows = 0;
|
|
122
|
+
var bytes = 0;
|
|
123
|
+
try {
|
|
124
|
+
for await (var row of rowIterable) {
|
|
125
|
+
var line = JSON.stringify(row) + '\n';
|
|
126
|
+
bytes += Buffer.byteLength(line);
|
|
127
|
+
rows++;
|
|
128
|
+
if (!ws.write(line)) {
|
|
129
|
+
await new Promise(function(resolve, reject) {
|
|
130
|
+
ws.once('drain', resolve);
|
|
131
|
+
ws.once('error', reject);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
await new Promise(function(resolve, reject) {
|
|
137
|
+
ws.end(resolve);
|
|
138
|
+
ws.once('error', reject);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { rows: rows, bytes: bytes };
|
|
142
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// db-list-columns action — wraps each driver's existing getTableSchema
|
|
2
|
+
// (already used internally by uploader.reconcile) as a standalone action:
|
|
3
|
+
// column name + data type for one named table.
|
|
4
|
+
|
|
5
|
+
var dbDrivers = require('../../drivers/db');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
name: 'db-list-columns',
|
|
9
|
+
description: 'List a table\'s columns (name + data type). Routes to the driver named by input.dbType.',
|
|
10
|
+
requires: [],
|
|
11
|
+
|
|
12
|
+
inputSchema: [
|
|
13
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
14
|
+
description: 'postgres | mysql | mssql' },
|
|
15
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
16
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
17
|
+
{ name: 'table', type: 'string', required: true, order: 3,
|
|
18
|
+
description: 'Table name to introspect' },
|
|
19
|
+
{ name: 'schema', type: 'string', order: 4,
|
|
20
|
+
description: 'Schema the table lives in (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
execute: async function(ctx) {
|
|
24
|
+
var input = ctx.input || {};
|
|
25
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
26
|
+
dbDrivers.checkDriverRequires('db-list-columns', input.dbType, driver);
|
|
27
|
+
if (typeof driver.getTableSchema !== 'function') {
|
|
28
|
+
throw new Error('db-list-columns: driver "' + input.dbType + '" does not implement getTableSchema yet');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
var pool = await driver.connect(input.connection);
|
|
32
|
+
try {
|
|
33
|
+
var columns = await driver.getTableSchema(pool, input.table, input.schema);
|
|
34
|
+
return { columns: columns };
|
|
35
|
+
} finally {
|
|
36
|
+
await driver.close(pool);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// db-list-procedures action — same shape as db-list-tables, lists stored
|
|
2
|
+
// procedures instead (excludes functions — see each driver's listProcedures).
|
|
3
|
+
|
|
4
|
+
var dbDrivers = require('../../drivers/db');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'db-list-procedures',
|
|
8
|
+
description: 'List stored procedure names in a schema/database. Routes to the driver named by input.dbType.',
|
|
9
|
+
requires: [],
|
|
10
|
+
|
|
11
|
+
inputSchema: [
|
|
12
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
13
|
+
description: 'postgres | mysql | mssql' },
|
|
14
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
15
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
16
|
+
{ name: 'schema', type: 'string', order: 3,
|
|
17
|
+
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
18
|
+
],
|
|
19
|
+
|
|
20
|
+
execute: async function(ctx) {
|
|
21
|
+
var input = ctx.input || {};
|
|
22
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
23
|
+
dbDrivers.checkDriverRequires('db-list-procedures', input.dbType, driver);
|
|
24
|
+
if (typeof driver.listProcedures !== 'function') {
|
|
25
|
+
throw new Error('db-list-procedures: driver "' + input.dbType + '" does not implement listProcedures yet');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var pool = await driver.connect(input.connection);
|
|
29
|
+
try {
|
|
30
|
+
var procedures = await driver.listProcedures(pool, input.schema);
|
|
31
|
+
return { procedures: procedures };
|
|
32
|
+
} finally {
|
|
33
|
+
await driver.close(pool);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// db-list-tables action — routes to getDriver(input.dbType) and lists base
|
|
2
|
+
// tables (never views) visible to the given schema/database. Small metadata
|
|
3
|
+
// read, no streaming — parity with db-fetch/db-push on connection lifecycle
|
|
4
|
+
// only (connect, use, close).
|
|
5
|
+
|
|
6
|
+
var dbDrivers = require('../../drivers/db');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
name: 'db-list-tables',
|
|
10
|
+
description: 'List base table names in a schema/database. Routes to the driver named by input.dbType.',
|
|
11
|
+
requires: [], // per-driver requires checked at runtime once dbType is known
|
|
12
|
+
|
|
13
|
+
inputSchema: [
|
|
14
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
15
|
+
description: 'postgres | mysql | mssql' },
|
|
16
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
17
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
18
|
+
{ name: 'schema', type: 'string', order: 3,
|
|
19
|
+
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
execute: async function(ctx) {
|
|
23
|
+
var input = ctx.input || {};
|
|
24
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
25
|
+
dbDrivers.checkDriverRequires('db-list-tables', input.dbType, driver);
|
|
26
|
+
if (typeof driver.listTables !== 'function') {
|
|
27
|
+
throw new Error('db-list-tables: driver "' + input.dbType + '" does not implement listTables yet');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var pool = await driver.connect(input.connection);
|
|
31
|
+
try {
|
|
32
|
+
var tables = await driver.listTables(pool, input.schema);
|
|
33
|
+
return { tables: tables };
|
|
34
|
+
} finally {
|
|
35
|
+
await driver.close(pool);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// db-list-views action — same shape as db-list-tables, lists views instead.
|
|
2
|
+
|
|
3
|
+
var dbDrivers = require('../../drivers/db');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'db-list-views',
|
|
7
|
+
description: 'List view names in a schema/database. Routes to the driver named by input.dbType.',
|
|
8
|
+
requires: [],
|
|
9
|
+
|
|
10
|
+
inputSchema: [
|
|
11
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
12
|
+
description: 'postgres | mysql | mssql' },
|
|
13
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
14
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
15
|
+
{ name: 'schema', type: 'string', order: 3,
|
|
16
|
+
description: 'Schema to list (postgres/mssql — default public/all; mysql ignores this in favor of connection.database)' }
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
execute: async function(ctx) {
|
|
20
|
+
var input = ctx.input || {};
|
|
21
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
22
|
+
dbDrivers.checkDriverRequires('db-list-views', input.dbType, driver);
|
|
23
|
+
if (typeof driver.listViews !== 'function') {
|
|
24
|
+
throw new Error('db-list-views: driver "' + input.dbType + '" does not implement listViews yet');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var pool = await driver.connect(input.connection);
|
|
28
|
+
try {
|
|
29
|
+
var views = await driver.listViews(pool, input.schema);
|
|
30
|
+
return { views: views };
|
|
31
|
+
} finally {
|
|
32
|
+
await driver.close(pool);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// db-push action — pushes rows into a target DB table.
|
|
2
|
+
//
|
|
3
|
+
// Composes: drivers.db.<type> + uploader.upload + SqlQueue
|
|
4
|
+
//
|
|
5
|
+
// Inputs accept EITHER `rows` (inline) OR `filePath` (NDJSON, one row per line
|
|
6
|
+
// — matches spool()'s output shape). The action creates its own SqlQueue per
|
|
7
|
+
// call and tears it down when done; movement lifecycle is fully self-contained.
|
|
8
|
+
|
|
9
|
+
var { upload } = require('../../uploader');
|
|
10
|
+
var dbDrivers = require('../../drivers/db');
|
|
11
|
+
var { SqlQueue } = require('@xeplr/utils/lib/queue');
|
|
12
|
+
var { generateId } = require('@xeplr/utils/lib/helpers');
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
var readline = require('readline');
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
name: 'db-push',
|
|
18
|
+
description: 'Push rows into a target DB table. Supports UPSERT via primaryKeys, ' +
|
|
19
|
+
'NDJSON streaming via filePath, movement-scoped rollback, and error-table ' +
|
|
20
|
+
'routing on failure. Routes to the driver named by input.dbType.',
|
|
21
|
+
requires: [], // per-driver requires checked at runtime once dbType is known
|
|
22
|
+
|
|
23
|
+
inputSchema: [
|
|
24
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 1,
|
|
25
|
+
description: 'postgres | mysql | mssql | mongo' },
|
|
26
|
+
{ name: 'connection', type: 'object', required: true, order: 2,
|
|
27
|
+
description: 'Connection config: { host, port, user, password, database }' },
|
|
28
|
+
{ name: 'targetTable', type: 'string', required: true, order: 3 },
|
|
29
|
+
{ name: 'primaryKeys', type: 'array', order: 4,
|
|
30
|
+
description: 'Column names for UPSERT. Empty/omitted = plain INSERT.' },
|
|
31
|
+
{ name: 'rows', type: 'array', order: 5,
|
|
32
|
+
description: 'Inline rows (mutually exclusive with filePath).' },
|
|
33
|
+
{ name: 'filePath', type: 'string', order: 6,
|
|
34
|
+
description: 'Path to an NDJSON file (one row per line). Use for large uploads.' },
|
|
35
|
+
{ name: 'movementId', type: 'string', order: 7,
|
|
36
|
+
description: 'Correlation key for rollback. Defaults to system.occurrenceId or a fresh id.' },
|
|
37
|
+
{ name: 'batchSize', type: 'number', default: 5000, order: 8 },
|
|
38
|
+
{ name: 'concurrency', type: 'number', default: 4, order: 9 }
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
execute: async function(ctx) {
|
|
42
|
+
var input = ctx.input || {};
|
|
43
|
+
var system = ctx.system || {};
|
|
44
|
+
|
|
45
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
46
|
+
dbDrivers.checkDriverRequires('db-push', input.dbType, driver);
|
|
47
|
+
|
|
48
|
+
var movementId = input.movementId || system.occurrenceId || ('mv_' + generateId());
|
|
49
|
+
var source = resolveSource(input);
|
|
50
|
+
|
|
51
|
+
var pool = await driver.connect(input.connection);
|
|
52
|
+
var connectionName = input.targetTable;
|
|
53
|
+
var connections = {};
|
|
54
|
+
connections[connectionName] = pool;
|
|
55
|
+
|
|
56
|
+
var queue = new SqlQueue({
|
|
57
|
+
connections: connections,
|
|
58
|
+
// Route through driver.query, not conn.query directly: each driver
|
|
59
|
+
// normalizes the result shape AND binds params dialect-correctly
|
|
60
|
+
// (pg/mysql take a positional array; mssql needs request.input @p0..@pN).
|
|
61
|
+
executor: async function(item, conn) { await driver.query(conn, item.sql, item.params || []); },
|
|
62
|
+
concurrency: input.concurrency || 4,
|
|
63
|
+
maxAttempts: 3
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await upload({
|
|
68
|
+
source: source,
|
|
69
|
+
driver: driver,
|
|
70
|
+
connection: pool,
|
|
71
|
+
targetTable: input.targetTable,
|
|
72
|
+
primaryKeys: (input.primaryKeys && input.primaryKeys.length) ? input.primaryKeys : null,
|
|
73
|
+
movementId: movementId,
|
|
74
|
+
queue: queue,
|
|
75
|
+
connectionName: connectionName,
|
|
76
|
+
dbType: input.dbType, // recorded in import_meta
|
|
77
|
+
// Movement metadata persists to xeplr_config when the framework supplies
|
|
78
|
+
// a meta store (meta-store-knex bound to xeplr_config); else no-op.
|
|
79
|
+
metaStore: ctx.metaStore,
|
|
80
|
+
batchSize: input.batchSize
|
|
81
|
+
});
|
|
82
|
+
} finally {
|
|
83
|
+
queue.stop();
|
|
84
|
+
await driver.close(pool);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Return an async iterable for whichever input shape the caller supplied.
|
|
90
|
+
function resolveSource(input) {
|
|
91
|
+
if (Array.isArray(input.rows) && input.rows.length > 0) return asyncFromArray(input.rows);
|
|
92
|
+
if (input.filePath) return asyncFromNDJSONFile(input.filePath);
|
|
93
|
+
throw new Error('db-push: either input.rows (non-empty) or input.filePath is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function* asyncFromArray(rows) {
|
|
97
|
+
for (var i = 0; i < rows.length; i++) yield rows[i];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function* asyncFromNDJSONFile(filePath) {
|
|
101
|
+
var stream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
|
102
|
+
var rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
103
|
+
for await (var line of rl) {
|
|
104
|
+
if (line.trim().length === 0) continue;
|
|
105
|
+
yield JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// file-move action — copies/moves files or entire directories from one
|
|
2
|
+
// location to another. Source + target can each be local/sharepoint/google
|
|
3
|
+
// (i.e. it uses getSource(sourceType) on both ends).
|
|
4
|
+
//
|
|
5
|
+
// Streaming: always streamed when files are large. Input flag:
|
|
6
|
+
// { operation: 'copy' | 'move', overwrite: boolean }
|
|
7
|
+
//
|
|
8
|
+
// Implementation pending user spec.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// file-upload action — read from a file source (local / sftp / sharepoint /
|
|
2
|
+
// google), parse via a format (csv / excel / txt / json), push to a target DB.
|
|
3
|
+
//
|
|
4
|
+
// The full pipe: [source stream] → [format parser] → [rows async iterable]
|
|
5
|
+
// → [uploader.upload → SqlQueue → DB]
|
|
6
|
+
//
|
|
7
|
+
// Composed from primitives that are all separately usable — a consumer that
|
|
8
|
+
// just wants "give me rows from this CSV without touching a DB" can call
|
|
9
|
+
// getFormat('csv').parseStream(fs.createReadStream(path)) directly.
|
|
10
|
+
|
|
11
|
+
var { upload } = require('../../uploader');
|
|
12
|
+
var fileSources = require('../../drivers/file');
|
|
13
|
+
var formats = require('../../formats');
|
|
14
|
+
var dbDrivers = require('../../drivers/db');
|
|
15
|
+
var { SqlQueue } = require('@xeplr/utils/lib/queue');
|
|
16
|
+
var { generateId } = require('@xeplr/utils/lib/helpers');
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
name: 'file-upload',
|
|
20
|
+
description: 'Read a file (CSV / Excel / JSON / TXT) from a source ' +
|
|
21
|
+
'(local / sftp / sharepoint / google) and push rows into a target DB table. ' +
|
|
22
|
+
'Types + columns are inferred from the first N rows.',
|
|
23
|
+
requires: [], // per-source/format/driver checks happen at runtime
|
|
24
|
+
|
|
25
|
+
inputSchema: [
|
|
26
|
+
// SOURCE
|
|
27
|
+
{ name: 'sourceType', type: 'string', required: true, default: 'local', order: 1,
|
|
28
|
+
description: 'local | sftp | sharepoint | google' },
|
|
29
|
+
{ name: 'sourcePath', type: 'string', required: true, order: 2,
|
|
30
|
+
description: 'File path (local) or key/id (remote sources)' },
|
|
31
|
+
{ name: 'sourceConfig', type: 'object', default: {}, order: 3,
|
|
32
|
+
description: 'Source-specific config (SFTP creds, SharePoint site, etc.)' },
|
|
33
|
+
|
|
34
|
+
// FORMAT
|
|
35
|
+
{ name: 'format', type: 'string', required: true, default: 'csv', order: 4,
|
|
36
|
+
description: 'csv | excel | txt | json' },
|
|
37
|
+
{ name: 'formatConfig', type: 'object', default: {}, order: 5,
|
|
38
|
+
description: 'Format-specific options (delimiter, sheet name, etc.)' },
|
|
39
|
+
|
|
40
|
+
// TARGET
|
|
41
|
+
{ name: 'dbType', type: 'string', required: true, default: 'postgres', order: 10 },
|
|
42
|
+
{ name: 'dbConnection', type: 'object', required: true, order: 11 },
|
|
43
|
+
{ name: 'targetTable', type: 'string', required: true, order: 12 },
|
|
44
|
+
{ name: 'primaryKeys', type: 'array', order: 13 },
|
|
45
|
+
|
|
46
|
+
// CONTROL
|
|
47
|
+
{ name: 'movementId', type: 'string', order: 20 },
|
|
48
|
+
{ name: 'batchSize', type: 'number', default: 5000, order: 21 },
|
|
49
|
+
{ name: 'concurrency', type: 'number', default: 4, order: 22 }
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
execute: async function(ctx) {
|
|
53
|
+
var input = ctx.input || {};
|
|
54
|
+
var system = ctx.system || {};
|
|
55
|
+
|
|
56
|
+
// Resolve source + format + target driver up front so any missing peer
|
|
57
|
+
// dep throws with a clear message before we start touching resources.
|
|
58
|
+
var source = fileSources.getSource(input.sourceType);
|
|
59
|
+
fileSources.checkSourceRequires('file-upload', input.sourceType, source);
|
|
60
|
+
|
|
61
|
+
var format = formats.getFormat(input.format);
|
|
62
|
+
formats.checkFormatRequires('file-upload', input.format, format);
|
|
63
|
+
|
|
64
|
+
var driver = dbDrivers.getDriver(input.dbType);
|
|
65
|
+
dbDrivers.checkDriverRequires('file-upload', input.dbType, driver);
|
|
66
|
+
|
|
67
|
+
// Some formats need a stream (csv, json, txt — pure pipe semantics);
|
|
68
|
+
// some need a file path (xlsx — the zip central directory is at the file
|
|
69
|
+
// tail so unzipper must seek). The format declares which via `inputMode`;
|
|
70
|
+
// sources implement whichever their transport can serve.
|
|
71
|
+
var sourceCfg = Object.assign({}, input.sourceConfig, { path: input.sourcePath });
|
|
72
|
+
var rowsIterable;
|
|
73
|
+
if (format.inputMode === 'path') {
|
|
74
|
+
if (typeof source.openPath !== 'function') {
|
|
75
|
+
throw new Error("file-upload: format '" + input.format + "' requires a file path but source '" +
|
|
76
|
+
input.sourceType + "' does not implement openPath(). Add openPath() to the source " +
|
|
77
|
+
"(local sources should return config.path; remote sources should download to a tmp path).");
|
|
78
|
+
}
|
|
79
|
+
var filePath = await source.openPath(sourceCfg);
|
|
80
|
+
rowsIterable = format.parsePath(filePath, input.formatConfig || {});
|
|
81
|
+
} else {
|
|
82
|
+
var stream = await source.openStream(sourceCfg);
|
|
83
|
+
rowsIterable = format.parseStream(stream, input.formatConfig || {});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set up target
|
|
87
|
+
var movementId = input.movementId || system.occurrenceId || ('mv_' + generateId());
|
|
88
|
+
var pool = await driver.connect(input.dbConnection);
|
|
89
|
+
var connections = {};
|
|
90
|
+
connections[input.targetTable] = pool;
|
|
91
|
+
|
|
92
|
+
var queue = new SqlQueue({
|
|
93
|
+
connections: connections,
|
|
94
|
+
executor: async function(item, conn) { await conn.query(item.sql, item.params || []); },
|
|
95
|
+
concurrency: input.concurrency || 4,
|
|
96
|
+
maxAttempts: 3
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
return await upload({
|
|
101
|
+
source: rowsIterable,
|
|
102
|
+
driver: driver,
|
|
103
|
+
connection: pool,
|
|
104
|
+
targetTable: input.targetTable,
|
|
105
|
+
primaryKeys: (input.primaryKeys && input.primaryKeys.length) ? input.primaryKeys : null,
|
|
106
|
+
movementId: movementId,
|
|
107
|
+
queue: queue,
|
|
108
|
+
connectionName: input.targetTable,
|
|
109
|
+
dbType: input.dbType, // recorded in import_meta
|
|
110
|
+
batchSize: input.batchSize,
|
|
111
|
+
// Movement metadata persists to xeplr_configs when the caller supplies
|
|
112
|
+
// a meta store (meta-store-knex); else no-op. mtId1-4/details are
|
|
113
|
+
// opaque tenant/app context — see runner.js's ctx.system passthrough.
|
|
114
|
+
metaStore: ctx.metaStore,
|
|
115
|
+
mtId1: system.mtId1, mtId2: system.mtId2, mtId3: system.mtId3, mtId4: system.mtId4,
|
|
116
|
+
details: system.details
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
queue.stop();
|
|
120
|
+
await driver.close(pool);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Built-in actions bundled with @xeplr/actions.
|
|
2
|
+
// Each entry is a module — registerAction(mod) picks up the name from mod.name.
|
|
3
|
+
// Placeholders throw at register/run time until specced.
|
|
4
|
+
//
|
|
5
|
+
// Streaming convention (uniform across db/ and file/ actions):
|
|
6
|
+
// - Inputs may include `streaming_mode: boolean` (default false).
|
|
7
|
+
// - When true, reads write to a temp file (rotating NDJSON for db rows,
|
|
8
|
+
// raw for files) and return { filePath | dirPath, format, bytes, ... }
|
|
9
|
+
// in the output.
|
|
10
|
+
// - When false, data stays inline in output.rows / output.content.
|
|
11
|
+
// - Writes may accept EITHER `rows: []` OR a spooled `filePath`/`dirPath`.
|
|
12
|
+
// - Temp dir: XEPLR_ACTIONS_TMP_DIR env var, default os.tmpdir() + '/xeplr-actions/'.
|
|
13
|
+
//
|
|
14
|
+
// Uploader primitive (lib/uploader/) handles DDL, type inference, TZ, batch
|
|
15
|
+
// bisection, and error-table routing. The DB actions here compose it with
|
|
16
|
+
// the appropriate driver.
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
// Generic actions (no runtime-config dependency)
|
|
20
|
+
httpRequest: require('./http-request'),
|
|
21
|
+
spawnProgram: require('./spawn-program'),
|
|
22
|
+
sendEmail: require('./send-email'),
|
|
23
|
+
|
|
24
|
+
// DB actions — factory-shaped (call fileUpload({ metaKnex }) to get a
|
|
25
|
+
// registerable module) so the Knex handle for the meta store binds at
|
|
26
|
+
// configuration time, not runtime.
|
|
27
|
+
dbFetch: require('./db/fetch'),
|
|
28
|
+
dbPush: require('./db/push'),
|
|
29
|
+
dbQuery: require('./db/query'),
|
|
30
|
+
|
|
31
|
+
// DB introspection — list tables/views/procedures, or one table's columns.
|
|
32
|
+
dbListTables: require('./db/list-tables'),
|
|
33
|
+
dbListViews: require('./db/list-views'),
|
|
34
|
+
dbListProcedures: require('./db/list-procedures'),
|
|
35
|
+
dbListColumns: require('./db/list-columns'),
|
|
36
|
+
|
|
37
|
+
// File actions — factory-shaped for the same reason (meta store binding).
|
|
38
|
+
fileUpload: require('./file/upload'),
|
|
39
|
+
fileMove: require('./file/move')
|
|
40
|
+
};
|