@dudousxd/nestjs-catalog 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/dist/catalog.controller.d.ts +8 -0
  4. package/dist/catalog.controller.js +482 -0
  5. package/dist/catalog.decorators.d.ts +37 -0
  6. package/dist/catalog.decorators.js +50 -0
  7. package/dist/catalog.environment.d.ts +442 -0
  8. package/dist/catalog.environment.js +645 -0
  9. package/dist/catalog.events.d.ts +179 -0
  10. package/dist/catalog.events.js +110 -0
  11. package/dist/catalog.module.d.ts +5 -0
  12. package/dist/catalog.module.js +71 -0
  13. package/dist/catalog.options.d.ts +79 -0
  14. package/dist/catalog.options.js +4 -0
  15. package/dist/catalog.overlay-store.d.ts +25 -0
  16. package/dist/catalog.overlay-store.js +44 -0
  17. package/dist/catalog.overlay-store.token.d.ts +1 -0
  18. package/dist/catalog.overlay-store.token.js +4 -0
  19. package/dist/catalog.pipeline.d.ts +800 -0
  20. package/dist/catalog.pipeline.js +606 -0
  21. package/dist/catalog.principal.d.ts +209 -0
  22. package/dist/catalog.principal.js +245 -0
  23. package/dist/catalog.query-cache.d.ts +25 -0
  24. package/dist/catalog.query-cache.js +0 -0
  25. package/dist/catalog.query.d.ts +76 -0
  26. package/dist/catalog.query.js +64 -0
  27. package/dist/catalog.registry.base.d.ts +21 -0
  28. package/dist/catalog.registry.base.js +17 -0
  29. package/dist/catalog.registry.d.ts +44 -0
  30. package/dist/catalog.registry.js +359 -0
  31. package/dist/catalog.service.d.ts +115 -0
  32. package/dist/catalog.service.js +366 -0
  33. package/dist/catalog.store.d.ts +419 -0
  34. package/dist/catalog.store.js +175 -0
  35. package/dist/catalog.types.d.ts +165 -0
  36. package/dist/catalog.types.js +19 -0
  37. package/dist/catalog.workspace.d.ts +426 -0
  38. package/dist/catalog.workspace.js +87 -0
  39. package/dist/client.d.ts +86 -0
  40. package/dist/client.js +83 -0
  41. package/dist/index.d.ts +19 -0
  42. package/dist/index.js +109 -0
  43. package/dist/stores/mikro-orm-read.store.d.ts +20 -0
  44. package/dist/stores/mikro-orm-read.store.js +120 -0
  45. package/dist/transform-runner.d.ts +54 -0
  46. package/dist/transform-runner.js +280 -0
  47. package/package.json +54 -0
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var SubprocessTransformRunner_1;
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.SubprocessTransformRunner = void 0;
14
+ const node_child_process_1 = require("node:child_process");
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const common_1 = require("@nestjs/common");
18
+ const DEFAULT_TIMEOUT_MS = 30_000;
19
+ const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
20
+ /** Packages worth telling the author about, if the environment has them. */
21
+ const REPORTED_PACKAGES = ['pandas', 'numpy', 'pyarrow', 'requests'];
22
+ /**
23
+ * Runs a transform in a child process, with a clock on it.
24
+ *
25
+ * **This is not a security boundary.** It stops accidents — an infinite loop, a
26
+ * runaway allocation, a stray read of `process.env.DATABASE_PASSWORD` — because
27
+ * the child gets a timeout and an empty environment. It does not stop code
28
+ * written to escape it: a child process can still open sockets and read the
29
+ * filesystem as whatever user the service runs as.
30
+ *
31
+ * That is a deliberate trade for the case this is built for, where transforms
32
+ * are written by the same people who already have database access. A catalog
33
+ * that accepts transforms from anyone else needs a container, gVisor, or a WASM
34
+ * runtime — and `TransformRunner` is an interface precisely so that swap is a
35
+ * provider change rather than a rewrite.
36
+ *
37
+ * `node:vm` was the other option and is worse on both counts: it is famously
38
+ * not an isolation boundary either, and it cannot be killed mid-loop.
39
+ */
40
+ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTransformRunner {
41
+ options;
42
+ logger = new common_1.Logger(SubprocessTransformRunner_1.name);
43
+ pythonPath;
44
+ packages;
45
+ constructor(options = {}) {
46
+ this.options = options;
47
+ }
48
+ async available() {
49
+ // TypeScript is Node's own type stripping, so it is available exactly when
50
+ // JavaScript is — no compiler, no build step, no extra dependency.
51
+ const languages = ['javascript', 'typescript'];
52
+ if (await this.resolvePython())
53
+ languages.push('python');
54
+ return languages;
55
+ }
56
+ /**
57
+ * Which Python libraries a transform can import here.
58
+ *
59
+ * Reported rather than assumed. "pandas is available" is a property of the
60
+ * image, and a UI that promises it on an image without it turns a deployment
61
+ * difference into a runtime traceback the author cannot act on.
62
+ */
63
+ async pythonPackages() {
64
+ if (this.packages !== undefined)
65
+ return this.packages;
66
+ const python = await this.resolvePython();
67
+ if (!python) {
68
+ this.packages = [];
69
+ return this.packages;
70
+ }
71
+ const probe = REPORTED_PACKAGES.map((name) => `try:\n __import__("${name}")\n found.append("${name}")\nexcept Exception:\n pass`).join('\n');
72
+ try {
73
+ const { stdout } = await this.spawn(python, ['-c', `found = []\n${probe}\nprint(",".join(found))`], '', 10_000);
74
+ this.packages = stdout.trim() ? stdout.trim().split(',') : [];
75
+ }
76
+ catch {
77
+ this.packages = [];
78
+ }
79
+ if (this.packages.length > 0) {
80
+ this.logger.log(`Python transforms may import: ${this.packages.join(', ')}`);
81
+ }
82
+ return this.packages;
83
+ }
84
+ async run(transform, records, options = {}) {
85
+ const started = Date.now();
86
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
87
+ const python = transform.language === 'python';
88
+ const interpreter = python ? await this.resolvePython() : process.execPath;
89
+ if (!interpreter) {
90
+ throw new Error('No python3 on PATH, so python transforms cannot run here. Use javascript or typescript, or install python in the image.');
91
+ }
92
+ const script = python ? pythonHarness(transform.code) : javascriptHarness(transform.code);
93
+ // `module-typescript` is Node's own stripping — types are erased, never
94
+ // checked. A transform with a wrong type still runs; the editor's try pane
95
+ // is what catches it, not the compiler.
96
+ const args = python
97
+ ? ['-c', script]
98
+ : [
99
+ '--input-type',
100
+ transform.language === 'typescript' ? 'module-typescript' : 'module',
101
+ '-e',
102
+ script,
103
+ ];
104
+ const { stdout, stderr } = await this.spawn(interpreter, args, JSON.stringify(records), timeoutMs);
105
+ let parsed;
106
+ try {
107
+ // The harness prints exactly one JSON line last; anything the code wrote
108
+ // to stdout itself would corrupt that, which is why logs are captured.
109
+ parsed = JSON.parse(stdout.trim().split('\n').pop() ?? '{}');
110
+ }
111
+ catch {
112
+ throw new Error(`The transform did not return anything readable. stderr: ${stderr.slice(0, 500)}`);
113
+ }
114
+ if (parsed.error)
115
+ throw new Error(parsed.error);
116
+ if (!Array.isArray(parsed.rows)) {
117
+ throw new Error('The transform must return an array of rows. Returning anything else would leave the load ambiguous.');
118
+ }
119
+ return {
120
+ rows: parsed.rows.filter((row) => typeof row === 'object' && row !== null && !Array.isArray(row)),
121
+ logs: Array.isArray(parsed.logs) ? parsed.logs.map(String) : [],
122
+ elapsedMs: Date.now() - started,
123
+ };
124
+ }
125
+ spawn(command, args, input, timeoutMs) {
126
+ return new Promise((resolve, reject) => {
127
+ const child = (0, node_child_process_1.spawn)(command, args, {
128
+ // An empty environment, not the parent's. A transform has no business
129
+ // reading the database password, and inheriting env is how it would.
130
+ env: { PATH: process.env.PATH ?? '', NODE_ENV: 'production' },
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ });
133
+ let stdout = '';
134
+ let stderr = '';
135
+ let settled = false;
136
+ const timer = setTimeout(() => {
137
+ if (settled)
138
+ return;
139
+ settled = true;
140
+ child.kill('SIGKILL');
141
+ reject(new Error(`The transform ran for longer than ${timeoutMs}ms and was stopped.`));
142
+ }, timeoutMs);
143
+ child.stdout.on('data', (chunk) => {
144
+ stdout += chunk.toString();
145
+ if (stdout.length > MAX_OUTPUT_BYTES)
146
+ child.kill('SIGKILL');
147
+ });
148
+ child.stderr.on('data', (chunk) => {
149
+ stderr += chunk.toString();
150
+ });
151
+ child.on('error', (error) => {
152
+ if (settled)
153
+ return;
154
+ settled = true;
155
+ clearTimeout(timer);
156
+ reject(error);
157
+ });
158
+ child.on('close', (code) => {
159
+ if (settled)
160
+ return;
161
+ settled = true;
162
+ clearTimeout(timer);
163
+ if (code !== 0 && stdout.trim().length === 0) {
164
+ reject(new Error(`The transform exited with code ${code}. ${stderr.slice(0, 500)}`));
165
+ return;
166
+ }
167
+ resolve({ stdout, stderr });
168
+ });
169
+ child.stdin.write(input);
170
+ child.stdin.end();
171
+ });
172
+ }
173
+ /** Cached, including the negative answer — probing on every run is wasteful. */
174
+ async resolvePython() {
175
+ if (this.pythonPath !== undefined)
176
+ return this.pythonPath;
177
+ const venv = this.options.pythonVenv ?? process.env.CATALOG_PYTHON_VENV;
178
+ if (venv) {
179
+ const candidate = (0, node_path_1.join)(venv, 'bin', 'python');
180
+ if ((0, node_fs_1.existsSync)(candidate)) {
181
+ this.pythonPath = candidate;
182
+ this.logger.log(`Python transforms run in the venv at ${venv}`);
183
+ return candidate;
184
+ }
185
+ this.logger.warn(`CATALOG_PYTHON_VENV points at ${venv} but there is no python there — falling back to PATH, which will not have the venv's libraries.`);
186
+ }
187
+ for (const candidate of ['python3', 'python']) {
188
+ const ok = await new Promise((resolve) => {
189
+ const probe = (0, node_child_process_1.spawn)(candidate, ['--version'], { stdio: 'ignore' });
190
+ probe.on('error', () => resolve(false));
191
+ probe.on('close', (code) => resolve(code === 0));
192
+ });
193
+ if (ok) {
194
+ this.pythonPath = candidate;
195
+ return candidate;
196
+ }
197
+ }
198
+ this.pythonPath = null;
199
+ return null;
200
+ }
201
+ };
202
+ exports.SubprocessTransformRunner = SubprocessTransformRunner;
203
+ exports.SubprocessTransformRunner = SubprocessTransformRunner = SubprocessTransformRunner_1 = __decorate([
204
+ (0, common_1.Injectable)(),
205
+ __metadata("design:paramtypes", [Object])
206
+ ], SubprocessTransformRunner);
207
+ /**
208
+ * The JavaScript and TypeScript harness.
209
+ *
210
+ * `console.log` is captured rather than left on stdout so user code cannot
211
+ * corrupt the single JSON line this prints — a transform that logs a `{` would
212
+ * otherwise break its own result parsing, which is a maddening thing to debug.
213
+ */
214
+ function javascriptHarness(code) {
215
+ return `
216
+ const logs = [];
217
+ const write = (...args) => logs.push(args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
218
+ console.log = write; console.info = write; console.warn = write; console.error = write;
219
+
220
+ let input = "";
221
+ process.stdin.setEncoding("utf8");
222
+ for await (const chunk of process.stdin) input += chunk;
223
+
224
+ try {
225
+ const records = JSON.parse(input || "[]");
226
+ const transform = async (records) => { ${code} };
227
+ const rows = await transform(records);
228
+ process.stdout.write(JSON.stringify({ rows: rows ?? [], logs }));
229
+ } catch (error) {
230
+ process.stdout.write(JSON.stringify({
231
+ error: error instanceof Error ? \`\${error.name}: \${error.message}\` : String(error),
232
+ logs,
233
+ }));
234
+ }
235
+ `;
236
+ }
237
+ /**
238
+ * The Python harness. `records` in, a list of dicts out.
239
+ *
240
+ * A DataFrame is accepted as a return value and converted, because a transform
241
+ * that reaches for pandas will naturally end with one — making it write
242
+ * `.to_dict("records")` would be a papercut on the only path pandas is worth
243
+ * importing for.
244
+ */
245
+ function pythonHarness(code) {
246
+ const indented = code
247
+ .split('\n')
248
+ .map((line) => ` ${line}`)
249
+ .join('\n');
250
+ return `
251
+ import sys, json
252
+
253
+ logs = []
254
+ def log(*args):
255
+ logs.append(" ".join(str(a) for a in args))
256
+
257
+ def transform(records):
258
+ ${indented || ' return records'}
259
+
260
+ def to_rows(result):
261
+ if result is None:
262
+ return []
263
+ # A DataFrame, without importing pandas to find out — checking for the
264
+ # method keeps this working when pandas is not installed at all.
265
+ if hasattr(result, "to_dict") and hasattr(result, "columns"):
266
+ return result.to_dict("records")
267
+ return result
268
+
269
+ try:
270
+ raw = sys.stdin.read()
271
+ records = json.loads(raw) if raw.strip() else []
272
+ rows = to_rows(transform(records))
273
+ sys.stdout.write(json.dumps(rows and {"rows": rows, "logs": logs} or {"rows": [], "logs": logs}, default=str))
274
+ except Exception as error:
275
+ sys.stdout.write(json.dumps({
276
+ "error": "{}: {}".format(type(error).__name__, error),
277
+ "logs": logs,
278
+ }))
279
+ `;
280
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@dudousxd/nestjs-catalog",
3
+ "version": "0.1.0",
4
+ "description": "A metadata registry for NestJS: object types, properties and relations, derived from your ORM and enriched with decorators.",
5
+ "license": "MIT",
6
+ "author": "Davide Carvalho",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/DavideCarvalho/nestjs-catalog.git"
10
+ },
11
+ "keywords": [
12
+ "nestjs",
13
+ "catalog",
14
+ "metadata",
15
+ "schema",
16
+ "mikro-orm"
17
+ ],
18
+ "main": "dist/index.js",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./client": {
26
+ "types": "./dist/client.d.ts",
27
+ "default": "./dist/client.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "peerDependencies": {
34
+ "@mikro-orm/core": ">=6",
35
+ "@nestjs/common": ">=10",
36
+ "@nestjs/core": ">=10",
37
+ "reflect-metadata": ">=0.1.13"
38
+ },
39
+ "devDependencies": {
40
+ "@mikro-orm/core": "7.0.17",
41
+ "@nestjs/common": "11.1.19",
42
+ "@nestjs/core": "11.1.19",
43
+ "@types/node": "25.6.0",
44
+ "typescript": "5.9.3",
45
+ "@dudousxd/nestjs-diagnostics": "0.7.0"
46
+ },
47
+ "dependencies": {
48
+ "@dudousxd/nestjs-diagnostics": "0.7.0"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.json",
52
+ "dev": "tsc -p tsconfig.json --watch"
53
+ }
54
+ }