@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/catalog.controller.d.ts +8 -0
- package/dist/catalog.controller.js +482 -0
- package/dist/catalog.decorators.d.ts +37 -0
- package/dist/catalog.decorators.js +50 -0
- package/dist/catalog.environment.d.ts +442 -0
- package/dist/catalog.environment.js +645 -0
- package/dist/catalog.events.d.ts +179 -0
- package/dist/catalog.events.js +110 -0
- package/dist/catalog.module.d.ts +5 -0
- package/dist/catalog.module.js +71 -0
- package/dist/catalog.options.d.ts +79 -0
- package/dist/catalog.options.js +4 -0
- package/dist/catalog.overlay-store.d.ts +25 -0
- package/dist/catalog.overlay-store.js +44 -0
- package/dist/catalog.overlay-store.token.d.ts +1 -0
- package/dist/catalog.overlay-store.token.js +4 -0
- package/dist/catalog.pipeline.d.ts +800 -0
- package/dist/catalog.pipeline.js +606 -0
- package/dist/catalog.principal.d.ts +209 -0
- package/dist/catalog.principal.js +245 -0
- package/dist/catalog.query-cache.d.ts +25 -0
- package/dist/catalog.query-cache.js +0 -0
- package/dist/catalog.query.d.ts +76 -0
- package/dist/catalog.query.js +64 -0
- package/dist/catalog.registry.base.d.ts +21 -0
- package/dist/catalog.registry.base.js +17 -0
- package/dist/catalog.registry.d.ts +44 -0
- package/dist/catalog.registry.js +359 -0
- package/dist/catalog.service.d.ts +115 -0
- package/dist/catalog.service.js +366 -0
- package/dist/catalog.store.d.ts +419 -0
- package/dist/catalog.store.js +175 -0
- package/dist/catalog.types.d.ts +165 -0
- package/dist/catalog.types.js +19 -0
- package/dist/catalog.workspace.d.ts +426 -0
- package/dist/catalog.workspace.js +87 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.js +83 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +109 -0
- package/dist/stores/mikro-orm-read.store.d.ts +20 -0
- package/dist/stores/mikro-orm-read.store.js +120 -0
- package/dist/transform-runner.d.ts +54 -0
- package/dist/transform-runner.js +280 -0
- 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
|
+
}
|