@martel/calyx 1.7.0 → 1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +71 -27
  3. package/benchmarks/graphql-benchmark.ts +81 -0
  4. package/benchmarks/index.ts +32 -0
  5. package/benchmarks/openapi-benchmark.ts +168 -0
  6. package/benchmarks/serialization-benchmark.ts +52 -0
  7. package/benchmarks/techniques-benchmark.ts +84 -0
  8. package/benchmarks/validation-benchmark.ts +74 -0
  9. package/bun.lock +14 -0
  10. package/package.json +8 -6
  11. package/src/cli/index.ts +19 -3
  12. package/src/compression/compression.middleware.ts +7 -0
  13. package/src/cookies/cookies.ts +69 -0
  14. package/src/database/mongoose.module.ts +250 -0
  15. package/src/database/typeorm.module.ts +276 -0
  16. package/src/file-upload/file-upload.interceptor.ts +93 -0
  17. package/src/file-upload/index.ts +1 -0
  18. package/src/graphql/decorators.ts +132 -0
  19. package/src/graphql/graphql.module.ts +316 -0
  20. package/src/graphql/index.ts +2 -0
  21. package/src/http/application.ts +380 -70
  22. package/src/http/factory.ts +1 -0
  23. package/src/http/router.ts +13 -0
  24. package/src/http-client/http-client.module.ts +124 -0
  25. package/src/http-client/index.ts +1 -0
  26. package/src/index.ts +15 -0
  27. package/src/logger/index.ts +1 -0
  28. package/src/logger/logger.service.ts +118 -0
  29. package/src/mvc/index.ts +1 -0
  30. package/src/mvc/mvc.ts +22 -0
  31. package/src/openapi/decorators.ts +203 -0
  32. package/src/openapi/index.ts +2 -0
  33. package/src/openapi/swagger.module.ts +326 -0
  34. package/src/queue/queue.module.ts +174 -0
  35. package/src/session/index.ts +1 -0
  36. package/src/session/session.middleware.ts +82 -0
  37. package/src/sse/index.ts +1 -0
  38. package/src/sse/sse.ts +18 -0
  39. package/src/streaming/index.ts +1 -0
  40. package/src/streaming/streamable-file.ts +32 -0
  41. package/src/validation/pipe.ts +79 -10
  42. package/src/versioning/versioning.ts +46 -0
  43. package/tests/graphql.test.ts +176 -0
  44. package/tests/openapi.test.ts +162 -0
  45. package/tests/techniques.test.ts +471 -0
package/bun.lock CHANGED
@@ -5,12 +5,14 @@
5
5
  "": {
6
6
  "name": "@martel/calyx",
7
7
  "dependencies": {
8
+ "graphql": "^17.0.1",
8
9
  "reflect-metadata": "^0.2.2",
9
10
  },
10
11
  "devDependencies": {
11
12
  "@nestjs/common": "^11.1.27",
12
13
  "@nestjs/core": "^11.1.27",
13
14
  "@nestjs/platform-express": "^11.1.27",
15
+ "@nestjs/swagger": "^11.4.5",
14
16
  "@semantic-release/changelog": "^6.0.3",
15
17
  "@semantic-release/git": "^10.0.1",
16
18
  "@semantic-release/github": "^12.0.0",
@@ -44,14 +46,20 @@
44
46
 
45
47
  "@lukeed/csprng": ["@lukeed/csprng@1.1.0", "", {}, "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA=="],
46
48
 
49
+ "@microsoft/tsdoc": ["@microsoft/tsdoc@0.16.0", "", {}, "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA=="],
50
+
47
51
  "@minimistjs/subarg": ["@minimistjs/subarg@1.0.0", "", { "dependencies": { "minimist": "^1.1.0" } }, "sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ=="],
48
52
 
49
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=="],
50
54
 
51
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=="],
52
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
+
53
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=="],
54
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
+
55
63
  "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
56
64
 
57
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=="],
@@ -80,6 +88,8 @@
80
88
 
81
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=="],
82
90
 
91
+ "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="],
92
+
83
93
  "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
84
94
 
85
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=="],
@@ -322,6 +332,8 @@
322
332
 
323
333
  "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
324
334
 
335
+ "graphql": ["graphql@17.0.1", "", {}, "sha512-8eWbg5Zcv/8o20nzEjHUGPTj20MLFJjc5kagbIPxbaeGxvFwpitJhemEC/k17n5+UD4M/9ea5rTuce78mELujQ=="],
336
+
325
337
  "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="],
326
338
 
327
339
  "has-async-hooks": ["has-async-hooks@1.0.0", "", {}, "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw=="],
@@ -674,6 +686,8 @@
674
686
 
675
687
  "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="],
676
688
 
689
+ "swagger-ui-dist": ["swagger-ui-dist@5.32.8", "", { "dependencies": { "@scarf/scarf": "=1.4.0" } }, "sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA=="],
690
+
677
691
  "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
678
692
 
679
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.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "High-performance Bun-native NestJS-compatible framework",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -12,21 +12,23 @@
12
12
  "benchmark": "bun run benchmarks/index.ts"
13
13
  },
14
14
  "dependencies": {
15
+ "graphql": "^17.0.1",
15
16
  "reflect-metadata": "^0.2.2"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@nestjs/common": "^11.1.27",
19
20
  "@nestjs/core": "^11.1.27",
20
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",
21
27
  "@types/autocannon": "^7.12.7",
22
28
  "@types/bun": "latest",
23
29
  "autocannon": "^8.0.0",
24
30
  "rxjs": "^7.8.2",
25
- "semantic-release": "^25.0.0",
26
- "@semantic-release/changelog": "^6.0.3",
27
- "@semantic-release/git": "^10.0.1",
28
- "@semantic-release/github": "^12.0.0",
29
- "@semantic-release/npm": "^13.0.0"
31
+ "semantic-release": "^25.0.0"
30
32
  },
31
33
  "publishConfig": {
32
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', ['build', mainPath, '--outdir', './dist', '--target', 'bun'], { stdio: 'inherit' });
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
- const isDevMode = process.env.CALYX_DEV === 'true';
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,7 @@
1
+ export function compression() {
2
+ return (req: any, res: any, next: any) => {
3
+ // Tag the response to enable compression
4
+ res.compressionEnabled = true;
5
+ next();
6
+ };
7
+ }
@@ -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
+ }