@dudousxd/nestjs-catalog 0.7.0 → 0.9.0
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/dist/catalog.controller.d.ts +6 -0
- package/dist/catalog.controller.js +54 -0
- package/dist/catalog.events.d.ts +72 -1
- package/dist/catalog.events.js +6 -0
- package/dist/catalog.overlay-store.d.ts +22 -1
- package/dist/catalog.overlay-store.js +47 -6
- package/dist/catalog.pipeline.d.ts +22 -1
- package/dist/catalog.registry.base.d.ts +66 -1
- package/dist/catalog.registry.base.js +102 -0
- package/dist/catalog.registry.d.ts +12 -2
- package/dist/catalog.registry.js +39 -68
- package/dist/catalog.service.d.ts +17 -0
- package/dist/catalog.service.js +72 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -2
- package/dist/search.d.ts +184 -0
- package/dist/search.js +345 -0
- package/dist/search.types.d.ts +93 -0
- package/dist/search.types.js +17 -0
- package/dist/transform-runner.js +223 -10
- package/package.json +1 -1
package/dist/transform-runner.js
CHANGED
|
@@ -17,6 +17,57 @@ const node_path_1 = require("node:path");
|
|
|
17
17
|
const common_1 = require("@nestjs/common");
|
|
18
18
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
19
|
const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* How much of what a transform logged is carried back, on both axes.
|
|
22
|
+
*
|
|
23
|
+
* Both, because either one alone leaves the capture unbounded in the dimension
|
|
24
|
+
* it does not cover, and this capture is *user code writing whatever it likes*.
|
|
25
|
+
* A transform that logs one line per record — the most natural debugging move
|
|
26
|
+
* there is — puts a copy of the source's data into `logs`, and `logs` is the one
|
|
27
|
+
* thing that crosses a durable step boundary and lands in the run record. So the
|
|
28
|
+
* ceiling is fixed here, in the child, before any of it is serialised: the
|
|
29
|
+
* alternative is a `finishRun` write whose size is a property of somebody's
|
|
30
|
+
* data.
|
|
31
|
+
*
|
|
32
|
+
* The same two numbers for JavaScript and for Python, applied by the two
|
|
33
|
+
* harnesses below in the same order. A transform's log behaviour changing
|
|
34
|
+
* because of the language it happens to be written in is a difference nobody can
|
|
35
|
+
* predict from reading either one.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately far above what anything downstream keeps — the connector runner
|
|
38
|
+
* takes fifty lines, the workflow runner twenty per node at four hundred
|
|
39
|
+
* characters — because this is the *safety* bound and those are the *display*
|
|
40
|
+
* bounds. A harness that truncated at the display limit would decide, in the
|
|
41
|
+
* child, what a future consumer is allowed to see.
|
|
42
|
+
*
|
|
43
|
+
* What is dropped is said out loud, in a final line, rather than dropped
|
|
44
|
+
* quietly. Silence about a missing log is the exact failure this whole capture
|
|
45
|
+
* exists to remove; reproducing it at line 501 would only move it.
|
|
46
|
+
*/
|
|
47
|
+
const MAX_LOG_LINES = 500;
|
|
48
|
+
const MAX_LOG_LINE_CHARS = 2_000;
|
|
49
|
+
/**
|
|
50
|
+
* How much of what a *failing* transform logged is folded into the error.
|
|
51
|
+
*
|
|
52
|
+
* A failure throws, and a throw carries a message and nothing else — so the
|
|
53
|
+
* `logs` of a run that raised never reach the caller at all, and every consumer
|
|
54
|
+
* records the traceback with none of the output that led to it. Capturing
|
|
55
|
+
* `print` and then discarding it at the exact moment it is most wanted would be
|
|
56
|
+
* a fix that stops one step short of the case it was written for.
|
|
57
|
+
*
|
|
58
|
+
* The **last** lines, not the first, which is the opposite of what the display
|
|
59
|
+
* caps downstream do — and deliberately. Those are trimming a successful run's
|
|
60
|
+
* narrative, where the beginning is the story; this is the approach to a
|
|
61
|
+
* traceback, where the last thing printed is the one that says where the code
|
|
62
|
+
* got to.
|
|
63
|
+
*
|
|
64
|
+
* Small on both axes because this lands in an error message, and an error
|
|
65
|
+
* message ends up in a run row, a log line and a console toast. The full set is
|
|
66
|
+
* still on the result whenever the transform returned at all; this is the
|
|
67
|
+
* consolation for the path where there is no result.
|
|
68
|
+
*/
|
|
69
|
+
const FAILURE_LOG_LINES = 10;
|
|
70
|
+
const FAILURE_LOG_CHARS = 200;
|
|
20
71
|
/** Packages worth telling the author about, if the environment has them. */
|
|
21
72
|
const REPORTED_PACKAGES = ['pandas', 'numpy', 'pyarrow', 'requests'];
|
|
22
73
|
/**
|
|
@@ -111,14 +162,15 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
111
162
|
catch {
|
|
112
163
|
throw new Error(`The transform did not return anything readable. stderr: ${stderr.slice(0, 500)}`);
|
|
113
164
|
}
|
|
165
|
+
const logs = Array.isArray(parsed.logs) ? parsed.logs.map(String) : [];
|
|
114
166
|
if (parsed.error)
|
|
115
|
-
throw new Error(parsed.error);
|
|
167
|
+
throw new Error(withFinalLogs(parsed.error, logs));
|
|
116
168
|
if (!Array.isArray(parsed.rows)) {
|
|
117
169
|
throw new Error('The transform must return an array of rows. Returning anything else would leave the load ambiguous.');
|
|
118
170
|
}
|
|
119
171
|
return {
|
|
120
172
|
rows: parsed.rows.filter((row) => typeof row === 'object' && row !== null && !Array.isArray(row)),
|
|
121
|
-
logs
|
|
173
|
+
logs,
|
|
122
174
|
elapsedMs: Date.now() - started,
|
|
123
175
|
};
|
|
124
176
|
}
|
|
@@ -204,18 +256,68 @@ exports.SubprocessTransformRunner = SubprocessTransformRunner = SubprocessTransf
|
|
|
204
256
|
(0, common_1.Injectable)(),
|
|
205
257
|
__metadata("design:paramtypes", [Object])
|
|
206
258
|
], SubprocessTransformRunner);
|
|
259
|
+
/**
|
|
260
|
+
* The traceback, plus the tail of what the code printed on its way to it.
|
|
261
|
+
*
|
|
262
|
+
* Named in the message rather than appended bare, and counted rather than
|
|
263
|
+
* merely truncated: "the last 10 of 57 lines" tells a reader there is more to
|
|
264
|
+
* find on the run's own log, where a silent tail would let them believe they
|
|
265
|
+
* were looking at everything the transform said.
|
|
266
|
+
*/
|
|
267
|
+
function withFinalLogs(error, logs) {
|
|
268
|
+
if (logs.length === 0)
|
|
269
|
+
return error;
|
|
270
|
+
const tail = logs
|
|
271
|
+
.slice(-FAILURE_LOG_LINES)
|
|
272
|
+
.map((line) => line.length > FAILURE_LOG_CHARS ? `${line.slice(0, FAILURE_LOG_CHARS)}…` : line);
|
|
273
|
+
const heading = logs.length > tail.length
|
|
274
|
+
? `The last ${tail.length} of ${logs.length} lines it logged first:`
|
|
275
|
+
: `${tail.length === 1 ? 'The line' : `The ${tail.length} lines`} it logged first:`;
|
|
276
|
+
return `${error}\n${heading}\n${tail.map((line) => ` ${line}`).join('\n')}`;
|
|
277
|
+
}
|
|
207
278
|
/**
|
|
208
279
|
* The JavaScript and TypeScript harness.
|
|
209
280
|
*
|
|
210
281
|
* `console.log` is captured rather than left on stdout so user code cannot
|
|
211
282
|
* corrupt the single JSON line this prints — a transform that logs a `{` would
|
|
212
283
|
* otherwise break its own result parsing, which is a maddening thing to debug.
|
|
284
|
+
*
|
|
285
|
+
* Every console channel that reaches a terminal is overridden, not just the four
|
|
286
|
+
* that were here first. `console.debug` writes to stdout exactly as `console.log`
|
|
287
|
+
* does, so leaving it alone left one spelling of "log something" that silently
|
|
288
|
+
* corrupted the result line; `console.trace` writes to stderr, so leaving it
|
|
289
|
+
* alone left one spelling that silently went nowhere. Both are the same mistake
|
|
290
|
+
* the Python harness made with `print`, and there is no reading of "anything the
|
|
291
|
+
* code logged" under which they are not it.
|
|
292
|
+
*
|
|
293
|
+
* The channels share one array and keep call order, which is the only ordering
|
|
294
|
+
* that answers the question logs are read for — what happened, and in what
|
|
295
|
+
* sequence. Nothing marks which channel a line came from: a reader looking at a
|
|
296
|
+
* failed run wants the sequence, and splitting it into two lists would make the
|
|
297
|
+
* interleaving unrecoverable to buy a label the line's own text usually carries.
|
|
298
|
+
*
|
|
299
|
+
* Bounded by {@link MAX_LOG_LINES} and {@link MAX_LOG_LINE_CHARS}, applied here
|
|
300
|
+
* rather than after the fact, so a transform that logs a copy of its input never
|
|
301
|
+
* gets as far as being serialised.
|
|
213
302
|
*/
|
|
214
303
|
function javascriptHarness(code) {
|
|
215
304
|
return `
|
|
216
305
|
const logs = [];
|
|
217
|
-
|
|
306
|
+
let dropped = 0;
|
|
307
|
+
const keep = (line) => {
|
|
308
|
+
if (logs.length >= ${MAX_LOG_LINES}) { dropped += 1; return; }
|
|
309
|
+
logs.push(
|
|
310
|
+
line.length > ${MAX_LOG_LINE_CHARS}
|
|
311
|
+
? line.slice(0, ${MAX_LOG_LINE_CHARS}) + "… (" + (line.length - ${MAX_LOG_LINE_CHARS}) + " more characters)"
|
|
312
|
+
: line,
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
const write = (...args) => keep(args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
|
|
218
316
|
console.log = write; console.info = write; console.warn = write; console.error = write;
|
|
317
|
+
console.debug = write; console.trace = write;
|
|
318
|
+
const captured = () => dropped === 0
|
|
319
|
+
? logs
|
|
320
|
+
: logs.concat(["… " + dropped + " more line(s) were logged and dropped: a transform keeps its first ${MAX_LOG_LINES}."]);
|
|
219
321
|
|
|
220
322
|
let input = "";
|
|
221
323
|
process.stdin.setEncoding("utf8");
|
|
@@ -225,11 +327,11 @@ try {
|
|
|
225
327
|
const records = JSON.parse(input || "[]");
|
|
226
328
|
const transform = async (records) => { ${code} };
|
|
227
329
|
const rows = await transform(records);
|
|
228
|
-
process.stdout.write(JSON.stringify({ rows: rows ?? [], logs }));
|
|
330
|
+
process.stdout.write(JSON.stringify({ rows: rows ?? [], logs: captured() }));
|
|
229
331
|
} catch (error) {
|
|
230
332
|
process.stdout.write(JSON.stringify({
|
|
231
333
|
error: error instanceof Error ? \`\${error.name}: \${error.message}\` : String(error),
|
|
232
|
-
logs,
|
|
334
|
+
logs: captured(),
|
|
233
335
|
}));
|
|
234
336
|
}
|
|
235
337
|
`;
|
|
@@ -241,6 +343,44 @@ try {
|
|
|
241
343
|
* that reaches for pandas will naturally end with one — making it write
|
|
242
344
|
* `.to_dict("records")` would be a papercut on the only path pandas is worth
|
|
243
345
|
* importing for.
|
|
346
|
+
*
|
|
347
|
+
* **`print` is redirected, for the same reason `console.log` is.** It used to go
|
|
348
|
+
* straight through to the child's real stdout, where the last-line result parse
|
|
349
|
+
* discarded it — so the single most obvious thing a person writes while working
|
|
350
|
+
* out what their transform is doing produced an empty log panel and no
|
|
351
|
+
* explanation. That is not a missing nicety: it costs the author their trust in
|
|
352
|
+
* the runner before they have written anything real, and the conclusion it
|
|
353
|
+
* invites ("my code never ran") is the wrong one. `log()` still exists, because
|
|
354
|
+
* transforms in the wild call it and a `NameError` is a worse answer than a
|
|
355
|
+
* redundant helper, but it is now literally `print` — one buffer, one ordering,
|
|
356
|
+
* and nothing that only works if you already knew about it.
|
|
357
|
+
*
|
|
358
|
+
* **stderr is captured too**, into the same list and in call order. `warnings`,
|
|
359
|
+
* a `logging` handler at its default configuration, and a traceback the code
|
|
360
|
+
* printed itself all land there, and those are precisely the lines somebody is
|
|
361
|
+
* looking for when a transform misbehaves. It is not marked as stderr, matching
|
|
362
|
+
* the JavaScript harness, which does not distinguish `console.error` either: the
|
|
363
|
+
* sequence is what a reader is reconstructing, and two lists would make the
|
|
364
|
+
* interleaving unrecoverable.
|
|
365
|
+
*
|
|
366
|
+
* What was written **before** an exception survives it. The redirect is a
|
|
367
|
+
* context manager around the call rather than a swap held for the whole script,
|
|
368
|
+
* so it unwinds on the way out of a traceback with the buffer intact, and the
|
|
369
|
+
* error branch reports the same lines the success branch would have. A
|
|
370
|
+
* transform that printed three things and then divided by zero is the case logs
|
|
371
|
+
* matter most for, and it is the case a naive swap loses.
|
|
372
|
+
*
|
|
373
|
+
* Bounded by {@link MAX_LOG_LINES} and {@link MAX_LOG_LINE_CHARS}, the same two
|
|
374
|
+
* numbers the JavaScript harness applies. Note that this bounds the *sink*, not
|
|
375
|
+
* only the result: an unterminated write longer than a line's ceiling is flushed
|
|
376
|
+
* as its own line rather than accumulated, so a transform writing without
|
|
377
|
+
* newlines cannot grow the child's memory either.
|
|
378
|
+
*
|
|
379
|
+
* The limit worth stating: this redirects Python-level writes to `sys.stdout`
|
|
380
|
+
* and `sys.stderr`. Output from a C extension or a subprocess that writes to the
|
|
381
|
+
* file descriptors underneath goes to the real streams, exactly as it does past
|
|
382
|
+
* an overridden `console` in Node. Redirecting the descriptors themselves would
|
|
383
|
+
* take the result channel with it.
|
|
244
384
|
*/
|
|
245
385
|
function pythonHarness(code) {
|
|
246
386
|
const indented = code
|
|
@@ -248,11 +388,68 @@ function pythonHarness(code) {
|
|
|
248
388
|
.map((line) => ` ${line}`)
|
|
249
389
|
.join('\n');
|
|
250
390
|
return `
|
|
251
|
-
import sys, json
|
|
391
|
+
import sys, json, contextlib
|
|
252
392
|
|
|
253
393
|
logs = []
|
|
394
|
+
# A one-element list rather than a module global reassigned inside the helper,
|
|
395
|
+
# so the counter needs no \`global\` statement in generated code.
|
|
396
|
+
dropped = [0]
|
|
397
|
+
|
|
398
|
+
def keep(line):
|
|
399
|
+
if len(logs) >= ${MAX_LOG_LINES}:
|
|
400
|
+
dropped[0] += 1
|
|
401
|
+
return
|
|
402
|
+
if len(line) > ${MAX_LOG_LINE_CHARS}:
|
|
403
|
+
line = "{}… ({} more characters)".format(
|
|
404
|
+
line[:${MAX_LOG_LINE_CHARS}], len(line) - ${MAX_LOG_LINE_CHARS}
|
|
405
|
+
)
|
|
406
|
+
logs.append(line)
|
|
407
|
+
|
|
408
|
+
class Sink:
|
|
409
|
+
"""Stands in for stdout and stderr while the transform runs.
|
|
410
|
+
|
|
411
|
+
Line-buffered by hand because \`print("a", "b")\` arrives as four separate
|
|
412
|
+
writes — the parts, the separators and the terminator — and appending each
|
|
413
|
+
one as its own entry would shred every multi-argument call.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
def __init__(self):
|
|
417
|
+
self.partial = ""
|
|
418
|
+
|
|
419
|
+
def write(self, text):
|
|
420
|
+
if not isinstance(text, str):
|
|
421
|
+
text = str(text)
|
|
422
|
+
self.partial += text
|
|
423
|
+
while "\\n" in self.partial:
|
|
424
|
+
line, self.partial = self.partial.split("\\n", 1)
|
|
425
|
+
keep(line)
|
|
426
|
+
# A write with no newline in it is still bounded: past a line's ceiling
|
|
427
|
+
# there is nothing more to keep, so it is emitted rather than held.
|
|
428
|
+
if len(self.partial) > ${MAX_LOG_LINE_CHARS}:
|
|
429
|
+
keep(self.partial)
|
|
430
|
+
self.partial = ""
|
|
431
|
+
return len(text)
|
|
432
|
+
|
|
433
|
+
def writelines(self, lines):
|
|
434
|
+
for line in lines:
|
|
435
|
+
self.write(line)
|
|
436
|
+
|
|
437
|
+
def flush(self):
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
def isatty(self):
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
def drain(self):
|
|
444
|
+
"""Whatever was written without a trailing newline is still output."""
|
|
445
|
+
if self.partial:
|
|
446
|
+
keep(self.partial)
|
|
447
|
+
self.partial = ""
|
|
448
|
+
|
|
449
|
+
sink = Sink()
|
|
450
|
+
|
|
254
451
|
def log(*args):
|
|
255
|
-
|
|
452
|
+
print(*args)
|
|
256
453
|
|
|
257
454
|
def transform(records):
|
|
258
455
|
${indented || ' return records'}
|
|
@@ -266,15 +463,31 @@ def to_rows(result):
|
|
|
266
463
|
return result.to_dict("records")
|
|
267
464
|
return result
|
|
268
465
|
|
|
466
|
+
def captured():
|
|
467
|
+
sink.drain()
|
|
468
|
+
if dropped[0] == 0:
|
|
469
|
+
return logs
|
|
470
|
+
return logs + [
|
|
471
|
+
"… {} more line(s) were logged and dropped: a transform keeps its first {}.".format(
|
|
472
|
+
dropped[0], ${MAX_LOG_LINES}
|
|
473
|
+
)
|
|
474
|
+
]
|
|
475
|
+
|
|
269
476
|
try:
|
|
270
477
|
raw = sys.stdin.read()
|
|
271
478
|
records = json.loads(raw) if raw.strip() else []
|
|
272
|
-
|
|
273
|
-
|
|
479
|
+
# \`to_rows\` is inside the redirect as well: a lazily-evaluated return value
|
|
480
|
+
# does its printing here, not before.
|
|
481
|
+
with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink):
|
|
482
|
+
rows = to_rows(transform(records))
|
|
483
|
+
# Back on the real stdout by now — the context manager restores on the way
|
|
484
|
+
# out, including out of an exception — so this is the only thing on it.
|
|
485
|
+
out = captured()
|
|
486
|
+
sys.stdout.write(json.dumps(rows and {"rows": rows, "logs": out} or {"rows": [], "logs": out}, default=str))
|
|
274
487
|
except Exception as error:
|
|
275
488
|
sys.stdout.write(json.dumps({
|
|
276
489
|
"error": "{}: {}".format(type(error).__name__, error),
|
|
277
|
-
"logs":
|
|
490
|
+
"logs": captured(),
|
|
278
491
|
}))
|
|
279
492
|
`;
|
|
280
493
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-catalog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|