@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/index.js +51 -0
  3. package/lib/builtins/db/fetch.js +142 -0
  4. package/lib/builtins/db/list-columns.js +39 -0
  5. package/lib/builtins/db/list-procedures.js +36 -0
  6. package/lib/builtins/db/list-tables.js +38 -0
  7. package/lib/builtins/db/list-views.js +35 -0
  8. package/lib/builtins/db/push.js +107 -0
  9. package/lib/builtins/db/query.js +5 -0
  10. package/lib/builtins/file/move.js +8 -0
  11. package/lib/builtins/file/upload.js +123 -0
  12. package/lib/builtins/http-request.js +3 -0
  13. package/lib/builtins/index.js +40 -0
  14. package/lib/builtins/send-email.js +8 -0
  15. package/lib/builtins/spawn-program.js +4 -0
  16. package/lib/drivers/db/index.js +41 -0
  17. package/lib/drivers/db/mongo.js +8 -0
  18. package/lib/drivers/db/mssql.js +464 -0
  19. package/lib/drivers/db/mysql.js +425 -0
  20. package/lib/drivers/db/postgres.js +372 -0
  21. package/lib/drivers/file/google.js +8 -0
  22. package/lib/drivers/file/index.js +42 -0
  23. package/lib/drivers/file/local.js +68 -0
  24. package/lib/drivers/file/sftp.js +9 -0
  25. package/lib/drivers/file/sharepoint.js +9 -0
  26. package/lib/errors.js +37 -0
  27. package/lib/formats/_xlsx-stream.js +355 -0
  28. package/lib/formats/csv.js +54 -0
  29. package/lib/formats/excel.js +87 -0
  30. package/lib/formats/index.js +38 -0
  31. package/lib/formats/json.js +42 -0
  32. package/lib/formats/txt.js +6 -0
  33. package/lib/registry.js +75 -0
  34. package/lib/runner.js +81 -0
  35. package/lib/streaming/checkpoint.js +46 -0
  36. package/lib/streaming/index.js +10 -0
  37. package/lib/streaming/resume.js +35 -0
  38. package/lib/streaming/spool.js +253 -0
  39. package/lib/streaming/utils.js +82 -0
  40. package/lib/uploader/attach.js +69 -0
  41. package/lib/uploader/index.js +298 -0
  42. package/lib/uploader/infer.js +76 -0
  43. package/lib/uploader/meta-store-knex.js +136 -0
  44. package/lib/uploader/meta-store-noop.js +11 -0
  45. package/lib/uploader/reconcile.js +88 -0
  46. 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,5 @@
1
+ // db-query action — routes to getDriver(input.dbType) and runs a raw
2
+ // query or procedure. Streaming semantics identical to db-fetch:
3
+ // streaming_mode=true → output.filePath instead of output.rows.
4
+ //
5
+ // Implementation pending user spec.
@@ -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,3 @@
1
+ // http-request action — generic HTTP client with optional auth pre-call.
2
+ // Uses Node's built-in fetch (Node 18+). No peer deps.
3
+ // Implementation pending user spec.
@@ -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
+ };