@xeplr/actions 1.1.14 → 1.1.15
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 +5 -2
- package/lib/builtins/db/move.js +3 -1
- package/lib/runner.js +2 -0
- package/lib/uploader/index.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -184,13 +184,16 @@ await actions.uploader.upload({
|
|
|
184
184
|
columns, // optional [{ name, type }] — declared; skips inference
|
|
185
185
|
batchSize: 5000, firstBatchScanRows: 1000,
|
|
186
186
|
metaStore, mtId1, mtId2, mtId3, mtId4, details,
|
|
187
|
-
errorTable, batchDir, keepBatchFiles, onProgress
|
|
187
|
+
errorTable, batchDir, keepBatchFiles, onProgress,
|
|
188
|
+
signal // optional AbortSignal: stop on request
|
|
188
189
|
})
|
|
189
190
|
// → { movementId, tables: { main, errors }, columns, totalRows, totalBatches, completed, dropped, aborted, durationMs }
|
|
190
191
|
```
|
|
191
192
|
|
|
192
193
|
`onProgress({ rowsRead, batches, rowsWritten, phase? })` fires after each batch is read, with `rowsWritten` from the write queue's own count, so a caller can show both and see whether writing keeps up. After the read, while the queue drains, it fires again each time the written count moves (`phase: 'writing'`), never on a bare timer, so a stuck write goes quiet.
|
|
193
194
|
|
|
195
|
+
`signal` (an `AbortSignal`, passed to db-move as `system.signal`) stops a running movement: the writes still queued are dropped at once (in-flight ones settle), the read stops at the next batch, and it ends with an error whose `code` is `'STOPPED'` (the runner keeps `error.code`), neither a failure nor a success. What it wrote stays, under its `movementId`, for the caller to roll back or keep.
|
|
196
|
+
|
|
194
197
|
Rows are spooled to NDJSON files on disk between source and database. From the first batch it creates, if missing: the target table, `<targetTable>_import_errors` (`movement_id`, `row_num`, `error_description`, `underlying_sql`, `raw_row`, `recorded_at`), a `__xeplr_movement_id__` column, and a unique index on `primaryKeys`. Server dialects also add `__xeplr_id__`; DuckDB does not.
|
|
195
198
|
|
|
196
199
|
Logical types and what each driver creates:
|
|
@@ -312,7 +315,7 @@ node --test test/uploader/reconcile.test.js
|
|
|
312
315
|
|
|
313
316
|
| needs nothing | needs a database |
|
|
314
317
|
|---|---|
|
|
315
|
-
| `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/write-progress.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
|
|
318
|
+
| `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/write-progress.test.js`, `test/stop-movement.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
|
|
316
319
|
| `test/drivers/duckdb.test.js`, `test/drivers/duckdb-values.test.js` (needs `@duckdb/node-api`) | MySQL `localhost:3306` (`MYSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mysql-integration.test.js` |
|
|
317
320
|
| | SQL Server `localhost:1433` (`MSSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mssql-integration.test.js` |
|
|
318
321
|
| | Postgres + MySQL + SQL Server (`PG_*`, `MYSQL_*`, `MSSQL_*`): `test/builtins/db-replication.test.js` |
|
package/lib/builtins/db/move.js
CHANGED
|
@@ -304,7 +304,9 @@ module.exports = {
|
|
|
304
304
|
// Passed through from the caller's `system`, not from `input` — a
|
|
305
305
|
// progress reporter is a function, and an action's input is data that
|
|
306
306
|
// has to survive being stored in a job row.
|
|
307
|
-
onProgress: system.onProgress || null
|
|
307
|
+
onProgress: system.onProgress || null,
|
|
308
|
+
// An AbortSignal from the caller: stop reading, drop the queued writes.
|
|
309
|
+
signal: system.signal || null
|
|
308
310
|
});
|
|
309
311
|
|
|
310
312
|
// Every count upload() has, not just the headline one — "read" and
|
package/lib/runner.js
CHANGED
|
@@ -109,6 +109,8 @@ function finish(start, status, output, err) {
|
|
|
109
109
|
if (err.stack) e.stack = err.stack;
|
|
110
110
|
if (err.details) e.details = err.details;
|
|
111
111
|
if (err.actionName) e.actionName = err.actionName;
|
|
112
|
+
// Kept, so a caller can tell a movement STOPPED on request from one that failed.
|
|
113
|
+
if (err.code) e.code = err.code;
|
|
112
114
|
}
|
|
113
115
|
return { status: status, output: output, error: e, durationMs: Date.now() - start };
|
|
114
116
|
}
|
package/lib/uploader/index.js
CHANGED
|
@@ -77,6 +77,14 @@ async function upload(opts) {
|
|
|
77
77
|
var spoolResult = null;
|
|
78
78
|
var runDir = null;
|
|
79
79
|
|
|
80
|
+
// STOPPING ON REQUEST (opts.signal, an AbortSignal). The writes still queued
|
|
81
|
+
// are dropped the moment it fires — in-flight ones settle — and the read
|
|
82
|
+
// stops at the next batch. What was written stays, identified by
|
|
83
|
+
// movementId, for the caller to roll back or keep.
|
|
84
|
+
if (opts.signal && opts.signal.aborted) throw stoppedError();
|
|
85
|
+
var onStop = opts.signal ? function() { opts.queue.abort(opts.movementId, 'stopped'); } : null;
|
|
86
|
+
if (onStop) opts.signal.addEventListener('abort', onStop, { once: true });
|
|
87
|
+
|
|
80
88
|
try {
|
|
81
89
|
var summary = await spool(opts.source, {
|
|
82
90
|
runId: opts.movementId,
|
|
@@ -131,6 +139,7 @@ async function upload(opts) {
|
|
|
131
139
|
// in turn awaits spool's next batch — the whole pipeline throttles
|
|
132
140
|
// correctly, but disk (not memory) is the buffer.
|
|
133
141
|
onBatch: async function(rows, meta) {
|
|
142
|
+
if (opts.signal && opts.signal.aborted) throw stoppedError();
|
|
134
143
|
if (!columns) throw new Error('uploader: onBatch before beforeAll — spool contract violated');
|
|
135
144
|
for (var off = 0; off < rows.length; off += effectiveBatchSize) {
|
|
136
145
|
var chunk = rows.slice(off, off + effectiveBatchSize);
|
|
@@ -207,6 +216,8 @@ async function upload(opts) {
|
|
|
207
216
|
} finally {
|
|
208
217
|
if (drainWatch) drainWatch.stop();
|
|
209
218
|
}
|
|
219
|
+
// Stopped while the queue drained: not a finished movement.
|
|
220
|
+
if (opts.signal && opts.signal.aborted) throw stoppedError();
|
|
210
221
|
|
|
211
222
|
var stats = opts.queue.stats(opts.movementId) || {};
|
|
212
223
|
var status = stats.aborted ? 'aborted' : 'completed';
|
|
@@ -236,9 +247,17 @@ async function upload(opts) {
|
|
|
236
247
|
try { await fsp.rm(runDir, { recursive: true, force: true }); } catch (_) {}
|
|
237
248
|
}
|
|
238
249
|
if (!poolIsExternal && pool) await driver.close(pool);
|
|
250
|
+
if (onStop) opts.signal.removeEventListener('abort', onStop);
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
|
|
254
|
+
/** The error a stopped movement ends with: code 'STOPPED', so a caller can tell it from a failure. */
|
|
255
|
+
function stoppedError() {
|
|
256
|
+
var err = new Error('Stopped by request');
|
|
257
|
+
err.code = 'STOPPED';
|
|
258
|
+
return err;
|
|
259
|
+
}
|
|
260
|
+
|
|
242
261
|
// Rollback a movement — drop from main + error tables, clear meta row.
|
|
243
262
|
// Graceful queue abort first (in-flight SQLs allowed to settle).
|
|
244
263
|
//
|
|
@@ -406,6 +425,7 @@ var { inferColumnType } = require('./infer');
|
|
|
406
425
|
module.exports = {
|
|
407
426
|
upload: upload,
|
|
408
427
|
watchWrites: watchWrites,
|
|
428
|
+
stoppedError: stoppedError,
|
|
409
429
|
rollback: rollback,
|
|
410
430
|
inferColumns: inferColumns,
|
|
411
431
|
inferColumnType: inferColumnType,
|
package/package.json
CHANGED