@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
@@ -0,0 +1,372 @@
1
+ // Postgres driver — thin. SqlQueue owns retries + bisection + abort,
2
+ // so the driver just does: connect / query / close, and provides SQL
3
+ // builders the uploader composes into batch INSERT statements.
4
+ //
5
+ // Peer dep: pg (declared in @xeplr/actions/package.json as optional).
6
+ //
7
+ // Interface every db driver implements:
8
+ //
9
+ // requires: ['pg']
10
+ // async connect(config) → pool
11
+ // async query(pool, sql, params?) → { rows, rowCount, ... }
12
+ // async close(pool) → void
13
+ //
14
+ // buildCreateTableSql(tableName, columns, primaryKeys?) → string
15
+ // buildCreateErrorTableSql(tableName) → string
16
+ // buildAlterTableAddSql(tableName, columns) → string[]
17
+ // buildInsertSql(tableName, rows, columns, movementId, primaryKeys?)
18
+ // → { sql, params }
19
+ // async rollbackMovement(pool, tableName, movementId) → { mainDeleted, errorDeleted }
20
+ //
21
+ // Type mapping — deliberately narrow to avoid width/precision footguns:
22
+ // string → TEXT
23
+ // number → NUMERIC (integer + float both handled)
24
+ // boolean → BOOLEAN
25
+ // date → TIMESTAMPTZ (user rule: "always datetime, never date-only")
26
+ // datetime→ TIMESTAMPTZ
27
+ // object → JSONB
28
+ // array → JSONB
29
+
30
+ var TYPE_MAP = {
31
+ string: 'TEXT',
32
+ number: 'NUMERIC',
33
+ boolean: 'BOOLEAN',
34
+ date: 'TIMESTAMPTZ',
35
+ datetime: 'TIMESTAMPTZ',
36
+ object: 'JSONB',
37
+ array: 'JSONB'
38
+ };
39
+
40
+ // Framework-managed columns on every uploader-created table.
41
+ var INTERNAL_ID_COL = '__xeplr_id__';
42
+ var INTERNAL_MOVEMENT_COL = '__xeplr_movement_id__';
43
+
44
+ async function connect(config) {
45
+ var pg = require('pg');
46
+ var pool = new pg.Pool({
47
+ host: config.host || 'localhost',
48
+ port: config.port || 5432,
49
+ user: config.user,
50
+ password: config.password,
51
+ database: config.database,
52
+ max: config.maxConnections || 10
53
+ });
54
+ return pool;
55
+ }
56
+
57
+ async function close(pool) {
58
+ if (pool) await pool.end();
59
+ }
60
+
61
+ async function query(pool, sql, params) {
62
+ return pool.query(sql, params || []);
63
+ }
64
+
65
+ function toPgType(type) { return TYPE_MAP[type] || 'TEXT'; }
66
+
67
+ // Double-quote a Postgres identifier, escaping embedded quotes.
68
+ function quoteIdent(name) {
69
+ return '"' + String(name).replace(/"/g, '""') + '"';
70
+ }
71
+
72
+ // Build the CREATE TABLE IF NOT EXISTS statement.
73
+ // columns — [{ name, type }]
74
+ // primaryKeys — optional; realized as a UNIQUE INDEX (see buildUpsertIndexSql)
75
+ // so __xeplr_id__ stays the framework PK.
76
+ function buildCreateTableSql(tableName, columns, _primaryKeys) {
77
+ var defs = [];
78
+ defs.push(quoteIdent(INTERNAL_ID_COL) + ' BIGSERIAL PRIMARY KEY');
79
+ defs.push(quoteIdent(INTERNAL_MOVEMENT_COL) + ' TEXT NOT NULL');
80
+ for (var i = 0; i < columns.length; i++) {
81
+ var c = columns[i];
82
+ defs.push(quoteIdent(c.name) + ' ' + toPgType(c.type));
83
+ }
84
+ return 'CREATE TABLE IF NOT EXISTS ' + quoteIdent(tableName) +
85
+ ' (\n ' + defs.join(',\n ') + '\n)';
86
+ }
87
+
88
+ function buildCreateErrorTableSql(tableName) {
89
+ var errTable = tableName + '_import_errors';
90
+ return 'CREATE TABLE IF NOT EXISTS ' + quoteIdent(errTable) + ' (\n' +
91
+ ' ' + quoteIdent(INTERNAL_ID_COL) + ' BIGSERIAL PRIMARY KEY,\n' +
92
+ ' movement_id TEXT NOT NULL,\n' +
93
+ ' row_num INTEGER,\n' +
94
+ ' error_description TEXT,\n' +
95
+ ' underlying_sql TEXT,\n' +
96
+ ' raw_row JSONB,\n' +
97
+ ' recorded_at TIMESTAMPTZ DEFAULT NOW()\n' +
98
+ ')';
99
+ }
100
+
101
+ // Optional UNIQUE INDEX for UPSERT conflict-target when primaryKeys given.
102
+ function buildUpsertIndexSql(tableName, primaryKeys) {
103
+ if (!primaryKeys || primaryKeys.length === 0) return null;
104
+ var idxName = tableName + '_upsert_uniq';
105
+ var cols = primaryKeys.map(quoteIdent).join(', ');
106
+ return 'CREATE UNIQUE INDEX IF NOT EXISTS ' + quoteIdent(idxName) +
107
+ ' ON ' + quoteIdent(tableName) + ' (' + cols + ')';
108
+ }
109
+
110
+ // One ALTER TABLE ADD COLUMN IF NOT EXISTS per new column — idempotent.
111
+ function buildAlterTableAddSql(tableName, columns) {
112
+ return columns.map(function(c) {
113
+ return 'ALTER TABLE ' + quoteIdent(tableName) +
114
+ ' ADD COLUMN IF NOT EXISTS ' + quoteIdent(c.name) + ' ' + toPgType(c.type);
115
+ });
116
+ }
117
+
118
+ // Bulk INSERT for a batch of rows. Emits parameterized SQL — values are
119
+ // never concatenated into the SQL string. Auto-adds __xeplr_movement_id__
120
+ // to every row. UPSERT if primaryKeys supplied.
121
+ function buildInsertSql(tableName, rows, columns, movementId, primaryKeys) {
122
+ var colNames = columns.map(function(c) { return c.name; });
123
+ var allCols = [INTERNAL_MOVEMENT_COL].concat(colNames);
124
+
125
+ var params = [];
126
+ var groups = [];
127
+ var idx = 1;
128
+
129
+ for (var r = 0; r < rows.length; r++) {
130
+ var row = rows[r];
131
+ var placeholders = ['$' + idx];
132
+ params.push(movementId); idx++;
133
+ for (var c = 0; c < colNames.length; c++) {
134
+ placeholders.push('$' + idx);
135
+ params.push(coerceValue(row[colNames[c]], columns[c].type));
136
+ idx++;
137
+ }
138
+ groups.push('(' + placeholders.join(',') + ')');
139
+ }
140
+
141
+ var sql = 'INSERT INTO ' + quoteIdent(tableName) +
142
+ ' (' + allCols.map(quoteIdent).join(', ') + ') VALUES ' + groups.join(',');
143
+
144
+ if (primaryKeys && primaryKeys.length > 0) {
145
+ // Non-PK user columns get updated. __xeplr_movement_id__ ALWAYS updates
146
+ // on conflict — otherwise the latest movement can silently modify a row
147
+ // it doesn't own on paper, and rollback would miss it.
148
+ var updateCols = colNames.filter(function(c) { return primaryKeys.indexOf(c) === -1; });
149
+ var setColumns = updateCols.concat([INTERNAL_MOVEMENT_COL]);
150
+ var setClause = setColumns.map(function(c) {
151
+ return quoteIdent(c) + ' = EXCLUDED.' + quoteIdent(c);
152
+ }).join(', ');
153
+ sql += ' ON CONFLICT (' + primaryKeys.map(quoteIdent).join(', ') +
154
+ ') DO UPDATE SET ' + setClause;
155
+ }
156
+
157
+ return { sql: sql, params: params };
158
+ }
159
+
160
+ // Coerce a JS value to the target column's effective type. The uploader
161
+ // passes the RECONCILED column type here (target's type wins), so this
162
+ // function's job is to widen/narrow the JS value to fit.
163
+ //
164
+ // Failures aren't thrown — mismatched values (e.g. "abc" going into a
165
+ // number column) pass through unchanged, PG rejects them, and SqlQueue's
166
+ // bisection isolates the offending row into the error table.
167
+ function coerceValue(v, type) {
168
+ if (v === undefined || v === null) return null;
169
+
170
+ switch (type) {
171
+ case 'string':
172
+ // "String wins" widening: TEXT columns accept everything.
173
+ if (v instanceof Date) return v.toISOString();
174
+ if (Array.isArray(v)) return JSON.stringify(v);
175
+ if (typeof v === 'object') return JSON.stringify(v);
176
+ return String(v);
177
+
178
+ case 'number':
179
+ if (typeof v === 'number') return v;
180
+ if (typeof v === 'string') {
181
+ var n = Number(v);
182
+ return isNaN(n) ? v : n; // non-numeric string → let PG reject
183
+ }
184
+ return v;
185
+
186
+ case 'boolean':
187
+ if (typeof v === 'boolean') return v;
188
+ if (typeof v === 'string') {
189
+ var lower = v.toLowerCase().trim();
190
+ if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'y') return true;
191
+ if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'n') return false;
192
+ }
193
+ if (typeof v === 'number') return v !== 0;
194
+ return v; // let PG reject
195
+
196
+ case 'datetime':
197
+ if (v instanceof Date) return v.toISOString();
198
+ return v; // strings pass through; PG parses
199
+
200
+ case 'object':
201
+ case 'array':
202
+ if (typeof v === 'string') return v; // assume valid JSON string
203
+ return JSON.stringify(v);
204
+
205
+ default:
206
+ return v;
207
+ }
208
+ }
209
+
210
+ // Native data_type → logical type, for reconcile. Shared with the reconcile
211
+ // module so the mapping lives in one place; exposed here so the uploader can
212
+ // call driver.dataTypeToLogical uniformly across all dialects.
213
+ var pgDataTypeToLogical = require('../../uploader/reconcile').pgDataTypeToLogical;
214
+ function dataTypeToLogical(dataType) { return pgDataTypeToLogical(dataType); }
215
+
216
+ // Idempotently ensure __xeplr_movement_id__ exists on a (possibly pre-existing)
217
+ // target table. Same operation the uploader used to inline; kept here so every
218
+ // driver exposes the same async ensure interface.
219
+ async function ensureMovementColumn(pool, tableName) {
220
+ await pool.query(
221
+ 'ALTER TABLE ' + quoteIdent(tableName) +
222
+ ' ADD COLUMN IF NOT EXISTS ' + quoteIdent(INTERNAL_MOVEMENT_COL) + ' TEXT'
223
+ );
224
+ }
225
+
226
+ // Idempotently ensure the UNIQUE index that backs ON CONFLICT.
227
+ async function ensureUpsertIndex(pool, tableName, primaryKeys) {
228
+ var sql = buildUpsertIndexSql(tableName, primaryKeys);
229
+ if (sql) await pool.query(sql);
230
+ }
231
+
232
+ // Query the target table's actual column set. Returns [{name, dataType, udtName}]
233
+ // in ordinal-position order. Empty array if the table doesn't exist.
234
+ async function getTableSchema(pool, tableName, schema) {
235
+ var r = await pool.query(
236
+ "SELECT column_name, data_type, udt_name " +
237
+ "FROM information_schema.columns " +
238
+ "WHERE table_schema = $2 AND table_name = $1 " +
239
+ "ORDER BY ordinal_position",
240
+ [tableName, schema || 'public']
241
+ );
242
+ return r.rows.map(function(row) {
243
+ return { name: row.column_name, dataType: row.data_type, udtName: row.udt_name };
244
+ });
245
+ }
246
+
247
+ // Table/view/procedure listing — used by the db-list-* actions. All three
248
+ // default to the 'public' schema (Postgres's default search_path entry),
249
+ // same fallback getTableSchema already uses.
250
+ async function listTables(pool, schema) {
251
+ var r = await pool.query(
252
+ "SELECT table_name FROM information_schema.tables " +
253
+ "WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name",
254
+ [schema || 'public']
255
+ );
256
+ return r.rows.map(function(row) { return row.table_name; });
257
+ }
258
+
259
+ async function listViews(pool, schema) {
260
+ var r = await pool.query(
261
+ "SELECT table_name FROM information_schema.views " +
262
+ "WHERE table_schema = $1 ORDER BY table_name",
263
+ [schema || 'public']
264
+ );
265
+ return r.rows.map(function(row) { return row.table_name; });
266
+ }
267
+
268
+ // routine_type='PROCEDURE' only (true CREATE PROCEDURE, PG11+) — deliberately
269
+ // excludes FUNCTION so this lines up with what "procedures" means on the
270
+ // other two dialects, which have no separate function/procedure distinction
271
+ // in this listing.
272
+ async function listProcedures(pool, schema) {
273
+ var r = await pool.query(
274
+ "SELECT routine_name FROM information_schema.routines " +
275
+ "WHERE routine_schema = $1 AND routine_type = 'PROCEDURE' ORDER BY routine_name",
276
+ [schema || 'public']
277
+ );
278
+ return r.rows.map(function(row) { return row.routine_name; });
279
+ }
280
+
281
+ // Stream a result set WITHOUT buffering it in memory — the read primitive
282
+ // db-fetch composes. Uses a server-side cursor (DECLARE/FETCH) on a dedicated
283
+ // checked-out client, so PG holds the result set and we pull it `batchSize`
284
+ // rows at a time. The async generator naturally applies backpressure: the next
285
+ // FETCH only fires when the consumer asks for more.
286
+ //
287
+ // opts: { sql, params?, batchSize? } → async iterable<row>
288
+ async function* fetchStream(pool, opts) {
289
+ var batchSize = Math.max(1, parseInt(opts.batchSize || 1000, 10));
290
+ var client = await pool.connect();
291
+ var declared = false;
292
+ try {
293
+ await client.query('BEGIN');
294
+ await client.query('DECLARE __xeplr_fetch_cur NO SCROLL CURSOR FOR ' + opts.sql, opts.params || []);
295
+ declared = true;
296
+ while (true) {
297
+ var res = await client.query('FETCH FORWARD ' + batchSize + ' FROM __xeplr_fetch_cur');
298
+ if (res.rows.length === 0) break;
299
+ for (var i = 0; i < res.rows.length; i++) yield res.rows[i];
300
+ }
301
+ } finally {
302
+ try { if (declared) await client.query('CLOSE __xeplr_fetch_cur'); } catch (_) {}
303
+ try { await client.query('COMMIT'); } catch (_) {}
304
+ client.release();
305
+ }
306
+ }
307
+
308
+ // Delete all rows for a movement across main + error tables.
309
+ // Error table might not exist yet (movement failed before first batch) —
310
+ // swallow those errors silently.
311
+ async function rollbackMovement(pool, tableName, movementId) {
312
+ var errTable = tableName + '_import_errors';
313
+ var result = { mainDeleted: 0, errorDeleted: 0 };
314
+
315
+ var r1 = await pool.query(
316
+ 'DELETE FROM ' + quoteIdent(tableName) +
317
+ ' WHERE ' + quoteIdent(INTERNAL_MOVEMENT_COL) + ' = $1',
318
+ [movementId]
319
+ );
320
+ result.mainDeleted = r1.rowCount || 0;
321
+
322
+ try {
323
+ var r2 = await pool.query(
324
+ 'DELETE FROM ' + quoteIdent(errTable) + ' WHERE movement_id = $1',
325
+ [movementId]
326
+ );
327
+ result.errorDeleted = r2.rowCount || 0;
328
+ } catch (err) {
329
+ if (err.code !== '42P01') throw err; // 42P01 = relation does not exist
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ module.exports = {
336
+ requires: ['pg'],
337
+
338
+ // Connection lifecycle
339
+ connect: connect,
340
+ query: query,
341
+ close: close,
342
+
343
+ // SQL builders (called by uploader — pure functions, no DB access)
344
+ toPgType: toPgType,
345
+ dataTypeToLogical: dataTypeToLogical,
346
+ quoteIdent: quoteIdent,
347
+ buildCreateTableSql: buildCreateTableSql,
348
+ buildCreateErrorTableSql:buildCreateErrorTableSql,
349
+ buildUpsertIndexSql: buildUpsertIndexSql,
350
+ buildAlterTableAddSql: buildAlterTableAddSql,
351
+ buildInsertSql: buildInsertSql,
352
+
353
+ // Idempotent DDL helpers (async — uniform across all dialect drivers)
354
+ ensureMovementColumn: ensureMovementColumn,
355
+ ensureUpsertIndex: ensureUpsertIndex,
356
+
357
+ // Introspection (used by uploader.reconcile + the db-list-* actions)
358
+ getTableSchema: getTableSchema,
359
+ listTables: listTables,
360
+ listViews: listViews,
361
+ listProcedures: listProcedures,
362
+
363
+ // Streaming read primitive (used by db-fetch)
364
+ fetchStream: fetchStream,
365
+
366
+ // Rollback helper
367
+ rollbackMovement: rollbackMovement,
368
+
369
+ // Constants
370
+ INTERNAL_ID_COL: INTERNAL_ID_COL,
371
+ INTERNAL_MOVEMENT_COL: INTERNAL_MOVEMENT_COL
372
+ };
@@ -0,0 +1,8 @@
1
+ // Google Drive source — implementation pending user spec.
2
+ // Peer dep: TBD (candidate: googleapis)
3
+ // Implements the same interface as file/sources/local.js.
4
+ // Config supplies OAuth credentials + folder — treated as a "dbInfo" equivalent.
5
+
6
+ module.exports = {
7
+ requires: []
8
+ };
@@ -0,0 +1,42 @@
1
+ // File source factory. Each action calls getSource(type) to get the right
2
+ // source driver, then invokes the common source interface (see local.js
3
+ // for the contract).
4
+ //
5
+ // Adding a new source type = add a file here + register it below.
6
+
7
+ var SOURCES = {
8
+ local: function() { return require('./local'); },
9
+ sharepoint: function() { return require('./sharepoint'); },
10
+ google: function() { return require('./google'); },
11
+ sftp: function() { return require('./sftp'); }
12
+ };
13
+
14
+ function getSource(type) {
15
+ var loader = SOURCES[type];
16
+ if (!loader) {
17
+ throw new Error('Unknown sourceType: "' + type + '". Supported: ' + Object.keys(SOURCES).join(', '));
18
+ }
19
+ return loader();
20
+ }
21
+
22
+ // Check a source's peer deps and throw a friendly error if any are missing.
23
+ // Called at action execution time (not registration) — the concrete
24
+ // sourceType is only known when the action fires.
25
+ function checkSourceRequires(actionName, sourceType, source) {
26
+ var requires = (source && source.requires) || [];
27
+ for (var i = 0; i < requires.length; i++) {
28
+ try { require.resolve(requires[i]); }
29
+ catch (_) {
30
+ throw new Error(
31
+ actionName + ' requires "' + requires[i] + '" for sourceType="' + sourceType + '". ' +
32
+ 'Install with: npm install ' + requires[i]
33
+ );
34
+ }
35
+ }
36
+ }
37
+
38
+ module.exports = {
39
+ getSource: getSource,
40
+ checkSourceRequires: checkSourceRequires,
41
+ SUPPORTED: Object.keys(SOURCES)
42
+ };
@@ -0,0 +1,68 @@
1
+ // Local filesystem source — thin wrappers over fs. No peer deps.
2
+ //
3
+ // openStream(config) → node stream (Readable)
4
+ // writeFile(config, {content|filePath})
5
+ // listFiles(config) → [{ name, size, mtime }]
6
+ // moveFile(config, { from, to }) → void
7
+ // deleteFile(config) → void
8
+ //
9
+ // `config.path` is the file/dir path — local source config is intentionally
10
+ // minimal since fs handles most concerns natively.
11
+
12
+ var fs = require('fs');
13
+ var fsp = require('fs/promises');
14
+ var path = require('path');
15
+
16
+ module.exports = {
17
+ requires: [],
18
+
19
+ async openStream(config) {
20
+ if (!config || !config.path) throw new Error('local source: config.path is required');
21
+ return fs.createReadStream(config.path, config.encoding ? { encoding: config.encoding } : {});
22
+ },
23
+
24
+ // Formats that need random access (e.g. .xlsx via unzipper — the zip central
25
+ // directory lives at the file tail) request a path instead of a stream.
26
+ // Local: return the path directly. Remote sources (SFTP, Google, etc.)
27
+ // must implement this by downloading to a caller-provided tmp path.
28
+ async openPath(config) {
29
+ if (!config || !config.path) throw new Error('local source: config.path is required');
30
+ return config.path;
31
+ },
32
+
33
+ async writeFile(config, opts) {
34
+ if (!config.path) throw new Error('local source: config.path is required for writeFile');
35
+ if (opts.filePath) {
36
+ await fsp.copyFile(opts.filePath, config.path);
37
+ return { bytes: (await fsp.stat(config.path)).size };
38
+ }
39
+ if (opts.content != null) {
40
+ await fsp.writeFile(config.path, opts.content);
41
+ return { bytes: Buffer.byteLength(opts.content) };
42
+ }
43
+ throw new Error('local source: writeFile requires opts.content or opts.filePath');
44
+ },
45
+
46
+ async listFiles(config) {
47
+ var entries = await fsp.readdir(config.path, { withFileTypes: true });
48
+ var out = [];
49
+ for (var i = 0; i < entries.length; i++) {
50
+ var e = entries[i];
51
+ if (!e.isFile()) continue;
52
+ var full = path.join(config.path, e.name);
53
+ var stat = await fsp.stat(full);
54
+ out.push({ name: e.name, path: full, size: stat.size, mtime: stat.mtime.toISOString() });
55
+ }
56
+ return out;
57
+ },
58
+
59
+ async moveFile(_config, opts) {
60
+ if (!opts || !opts.from || !opts.to) throw new Error('local source: moveFile requires from + to');
61
+ await fsp.rename(opts.from, opts.to);
62
+ },
63
+
64
+ async deleteFile(config) {
65
+ if (!config.path) throw new Error('local source: config.path is required for deleteFile');
66
+ await fsp.unlink(config.path);
67
+ }
68
+ };
@@ -0,0 +1,9 @@
1
+ // SFTP source — implementation pending user spec.
2
+ // Peer dep candidate: ssh2-sftp-client (friendlier wrapper) or ssh2 (raw).
3
+ // Implements the same interface as file/sources/local.js — see that file
4
+ // for the full contract including streaming_mode semantics.
5
+ // Config supplies host, port, username, and one of password | privateKey.
6
+
7
+ module.exports = {
8
+ requires: ['ssh2-sftp-client']
9
+ };
@@ -0,0 +1,9 @@
1
+ // SharePoint source — implementation pending user spec.
2
+ // Peer dep: TBD (candidate: @microsoft/microsoft-graph-client)
3
+ // Implements the same interface as file/sources/local.js — see that file
4
+ // for the full contract including streaming_mode semantics.
5
+ // Config supplies auth + site/library — treated as a "dbInfo" equivalent.
6
+
7
+ module.exports = {
8
+ requires: []
9
+ };
package/lib/errors.js ADDED
@@ -0,0 +1,37 @@
1
+ class ActionNotRegisteredError extends Error {
2
+ constructor(name) {
3
+ super('No action registered: ' + name);
4
+ this.name = 'ActionNotRegisteredError';
5
+ this.actionName = name;
6
+ }
7
+ }
8
+
9
+ // Thrown at register() when a declared peer dep (`requires`) isn't installed.
10
+ class ActionMissingDependencyError extends Error {
11
+ constructor(actionName, dep) {
12
+ super(
13
+ 'Action "' + actionName + '" requires peer dependency "' + dep +
14
+ '" which is not installed. Install with: npm install ' + dep
15
+ );
16
+ this.name = 'ActionMissingDependencyError';
17
+ this.actionName = actionName;
18
+ this.dep = dep;
19
+ }
20
+ }
21
+
22
+ // Placeholder for future retry logic. Callers (jobs, workflow) can use this
23
+ // to signal "this failure is transient, please retry me." Today no framework
24
+ // acts on it; when retry rules land, they will branch on this error class.
25
+ class TransientError extends Error {
26
+ constructor(message, opts) {
27
+ super(message);
28
+ this.name = 'TransientError';
29
+ if (opts && opts.cause) this.cause = opts.cause;
30
+ }
31
+ }
32
+
33
+ module.exports = {
34
+ ActionNotRegisteredError: ActionNotRegisteredError,
35
+ ActionMissingDependencyError: ActionMissingDependencyError,
36
+ TransientError: TransientError
37
+ };