@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/lib/runner.js ADDED
@@ -0,0 +1,81 @@
1
+ var { applySchema } = require('@xeplr/schema-handler');
2
+ var { get } = require('./registry');
3
+ var { ActionNotRegisteredError } = require('./errors');
4
+
5
+ /**
6
+ * Safely run an action. Two call forms supported:
7
+ *
8
+ * runAction({ name, input, system?, timeoutMs? })
9
+ * — look up a registered action by name (the Jobs / config path).
10
+ *
11
+ * runAction(nameOrAction, input, opts?)
12
+ * — shorthand for one-off programmatic use. First arg can be a name
13
+ * (string) OR an action module `{ name, inputSchema, execute, ... }`.
14
+ * `opts` = { system, timeoutMs, metaStore }.
15
+ *
16
+ * Returns { status: 'success' | 'failed', output, error, durationMs }.
17
+ * Never throws — every failure (unregistered action, validation, executor
18
+ * exception, timeout) is captured in `error`.
19
+ */
20
+ async function runAction(first, second, third) {
21
+ var start = Date.now();
22
+ var opts = normalizeArgs(first, second, third);
23
+
24
+ var def = opts.action || get(opts.name);
25
+ if (!def) return finish(start, 'failed', null, new ActionNotRegisteredError(opts.name));
26
+
27
+ var input;
28
+ try {
29
+ input = applySchema(def.inputSchema, opts.input || {}, 'input');
30
+ } catch (err) {
31
+ return finish(start, 'failed', null, err);
32
+ }
33
+
34
+ try {
35
+ var call = def.execute({ input: input, system: opts.system || {}, metaStore: opts.metaStore });
36
+ var settled = (opts.timeoutMs && call && typeof call.then === 'function')
37
+ ? withTimeout(call, opts.timeoutMs)
38
+ : call;
39
+ var output = await settled;
40
+ return finish(start, 'success', output, null);
41
+ } catch (err) {
42
+ return finish(start, 'failed', null, err);
43
+ }
44
+ }
45
+
46
+ // Accept either the options-object form or the shorthand (action, input, opts).
47
+ function normalizeArgs(first, second, third) {
48
+ // Shorthand form: first arg is a string name or an action module.
49
+ if (typeof first === 'string' || (first && typeof first.execute === 'function')) {
50
+ var extra = third || {};
51
+ var result = { input: second || {}, system: extra.system, timeoutMs: extra.timeoutMs, metaStore: extra.metaStore };
52
+ if (typeof first === 'string') result.name = first;
53
+ else result.action = first;
54
+ return result;
55
+ }
56
+ // Options-object form.
57
+ return first || {};
58
+ }
59
+
60
+ function finish(start, status, output, err) {
61
+ var e = null;
62
+ if (err) {
63
+ e = { name: err.name || 'Error', message: err.message };
64
+ if (err.stack) e.stack = err.stack;
65
+ if (err.details) e.details = err.details;
66
+ if (err.actionName) e.actionName = err.actionName;
67
+ }
68
+ return { status: status, output: output, error: e, durationMs: Date.now() - start };
69
+ }
70
+
71
+ function withTimeout(promise, ms) {
72
+ return new Promise(function(resolve, reject) {
73
+ var t = setTimeout(function() { reject(new Error('Action timed out after ' + ms + 'ms')); }, ms);
74
+ promise.then(
75
+ function(v) { clearTimeout(t); resolve(v); },
76
+ function(e) { clearTimeout(t); reject(e); }
77
+ );
78
+ });
79
+ }
80
+
81
+ module.exports = { runAction: runAction };
@@ -0,0 +1,46 @@
1
+ // Per-run checkpoint file. Written atomically (write .tmp, rename) after every
2
+ // batch completes its onBatch hook. Enables resume() to skip already-processed
3
+ // batches on retry.
4
+ //
5
+ // Shape:
6
+ // { runId, batchDir, batchSize, lastCompletedBatch, updatedAt }
7
+
8
+ var fs = require('fs');
9
+ var fsp = require('fs/promises');
10
+ var path = require('path');
11
+
12
+ function checkpointPath(dir, runId) {
13
+ return path.join(dir, runId + '.json');
14
+ }
15
+
16
+ async function ensureDir(dir) {
17
+ await fsp.mkdir(dir, { recursive: true });
18
+ }
19
+
20
+ async function read(dir, runId) {
21
+ if (!dir) return null;
22
+ try {
23
+ var raw = await fsp.readFile(checkpointPath(dir, runId), 'utf8');
24
+ return JSON.parse(raw);
25
+ } catch (err) {
26
+ if (err.code === 'ENOENT') return null;
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ async function write(dir, checkpoint) {
32
+ if (!dir) return;
33
+ await ensureDir(dir);
34
+ var target = checkpointPath(dir, checkpoint.runId);
35
+ var tmp = target + '.tmp';
36
+ var payload = JSON.stringify(Object.assign({}, checkpoint, { updatedAt: new Date().toISOString() }));
37
+ await fsp.writeFile(tmp, payload, { encoding: 'utf8' });
38
+ await fsp.rename(tmp, target);
39
+ }
40
+
41
+ async function clear(dir, runId) {
42
+ if (!dir) return;
43
+ await fsp.unlink(checkpointPath(dir, runId)).catch(function() { /* ignore */ });
44
+ }
45
+
46
+ module.exports = { read: read, write: write, clear: clear, checkpointPath: checkpointPath };
@@ -0,0 +1,10 @@
1
+ var { spool, readBatchFile } = require('./spool');
2
+ var { resume } = require('./resume');
3
+ var checkpoint = require('./checkpoint');
4
+
5
+ module.exports = {
6
+ spool: spool,
7
+ resume: resume,
8
+ readBatchFile: readBatchFile,
9
+ checkpoint: checkpoint
10
+ };
@@ -0,0 +1,35 @@
1
+ // resume(runId, source, opts)
2
+ //
3
+ // Look up the checkpoint for runId (in opts.checkpointDir), figure out how
4
+ // many batches were already completed, then re-invoke spool() telling it to
5
+ // skip forward by that much. The caller's `source` must be replayable / at
6
+ // least positioned so the first row it yields corresponds to the first row
7
+ // of batch `lastCompletedBatch + 1`.
8
+ //
9
+ // Concretely: if `source` is `for-await SELECT * FROM foo ORDER BY id LIMIT ...
10
+ // OFFSET (lastCompletedBatch * batchSize)`, resume() will "just work."
11
+
12
+ var checkpoint = require('./checkpoint');
13
+ var { spool } = require('./spool');
14
+
15
+ async function resume(runId, source, opts) {
16
+ opts = opts || {};
17
+ if (!runId) throw new Error('resume: runId is required');
18
+ if (!opts.checkpointDir) throw new Error('resume: opts.checkpointDir is required');
19
+
20
+ var cp = await checkpoint.read(opts.checkpointDir, runId);
21
+ if (!cp) {
22
+ // No checkpoint → cold start with this runId.
23
+ return spool(source, Object.assign({}, opts, { runId: runId }));
24
+ }
25
+
26
+ var startFromBatch = (cp.lastCompletedBatch != null ? cp.lastCompletedBatch : -1) + 1;
27
+
28
+ return spool(source, Object.assign({}, opts, {
29
+ runId: runId,
30
+ startFromBatch: startFromBatch,
31
+ batchSize: cp.batchSize || opts.batchSize
32
+ }));
33
+ }
34
+
35
+ module.exports = { resume: resume };
@@ -0,0 +1,253 @@
1
+ // spool(source, opts)
2
+ //
3
+ // Consume an async iterable of rows, write them as rotating NDJSON batch
4
+ // files, and drive `target` hooks in order. Never throws mid-stream —
5
+ // hook errors reject the whole spool.
6
+ //
7
+ // See project_xeplr_actions_streaming.md for the locked design.
8
+
9
+ var fs = require('fs');
10
+ var fsp = require('fs/promises');
11
+ var path = require('path');
12
+
13
+ var { semaphore, rateLimiter, memoryMonitor, toNDJSON } = require('./utils');
14
+ var checkpoint = require('./checkpoint');
15
+
16
+ var DEFAULTS = {
17
+ batchSize: 5000,
18
+ maxInFlight: 1,
19
+ ratePerSecond: null,
20
+ maxMemoryMB: 256
21
+ };
22
+
23
+ /**
24
+ * @param {AsyncIterable<object>} source
25
+ * @param {object} opts
26
+ * @param {string} opts.runId stable id — used for the sub-dir + checkpoint file
27
+ * @param {string} opts.batchDir where the run dir goes (final dir = batchDir/<runId>)
28
+ * @param {number} [opts.batchSize=5000]
29
+ * @param {number} [opts.startFromBatch=0] skip N batches (drives resume)
30
+ * @param {number} [opts.maxInFlight=1]
31
+ * @param {number} [opts.ratePerSecond]
32
+ * @param {number} [opts.maxMemoryMB=256]
33
+ * @param {string} [opts.checkpointDir] enables checkpoint writes
34
+ * @param {object} [opts.target]
35
+ * @param {function} [opts.target.beforeAll] (ctx) => void fires ONCE, after 1st batch is buffered to disk
36
+ * @param {function} [opts.target.onBatch] (rows, meta) => void fires per completed batch, in order
37
+ * @param {function} [opts.target.afterAll] (summary) => void fires ONCE, after all batches settle
38
+ * @returns {Promise<{runId, batchDir, totalRows, totalBatches, files, durationMs}>}
39
+ */
40
+ async function spool(source, opts) {
41
+ opts = Object.assign({}, DEFAULTS, opts || {});
42
+
43
+ if (!opts.runId) throw new Error('spool: opts.runId is required');
44
+ if (!opts.batchDir) throw new Error('spool: opts.batchDir is required');
45
+ if (!source || typeof source[Symbol.asyncIterator] !== 'function') {
46
+ throw new Error('spool: source must be an async iterable');
47
+ }
48
+
49
+ var target = opts.target || {};
50
+ var runDir = path.join(opts.batchDir, opts.runId);
51
+ await fsp.mkdir(runDir, { recursive: true });
52
+
53
+ var started = Date.now();
54
+ var sem = semaphore(opts.maxInFlight);
55
+ var rl = rateLimiter(opts.ratePerSecond);
56
+ var mem = memoryMonitor(opts.maxMemoryMB);
57
+
58
+ // Ordered checkpoint tracking: batches may complete out of order when
59
+ // maxInFlight > 1. `lastContiguous` = highest N where every batch 0..N has
60
+ // reported done. Only checkpoint that highwater — safer for resume.
61
+ var completedSet = new Set();
62
+ var lastContiguous = (opts.startFromBatch || 0) - 1;
63
+
64
+ // First-error latch. Since spool guarantees "all hooks awaited in order,
65
+ // any hook error rejects the whole thing," we surface the first error
66
+ // deterministically even if newer batches also fail.
67
+ var firstError = null;
68
+
69
+ var fileIndex = opts.startFromBatch || 0;
70
+ var currentFile = null; // { stream, path, byteCount, rowCount, buffered }
71
+ var beforeAllFired = false;
72
+ var pendingChain = Promise.resolve(); // sequential-arrival ordering into the semaphore
73
+ var filesCreated = [];
74
+ var totalRows = 0;
75
+
76
+ async function openNextFile() {
77
+ var name = String(fileIndex).padStart(6, '0') + '.ndjson';
78
+ var filePath = path.join(runDir, name);
79
+ var stream = fs.createWriteStream(filePath);
80
+ currentFile = {
81
+ stream: stream,
82
+ path: filePath,
83
+ byteCount: 0,
84
+ rowCount: 0,
85
+ // Buffer rows in memory too — we need them to invoke onBatch(rows, meta).
86
+ // Same footprint as reading the file back, but avoids the round trip.
87
+ buffered: []
88
+ };
89
+ }
90
+
91
+ async function closeCurrentFile() {
92
+ if (!currentFile) return null;
93
+ var cf = currentFile;
94
+ currentFile = null;
95
+ await new Promise(function(resolve) { cf.stream.end(resolve); });
96
+ return cf;
97
+ }
98
+
99
+ // Drop the current file if it's empty (avoids leaving 000042.ndjson behind
100
+ // when the source ended right on a rotation boundary).
101
+ async function dropEmptyFile(cf) {
102
+ if (!cf || cf.rowCount > 0) return false;
103
+ await fsp.unlink(cf.path).catch(function() { /* ignore */ });
104
+ return true;
105
+ }
106
+
107
+ // Schedule a completed file for hook processing.
108
+ // Adds to the pendingChain to preserve arrival order into the semaphore,
109
+ // then the semaphore itself gates concurrency (maxInFlight).
110
+ function schedule(cf, batchIndex, isLast) {
111
+ pendingChain = pendingChain.then(async function() {
112
+ if (firstError) return; // short-circuit on failure
113
+
114
+ var release = await sem.acquire();
115
+
116
+ try {
117
+ // First-batch bootstrap — fire beforeAll AFTER the first batch is on
118
+ // disk (so consumers can peek at it), BEFORE its onBatch runs.
119
+ if (!beforeAllFired && batchIndex === 0 && target.beforeAll) {
120
+ beforeAllFired = true;
121
+ await target.beforeAll({
122
+ runId: opts.runId,
123
+ batchDir: runDir,
124
+ firstBatchPath: cf.path
125
+ });
126
+ }
127
+
128
+ if (target.onBatch) {
129
+ await target.onBatch(cf.buffered, {
130
+ batchIndex: batchIndex,
131
+ filePath: cf.path,
132
+ rowCount: cf.rowCount,
133
+ byteCount: cf.byteCount,
134
+ isLast: isLast
135
+ });
136
+ }
137
+
138
+ completedSet.add(batchIndex);
139
+ // Advance the contiguous highwater.
140
+ while (completedSet.has(lastContiguous + 1)) {
141
+ lastContiguous++;
142
+ completedSet.delete(lastContiguous);
143
+ }
144
+
145
+ // Release the row bytes for GC + memory accounting.
146
+ mem.sub(cf.byteCount);
147
+ cf.buffered = null;
148
+
149
+ if (opts.checkpointDir) {
150
+ await checkpoint.write(opts.checkpointDir, {
151
+ runId: opts.runId,
152
+ batchDir: runDir,
153
+ batchSize: opts.batchSize,
154
+ lastCompletedBatch: lastContiguous
155
+ });
156
+ }
157
+ } catch (err) {
158
+ if (!firstError) firstError = err;
159
+ } finally {
160
+ release();
161
+ }
162
+ });
163
+ }
164
+
165
+ // ─── main pump ──────────────────────────────────────────────────────────
166
+ try {
167
+ await openNextFile();
168
+
169
+ for await (var row of source) {
170
+ if (firstError) break;
171
+
172
+ await rl.take(1);
173
+ var line = toNDJSON(row);
174
+ mem.add(line.length);
175
+
176
+ currentFile.stream.write(line);
177
+ currentFile.byteCount += line.length;
178
+ currentFile.rowCount++;
179
+ currentFile.buffered.push(row);
180
+ totalRows++;
181
+
182
+ if (currentFile.rowCount >= opts.batchSize) {
183
+ var idx = fileIndex;
184
+ var cf = await closeCurrentFile();
185
+ filesCreated.push(cf.path);
186
+ schedule(cf, idx, false);
187
+ fileIndex++;
188
+ await openNextFile();
189
+ }
190
+ }
191
+
192
+ // Close the trailing file. If it's empty, drop it. If it had rows,
193
+ // it counts as the last batch.
194
+ var tail = await closeCurrentFile();
195
+ if (tail) {
196
+ if (await dropEmptyFile(tail)) {
197
+ // Nothing to schedule.
198
+ } else {
199
+ var tailIdx = fileIndex;
200
+ filesCreated.push(tail.path);
201
+ schedule(tail, tailIdx, true);
202
+ }
203
+ }
204
+
205
+ // Wait for every scheduled hook to settle.
206
+ await pendingChain;
207
+ if (firstError) throw firstError;
208
+
209
+ if (target.afterAll) {
210
+ await target.afterAll({
211
+ runId: opts.runId,
212
+ batchDir: runDir,
213
+ totalRows: totalRows,
214
+ totalBatches: filesCreated.length,
215
+ files: filesCreated,
216
+ durationMs: Date.now() - started
217
+ });
218
+ }
219
+
220
+ // Success — clear the checkpoint so a future resume() with the same runId
221
+ // treats the run as fresh (rather than "already done at batch N").
222
+ if (opts.checkpointDir) {
223
+ await checkpoint.clear(opts.checkpointDir, opts.runId).catch(function() {});
224
+ }
225
+
226
+ return {
227
+ runId: opts.runId,
228
+ batchDir: runDir,
229
+ totalRows: totalRows,
230
+ totalBatches: filesCreated.length,
231
+ files: filesCreated,
232
+ durationMs: Date.now() - started
233
+ };
234
+ } catch (err) {
235
+ // Best-effort teardown of the currently open file (nothing to schedule).
236
+ if (currentFile) {
237
+ try { await new Promise(function(r) { currentFile.stream.end(r); }); } catch (_) {}
238
+ currentFile = null;
239
+ }
240
+ throw err;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Read one NDJSON batch file back into an array of rows. Consumer counterpart
246
+ * to spool's writer — one JSON object per non-blank line.
247
+ */
248
+ async function readBatchFile(filePath) {
249
+ var raw = await fsp.readFile(filePath, 'utf8');
250
+ return raw.split(/\r?\n/).filter(function(l) { return l.trim().length > 0; }).map(function(l) { return JSON.parse(l); });
251
+ }
252
+
253
+ module.exports = { spool: spool, readBatchFile: readBatchFile };
@@ -0,0 +1,82 @@
1
+ // Small building blocks used by spool.js. No external deps.
2
+
3
+ // Semaphore that gates concurrency to `n` (default 1).
4
+ // acquire() returns a release() function that MUST be called (finally block).
5
+ function semaphore(n) {
6
+ var slots = n;
7
+ var queue = [];
8
+ return {
9
+ async acquire() {
10
+ if (slots > 0) { slots--; return release; }
11
+ await new Promise(function(resolve) { queue.push(resolve); });
12
+ slots--;
13
+ return release;
14
+ },
15
+ inFlight() { return n - slots; }
16
+ };
17
+ function release() {
18
+ slots++;
19
+ var next = queue.shift();
20
+ if (next) next();
21
+ }
22
+ }
23
+
24
+ // Simple leaky-bucket rate limiter. `perSecond=null` disables (no-op).
25
+ function rateLimiter(perSecond) {
26
+ if (!perSecond || perSecond <= 0) {
27
+ return { async take(_n) { /* no-op */ } };
28
+ }
29
+ var tokens = perSecond;
30
+ var lastRefill = Date.now();
31
+ return {
32
+ async take(n) {
33
+ n = n || 1;
34
+ while (true) {
35
+ // Refill based on elapsed time.
36
+ var now = Date.now();
37
+ var elapsed = (now - lastRefill) / 1000;
38
+ tokens = Math.min(perSecond, tokens + elapsed * perSecond);
39
+ lastRefill = now;
40
+ if (tokens >= n) { tokens -= n; return; }
41
+ // Wait for enough tokens.
42
+ var need = (n - tokens) / perSecond * 1000;
43
+ await new Promise(function(r) { setTimeout(r, Math.max(1, Math.ceil(need))); });
44
+ }
45
+ }
46
+ };
47
+ }
48
+
49
+ // Coarse memory tracker. Bytes added via .add(n); throws with a clear message
50
+ // when the running total crosses the ceiling. Consumer decrements via .sub()
51
+ // when a batch is released (file flushed + onBatch settled).
52
+ function memoryMonitor(maxMB) {
53
+ if (!maxMB || maxMB <= 0) {
54
+ return { add() {}, sub() {}, current() { return 0; } };
55
+ }
56
+ var ceiling = maxMB * 1024 * 1024;
57
+ var used = 0;
58
+ return {
59
+ add(n) {
60
+ used += n;
61
+ if (used > ceiling) {
62
+ var mb = Math.round(used / 1024 / 1024);
63
+ throw new Error('Streaming memory ceiling exceeded: ' + mb + 'MB > ' + maxMB + 'MB (raise maxMemoryMB or lower batchSize)');
64
+ }
65
+ },
66
+ sub(n) { used = Math.max(0, used - n); },
67
+ current() { return used; }
68
+ };
69
+ }
70
+
71
+ // Serialize a row to a NDJSON line. Replaces embedded newlines so each row is
72
+ // exactly one line — the invariant `readNDJSONLines` relies on.
73
+ function toNDJSON(row) {
74
+ return JSON.stringify(row).replace(/\r?\n/g, ' ') + '\n';
75
+ }
76
+
77
+ module.exports = {
78
+ semaphore: semaphore,
79
+ rateLimiter: rateLimiter,
80
+ memoryMonitor: memoryMonitor,
81
+ toNDJSON: toNDJSON
82
+ };
@@ -0,0 +1,69 @@
1
+ // attachConfig — connect an app to the shared xeplr_configs control-plane DB
2
+ // and get back a ready-to-use metaStore + ImportMeta model, in one call.
3
+ // Mirrors @xeplr/auth's attach() shape exactly (ready()/model-style getters,
4
+ // required env owned by the library, lazy + memoized) so every consuming app
5
+ // writes the SAME few lines instead of each hand-rolling its own
6
+ // bootstrapConfigDb()/bindKnex()/meta-store-knex() dance:
7
+ //
8
+ // const { attachConfig } = require('@xeplr/actions');
9
+ // const xcfg = attachConfig({ service: 'xeplr-bi' }); // service = this app's name
10
+ // await xcfg.ready();
11
+ // xcfg.metaStore // pass into uploads/loadMapping, uploader.rollback
12
+ // xcfg.ImportMeta // BaseModel bound to xeplr_configs — reads/soft-deletes
13
+ // xcfg.SERVICE_NAME // same string, for `.where('service', ...)` filters
14
+ //
15
+ // import_meta's SCHEMA is identical for every app (see xeplr-db/config/
16
+ // migrations/0001_import_meta.js + 0002_...) — there is nothing app-specific
17
+ // about the ImportMeta model, so it lives here once instead of being
18
+ // copy-pasted into every app's models/ directory.
19
+ var { BaseModel, bootstrapConfigDb } = require('@xeplr/db');
20
+ var { decrypt } = require('@xeplr/utils/isomorphic/crypto');
21
+ var makeMetaStore = require('./meta-store-knex');
22
+
23
+ var requiredEnv = [
24
+ 'XCFG_DB_NAME', // xeplr_configs db name
25
+ 'XCFG_DB_CONNECTION_INFO_ENCRYPTED', // xeplr_configs server login
26
+ ];
27
+
28
+ class ImportMeta extends BaseModel {
29
+ static get tableName() { return 'import_meta'; }
30
+ static get idColumn() { return 'id'; }
31
+ }
32
+
33
+ function attachConfig(options) {
34
+ options = options || {};
35
+ var service = options.service;
36
+ if (!service) throw new Error('attachConfig: options.service is required');
37
+
38
+ var _ready = null;
39
+ var _xcfg = null;
40
+ var _boundImportMeta = null;
41
+ var _metaStore = null;
42
+
43
+ function ready() {
44
+ if (!_ready) {
45
+ _ready = (async function() {
46
+ var login = JSON.parse(await decrypt(process.env.XCFG_DB_CONNECTION_INFO_ENCRYPTED, process.env.ENCRYPTION_KEY));
47
+ _xcfg = await bootstrapConfigDb({ connection: login, database: process.env.XCFG_DB_NAME });
48
+ _boundImportMeta = ImportMeta.bindKnex(_xcfg.db);
49
+ _metaStore = makeMetaStore(_xcfg.db, { table: 'import_meta', service: service });
50
+ return _xcfg;
51
+ })();
52
+ }
53
+ return _ready;
54
+ }
55
+
56
+ function requireReady(name) {
57
+ if (!_xcfg) throw new Error('attachConfig("' + service + '"): ' + name + ' accessed before ready() resolved');
58
+ }
59
+
60
+ return {
61
+ ready: ready,
62
+ SERVICE_NAME: service,
63
+ get metaStore() { requireReady('metaStore'); return _metaStore; },
64
+ get ImportMeta() { requireReady('ImportMeta'); return _boundImportMeta; },
65
+ get db() { requireReady('db'); return _xcfg.db; }
66
+ };
67
+ }
68
+
69
+ module.exports = { attachConfig: attachConfig, requiredEnv: requiredEnv, ImportMeta: ImportMeta };