@martel/calyx 1.8.0 → 1.10.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/CHANGELOG.md +15 -0
- package/README.md +71 -27
- package/benchmarks/graphql-benchmark.ts +81 -0
- package/benchmarks/index.ts +32 -0
- package/benchmarks/openapi-benchmark.ts +168 -0
- package/benchmarks/serialization-benchmark.ts +52 -0
- package/benchmarks/techniques-benchmark.ts +84 -0
- package/benchmarks/validation-benchmark.ts +74 -0
- package/bun.lock +11 -0
- package/package.json +7 -6
- package/src/cli/index.ts +19 -3
- package/src/compression/compression.middleware.ts +7 -0
- package/src/cookies/cookies.ts +69 -0
- package/src/database/mongoose.module.ts +250 -0
- package/src/database/typeorm.module.ts +276 -0
- package/src/file-upload/file-upload.interceptor.ts +93 -0
- package/src/file-upload/index.ts +1 -0
- package/src/graphql/decorators.ts +70 -0
- package/src/graphql/graphql.module.ts +401 -57
- package/src/http/application.ts +434 -74
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +14 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/logger.service.ts +118 -0
- package/src/mvc/index.ts +1 -0
- package/src/mvc/mvc.ts +22 -0
- package/src/openapi/decorators.ts +154 -0
- package/src/openapi/swagger.module.ts +172 -20
- package/src/queue/queue.module.ts +174 -0
- package/src/session/index.ts +1 -0
- package/src/session/session.middleware.ts +82 -0
- package/src/sse/index.ts +1 -0
- package/src/sse/sse.ts +18 -0
- package/src/streaming/index.ts +1 -0
- package/src/streaming/streamable-file.ts +32 -0
- package/src/validation/pipe.ts +79 -10
- package/src/versioning/versioning.ts +46 -0
- package/tests/graphql.test.ts +245 -6
- package/tests/openapi.test.ts +78 -11
- package/tests/techniques.test.ts +471 -0
package/bun.lock
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"@nestjs/common": "^11.1.27",
|
|
13
13
|
"@nestjs/core": "^11.1.27",
|
|
14
14
|
"@nestjs/platform-express": "^11.1.27",
|
|
15
|
+
"@nestjs/swagger": "^11.4.5",
|
|
15
16
|
"@semantic-release/changelog": "^6.0.3",
|
|
16
17
|
"@semantic-release/git": "^10.0.1",
|
|
17
18
|
"@semantic-release/github": "^12.0.0",
|
|
@@ -45,14 +46,20 @@
|
|
|
45
46
|
|
|
46
47
|
"@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="],
|
|
47
48
|
|
|
49
|
+
"@microsoft/tsdoc": ["@microsoft/tsdoc@0.16.0", "", {}, "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA=="],
|
|
50
|
+
|
|
48
51
|
"@minimistjs/subarg": ["@minimistjs/subarg@1.0.0", "", { "dependencies": { "minimist": "^1.1.0" } }, "sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ=="],
|
|
49
52
|
|
|
50
53
|
"@nestjs/common": ["@nestjs/common@11.1.27", "", { "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", "load-esm": "1.0.3", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "class-transformer": ">=0.4.1", "class-validator": ">=0.13.2", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg=="],
|
|
51
54
|
|
|
52
55
|
"@nestjs/core": ["@nestjs/core@11.1.27", "", { "dependencies": { "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "8.4.2", "tslib": "2.8.1", "uid": "2.0.2" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/websockets": "^11.0.0", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "optionalPeers": ["@nestjs/microservices", "@nestjs/platform-express", "@nestjs/websockets"] }, "sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg=="],
|
|
53
56
|
|
|
57
|
+
"@nestjs/mapped-types": ["@nestjs/mapped-types@2.1.1", "", { "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "class-transformer": "^0.4.0 || ^0.5.0", "class-validator": "^0.13.0 || ^0.14.0 || ^0.15.0", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "optionalPeers": ["class-transformer", "class-validator"] }, "sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A=="],
|
|
58
|
+
|
|
54
59
|
"@nestjs/platform-express": ["@nestjs/platform-express@11.1.27", "", { "dependencies": { "cors": "2.8.6", "express": "5.2.1", "multer": "2.1.1", "path-to-regexp": "8.4.2", "tslib": "2.8.1" }, "peerDependencies": { "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0" } }, "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ=="],
|
|
55
60
|
|
|
61
|
+
"@nestjs/swagger": ["@nestjs/swagger@11.4.5", "", { "dependencies": { "@microsoft/tsdoc": "0.16.0", "@nestjs/mapped-types": "2.1.1", "js-yaml": "4.3.0", "lodash": "4.18.1", "path-to-regexp": "8.4.2", "swagger-ui-dist": "5.32.8" }, "peerDependencies": { "@fastify/static": "^8.0.0 || ^9.0.0", "@nestjs/common": "^11.0.1", "@nestjs/core": "^11.0.1", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12 || ^0.2.0" }, "optionalPeers": ["@fastify/static", "class-transformer", "class-validator"] }, "sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig=="],
|
|
62
|
+
|
|
56
63
|
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
|
57
64
|
|
|
58
65
|
"@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
|
|
@@ -81,6 +88,8 @@
|
|
|
81
88
|
|
|
82
89
|
"@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.3", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA=="],
|
|
83
90
|
|
|
91
|
+
"@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="],
|
|
92
|
+
|
|
84
93
|
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
|
85
94
|
|
|
86
95
|
"@semantic-release/changelog": ["@semantic-release/changelog@6.0.3", "", { "dependencies": { "@semantic-release/error": "^3.0.0", "aggregate-error": "^3.0.0", "fs-extra": "^11.0.0", "lodash": "^4.17.4" }, "peerDependencies": { "semantic-release": ">=18.0.0" } }, "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag=="],
|
|
@@ -677,6 +686,8 @@
|
|
|
677
686
|
|
|
678
687
|
"supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="],
|
|
679
688
|
|
|
689
|
+
"swagger-ui-dist": ["swagger-ui-dist@5.32.8", "", { "dependencies": { "@scarf/scarf": "=1.4.0" } }, "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA=="],
|
|
690
|
+
|
|
680
691
|
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
|
|
681
692
|
|
|
682
693
|
"temp-dir": ["temp-dir@3.0.0", "", {}, "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martel/calyx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "High-performance Bun-native NestJS-compatible framework",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -19,15 +19,16 @@
|
|
|
19
19
|
"@nestjs/common": "^11.1.27",
|
|
20
20
|
"@nestjs/core": "^11.1.27",
|
|
21
21
|
"@nestjs/platform-express": "^11.1.27",
|
|
22
|
+
"@nestjs/swagger": "^11.4.5",
|
|
23
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
24
|
+
"@semantic-release/git": "^10.0.1",
|
|
25
|
+
"@semantic-release/github": "^12.0.0",
|
|
26
|
+
"@semantic-release/npm": "^13.0.0",
|
|
22
27
|
"@types/autocannon": "^7.12.7",
|
|
23
28
|
"@types/bun": "latest",
|
|
24
29
|
"autocannon": "^8.0.0",
|
|
25
30
|
"rxjs": "^7.8.2",
|
|
26
|
-
"semantic-release": "^25.0.0"
|
|
27
|
-
"@semantic-release/changelog": "^6.0.3",
|
|
28
|
-
"@semantic-release/git": "^10.0.1",
|
|
29
|
-
"@semantic-release/github": "^12.0.0",
|
|
30
|
-
"@semantic-release/npm": "^13.0.0"
|
|
31
|
+
"semantic-release": "^25.0.0"
|
|
31
32
|
},
|
|
32
33
|
"publishConfig": {
|
|
33
34
|
"access": "public",
|
package/src/cli/index.ts
CHANGED
|
@@ -85,7 +85,20 @@ function runBuild(cmdArgs: string[]) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
console.log('Building Calyx application using bun build...');
|
|
88
|
-
const proc = spawnSync('bun', [
|
|
88
|
+
const proc = spawnSync('bun', [
|
|
89
|
+
'build',
|
|
90
|
+
mainPath,
|
|
91
|
+
'--outdir',
|
|
92
|
+
'./dist',
|
|
93
|
+
'--target',
|
|
94
|
+
'bun',
|
|
95
|
+
'--external',
|
|
96
|
+
'mongoose',
|
|
97
|
+
'--external',
|
|
98
|
+
'typeorm',
|
|
99
|
+
'--external',
|
|
100
|
+
'graphql'
|
|
101
|
+
], { stdio: 'inherit' });
|
|
89
102
|
if (proc.status === 0) {
|
|
90
103
|
console.log('Build completed successfully. Output at ./dist/main.js');
|
|
91
104
|
}
|
|
@@ -108,11 +121,14 @@ function runNew(name: string) {
|
|
|
108
121
|
mkdirSync(name, { recursive: true });
|
|
109
122
|
mkdirSync(join(name, 'src'), { recursive: true });
|
|
110
123
|
|
|
111
|
-
|
|
124
|
+
let isDevMode = process.env.CALYX_DEV === 'true';
|
|
125
|
+
const selfPkgPath = join(import.meta.dir, '../../package.json');
|
|
126
|
+
if (existsSync(selfPkgPath)) {
|
|
127
|
+
isDevMode = true; // Auto-detect workspace dev environment
|
|
128
|
+
}
|
|
112
129
|
|
|
113
130
|
let packageVersion = '0.1.0';
|
|
114
131
|
try {
|
|
115
|
-
const selfPkgPath = join(import.meta.dir, '../../package.json');
|
|
116
132
|
if (existsSync(selfPkgPath)) {
|
|
117
133
|
const selfPkg = JSON.parse(readFileSync(selfPkgPath, 'utf-8'));
|
|
118
134
|
if (selfPkg.version) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createParamDecorator } from '../http/decorators.ts';
|
|
2
|
+
|
|
3
|
+
export interface CookieOptions {
|
|
4
|
+
maxAge?: number;
|
|
5
|
+
expires?: Date;
|
|
6
|
+
httpOnly?: boolean;
|
|
7
|
+
secure?: boolean;
|
|
8
|
+
path?: string;
|
|
9
|
+
domain?: string;
|
|
10
|
+
sameSite?: 'lax' | 'strict' | 'none' | boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseCookies(cookieHeader: string): Record<string, string> {
|
|
14
|
+
const cookies: Record<string, string> = {};
|
|
15
|
+
if (!cookieHeader) return cookies;
|
|
16
|
+
const parts = cookieHeader.split(';');
|
|
17
|
+
for (let i = 0; i < parts.length; i++) {
|
|
18
|
+
const part = parts[i];
|
|
19
|
+
const eqIdx = part.indexOf('=');
|
|
20
|
+
if (eqIdx !== -1) {
|
|
21
|
+
const key = part.substring(0, eqIdx).trim();
|
|
22
|
+
const val = part.substring(eqIdx + 1).trim();
|
|
23
|
+
cookies[key] = decodeURIComponent(val);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return cookies;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatCookie(name: string, value: string, options: CookieOptions = {}): string {
|
|
30
|
+
let str = `${name}=${encodeURIComponent(value)}`;
|
|
31
|
+
if (options.maxAge !== undefined) {
|
|
32
|
+
str += `; Max-Age=${options.maxAge}`;
|
|
33
|
+
}
|
|
34
|
+
if (options.expires !== undefined) {
|
|
35
|
+
str += `; Expires=${options.expires.toUTCString()}`;
|
|
36
|
+
}
|
|
37
|
+
if (options.path !== undefined) {
|
|
38
|
+
str += `; Path=${options.path}`;
|
|
39
|
+
} else {
|
|
40
|
+
str += `; Path=/`;
|
|
41
|
+
}
|
|
42
|
+
if (options.domain !== undefined) {
|
|
43
|
+
str += `; Domain=${options.domain}`;
|
|
44
|
+
}
|
|
45
|
+
if (options.secure) {
|
|
46
|
+
str += `; Secure`;
|
|
47
|
+
}
|
|
48
|
+
if (options.httpOnly) {
|
|
49
|
+
str += `; HttpOnly`;
|
|
50
|
+
}
|
|
51
|
+
if (options.sameSite !== undefined) {
|
|
52
|
+
if (options.sameSite === true) {
|
|
53
|
+
str += `; SameSite=Strict`;
|
|
54
|
+
} else if (options.sameSite === 'lax') {
|
|
55
|
+
str += `; SameSite=Lax`;
|
|
56
|
+
} else if (options.sameSite === 'strict') {
|
|
57
|
+
str += `; SameSite=Strict`;
|
|
58
|
+
} else if (options.sameSite === 'none') {
|
|
59
|
+
str += `; SameSite=None`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return str;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Cookies = createParamDecorator((data: string | undefined, ctx) => {
|
|
66
|
+
const req = ctx.switchToHttp().getRequest();
|
|
67
|
+
const cookies = (req as any).cookies || {};
|
|
68
|
+
return data ? cookies[data] : cookies;
|
|
69
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { Module, DynamicModule, Inject } from '../core/decorators.ts';
|
|
3
|
+
import { ConnectionManager } from './typeorm.module.ts';
|
|
4
|
+
|
|
5
|
+
// Dynamic detection of Mongoose availability
|
|
6
|
+
let isMongooseAvailable = false;
|
|
7
|
+
try {
|
|
8
|
+
require.resolve('mongoose');
|
|
9
|
+
isMongooseAvailable = true;
|
|
10
|
+
} catch {
|
|
11
|
+
// ignore
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class QueryPromise<T> {
|
|
15
|
+
constructor(private readonly promise: Promise<T>) {}
|
|
16
|
+
then(onfulfilled?: any, onrejected?: any) {
|
|
17
|
+
return this.promise.then(onfulfilled, onrejected);
|
|
18
|
+
}
|
|
19
|
+
catch(onrejected?: any) {
|
|
20
|
+
return this.promise.catch(onrejected);
|
|
21
|
+
}
|
|
22
|
+
finally(onfn?: any) {
|
|
23
|
+
return this.promise.finally(onfn);
|
|
24
|
+
}
|
|
25
|
+
exec(): Promise<T> {
|
|
26
|
+
return this.promise;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Native Sqlite-backed document store model fallback
|
|
31
|
+
class NativeSqliteModel<T extends Record<string, any>> {
|
|
32
|
+
private tableName: string;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly db: Database, private readonly modelName: string) {
|
|
35
|
+
this.tableName = `mongo_${modelName.toLowerCase()}`;
|
|
36
|
+
this.db.run(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
data TEXT
|
|
40
|
+
)
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private toDoc(row: any): T | null {
|
|
45
|
+
if (!row) return null;
|
|
46
|
+
try {
|
|
47
|
+
const obj = JSON.parse(row.data);
|
|
48
|
+
obj._id = row.id;
|
|
49
|
+
return obj as T;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async create(doc: Partial<T> | Partial<T>[]): Promise<any> {
|
|
56
|
+
const docs = Array.isArray(doc) ? doc : [doc];
|
|
57
|
+
const created: T[] = [];
|
|
58
|
+
for (const d of docs) {
|
|
59
|
+
const dataCopy = { ...d };
|
|
60
|
+
delete dataCopy._id;
|
|
61
|
+
const jsonStr = JSON.stringify(dataCopy);
|
|
62
|
+
const result = this.db.query(`INSERT INTO ${this.tableName} (data) VALUES ($data) RETURNING id`).get({
|
|
63
|
+
$data: jsonStr,
|
|
64
|
+
}) as any;
|
|
65
|
+
created.push({ ...d, _id: result.id } as unknown as T);
|
|
66
|
+
}
|
|
67
|
+
return Array.isArray(doc) ? created : created[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async find(conditions: Partial<T> = {}): Promise<T[]> {
|
|
71
|
+
if (Object.keys(conditions).length === 0) {
|
|
72
|
+
const rows = this.db.query(`SELECT id, data FROM ${this.tableName}`).all() as any[];
|
|
73
|
+
return rows.map((r) => this.toDoc(r)).filter(Boolean) as T[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const conds: string[] = [];
|
|
77
|
+
const params: Record<string, any> = {};
|
|
78
|
+
|
|
79
|
+
for (const [key, val] of Object.entries(conditions)) {
|
|
80
|
+
if (key === '_id') {
|
|
81
|
+
conds.push(`id = $id`);
|
|
82
|
+
params['$id'] = val;
|
|
83
|
+
} else {
|
|
84
|
+
conds.push(`json_extract(data, '$.${key}') = $${key}`);
|
|
85
|
+
params[`$${key}`] = val;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sql = `SELECT id, data FROM ${this.tableName} WHERE ${conds.join(' AND ')}`;
|
|
90
|
+
const rows = this.db.query(sql).all(params) as any[];
|
|
91
|
+
return rows.map((r) => this.toDoc(r)).filter(Boolean) as T[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async findOne(conditions: Partial<T> = {}): Promise<T | null> {
|
|
95
|
+
const list = await this.find(conditions);
|
|
96
|
+
return list.length > 0 ? list[0] : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async updateOne(conditions: Partial<T>, update: Partial<T>): Promise<any> {
|
|
100
|
+
const doc = await this.findOne(conditions);
|
|
101
|
+
if (!doc) return { matchedCount: 0, modifiedCount: 0 };
|
|
102
|
+
|
|
103
|
+
const updatedData = { ...doc, ...update };
|
|
104
|
+
const id = doc._id;
|
|
105
|
+
delete updatedData._id;
|
|
106
|
+
|
|
107
|
+
this.db.query(`UPDATE ${this.tableName} SET data = $data WHERE id = $id`).run({
|
|
108
|
+
$data: JSON.stringify(updatedData),
|
|
109
|
+
$id: id,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { matchedCount: 1, modifiedCount: 1 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async deleteOne(conditions: Partial<T>): Promise<any> {
|
|
116
|
+
const doc = await this.findOne(conditions);
|
|
117
|
+
if (!doc) return { deletedCount: 0 };
|
|
118
|
+
|
|
119
|
+
this.db.query(`DELETE FROM ${this.tableName} WHERE id = $id`).run({ $id: doc._id });
|
|
120
|
+
return { deletedCount: 1 };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Unified Mongoose Model wrapper
|
|
125
|
+
export class Model<T extends Record<string, any>> {
|
|
126
|
+
private modelPromise: Promise<any>;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
connOrDbPromise: Promise<any>,
|
|
130
|
+
private readonly modelName: string,
|
|
131
|
+
private readonly schema: any,
|
|
132
|
+
private readonly isNative: boolean
|
|
133
|
+
) {
|
|
134
|
+
if (isNative) {
|
|
135
|
+
this.modelPromise = connOrDbPromise.then((db) => new NativeSqliteModel(db, modelName));
|
|
136
|
+
} else {
|
|
137
|
+
this.modelPromise = connOrDbPromise.then((conn) => {
|
|
138
|
+
// Compile schema/model using real mongoose connection
|
|
139
|
+
return conn.models[modelName] || conn.model(modelName, schema || {});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
create(doc: Partial<T> | Partial<T>[]): QueryPromise<any> {
|
|
145
|
+
const promise = this.modelPromise.then((model) => model.create(doc));
|
|
146
|
+
return new QueryPromise(promise);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
find(conditions: Partial<T> = {}): QueryPromise<T[]> {
|
|
150
|
+
const promise = this.modelPromise.then(async (model) => {
|
|
151
|
+
const q = model.find(conditions);
|
|
152
|
+
return typeof q.exec === 'function' ? await q.exec() : await q;
|
|
153
|
+
});
|
|
154
|
+
return new QueryPromise(promise);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
findOne(conditions: Partial<T> = {}): QueryPromise<T | null> {
|
|
158
|
+
const promise = this.modelPromise.then(async (model) => {
|
|
159
|
+
const q = model.findOne(conditions);
|
|
160
|
+
return typeof q.exec === 'function' ? await q.exec() : await q;
|
|
161
|
+
});
|
|
162
|
+
return new QueryPromise(promise);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updateOne(conditions: Partial<T>, update: Partial<T>): QueryPromise<any> {
|
|
166
|
+
const promise = this.modelPromise.then(async (model) => {
|
|
167
|
+
const q = model.updateOne(conditions, update);
|
|
168
|
+
return typeof q.exec === 'function' ? await q.exec() : await q;
|
|
169
|
+
});
|
|
170
|
+
return new QueryPromise(promise);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
deleteOne(conditions: Partial<T>): QueryPromise<any> {
|
|
174
|
+
const promise = this.modelPromise.then(async (model) => {
|
|
175
|
+
const q = model.deleteOne(conditions);
|
|
176
|
+
return typeof q.exec === 'function' ? await q.exec() : await q;
|
|
177
|
+
});
|
|
178
|
+
return new QueryPromise(promise);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function InjectModel(name: string): ParameterDecorator & PropertyDecorator {
|
|
183
|
+
return Inject(`Model_${name}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Module({})
|
|
187
|
+
export class MongooseModule {
|
|
188
|
+
static forRoot(uri: string, options: any = {}): DynamicModule {
|
|
189
|
+
const isUsingMongoose = isMongooseAvailable && !options.useNativeFallback;
|
|
190
|
+
|
|
191
|
+
let connOrDbPromise: Promise<any>;
|
|
192
|
+
if (isUsingMongoose) {
|
|
193
|
+
connOrDbPromise = (async () => {
|
|
194
|
+
const mongoose = await import('mongoose');
|
|
195
|
+
const conn = mongoose.createConnection(uri, options);
|
|
196
|
+
// Wait for connection to open
|
|
197
|
+
await new Promise((resolve, reject) => {
|
|
198
|
+
conn.once('open', resolve);
|
|
199
|
+
conn.once('error', reject);
|
|
200
|
+
});
|
|
201
|
+
return conn;
|
|
202
|
+
})();
|
|
203
|
+
} else {
|
|
204
|
+
let dbPath = ':memory:';
|
|
205
|
+
if (uri.startsWith('mongodb://') || uri.startsWith('mongodb+srv://')) {
|
|
206
|
+
const match = uri.match(/\/([a-zA-Z0-9_-]+)(?:\?|$)/);
|
|
207
|
+
if (match && match[1]) {
|
|
208
|
+
dbPath = `${match[1]}.db`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
connOrDbPromise = Promise.resolve(ConnectionManager.getOrCreate(dbPath));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
module: MongooseModule,
|
|
216
|
+
providers: [
|
|
217
|
+
{
|
|
218
|
+
provide: 'Calyx_Mongo_Connection',
|
|
219
|
+
useValue: connOrDbPromise,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
provide: 'Calyx_Mongo_IsNative',
|
|
223
|
+
useValue: !isUsingMongoose,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
exports: ['Calyx_Mongo_Connection', 'Calyx_Mongo_IsNative'],
|
|
227
|
+
global: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
static forFeature(models: { name: string; schema?: any }[] = []): DynamicModule {
|
|
232
|
+
const providers = models.map((m) => {
|
|
233
|
+
return {
|
|
234
|
+
provide: `Model_${m.name}`,
|
|
235
|
+
useFactory: (connOrDbPromise: Promise<any>, isNative: boolean) => {
|
|
236
|
+
const resolvedPromise = connOrDbPromise ?? Promise.resolve(ConnectionManager.getOrCreate());
|
|
237
|
+
const resolvedIsNative = isNative !== undefined ? isNative : true;
|
|
238
|
+
return new Model(resolvedPromise, m.name, m.schema, resolvedIsNative);
|
|
239
|
+
},
|
|
240
|
+
inject: ['Calyx_Mongo_Connection', 'Calyx_Mongo_IsNative'],
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
module: MongooseModule,
|
|
246
|
+
providers,
|
|
247
|
+
exports: models.map((m) => `Model_${m.name}`),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|