@martel/calyx 1.8.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 (41) hide show
  1. package/CHANGELOG.md +8 -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 +11 -0
  10. package/package.json +7 -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 +70 -0
  19. package/src/graphql/graphql.module.ts +197 -47
  20. package/src/http/application.ts +330 -70
  21. package/src/http-client/http-client.module.ts +124 -0
  22. package/src/http-client/index.ts +1 -0
  23. package/src/index.ts +14 -0
  24. package/src/logger/index.ts +1 -0
  25. package/src/logger/logger.service.ts +118 -0
  26. package/src/mvc/index.ts +1 -0
  27. package/src/mvc/mvc.ts +22 -0
  28. package/src/openapi/decorators.ts +154 -0
  29. package/src/openapi/swagger.module.ts +172 -20
  30. package/src/queue/queue.module.ts +174 -0
  31. package/src/session/index.ts +1 -0
  32. package/src/session/session.middleware.ts +82 -0
  33. package/src/sse/index.ts +1 -0
  34. package/src/sse/sse.ts +18 -0
  35. package/src/streaming/index.ts +1 -0
  36. package/src/streaming/streamable-file.ts +32 -0
  37. package/src/validation/pipe.ts +79 -10
  38. package/src/versioning/versioning.ts +46 -0
  39. package/tests/graphql.test.ts +68 -4
  40. package/tests/openapi.test.ts +78 -11
  41. package/tests/techniques.test.ts +471 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martel/calyx",
3
- "version": "1.8.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": {
@@ -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', ['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
+ }
@@ -0,0 +1,276 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { Module, DynamicModule, Inject } from '../core/decorators.ts';
3
+
4
+ // Dynamic detection of TypeORM availability
5
+ let isTypeormAvailable = false;
6
+ try {
7
+ // We check if typeorm package is in path
8
+ require.resolve('typeorm');
9
+ isTypeormAvailable = true;
10
+ } catch {
11
+ // ignore
12
+ }
13
+
14
+ // Connection manager for Native SQLite fallback
15
+ export class ConnectionManager {
16
+ private static db: Database | null = null;
17
+
18
+ static getOrCreate(databasePath = ':memory:'): Database {
19
+ if (!this.db) {
20
+ this.db = new Database(databasePath);
21
+ }
22
+ return this.db;
23
+ }
24
+
25
+ static close() {
26
+ if (this.db) {
27
+ this.db.close();
28
+ this.db = null;
29
+ }
30
+ }
31
+ }
32
+
33
+ // Native Sqlite Repository fallback
34
+ class NativeSqliteRepository<Entity extends Record<string, any>> {
35
+ private tableName: string;
36
+
37
+ constructor(private readonly db: Database, private readonly entityClass: any) {
38
+ this.tableName = entityClass.name.toLowerCase();
39
+ this.db.run(`
40
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ data TEXT
43
+ )
44
+ `);
45
+ }
46
+
47
+ private toEntity(row: any): Entity | null {
48
+ if (!row) return null;
49
+ try {
50
+ const obj = JSON.parse(row.data);
51
+ obj.id = row.id;
52
+ const instance = Object.create(this.entityClass.prototype);
53
+ Object.assign(instance, obj);
54
+ return instance;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ async save(entity: Entity | Entity[]): Promise<any> {
61
+ const entities = Array.isArray(entity) ? entity : [entity];
62
+ const saved: Entity[] = [];
63
+
64
+ for (const ent of entities) {
65
+ const dataCopy = { ...ent };
66
+ const id = dataCopy.id;
67
+ delete dataCopy.id;
68
+
69
+ const jsonStr = JSON.stringify(dataCopy);
70
+
71
+ if (id !== undefined && id !== null) {
72
+ this.db.query(`UPDATE ${this.tableName} SET data = $data WHERE id = $id`).run({
73
+ $data: jsonStr,
74
+ $id: id,
75
+ });
76
+ saved.push(ent);
77
+ } else {
78
+ const result = this.db.query(`INSERT INTO ${this.tableName} (data) VALUES ($data) RETURNING id`).get({
79
+ $data: jsonStr,
80
+ }) as any;
81
+ const newEntity = Object.create(this.entityClass.prototype);
82
+ Object.assign(newEntity, ent, { id: result.id });
83
+ saved.push(newEntity);
84
+ }
85
+ }
86
+
87
+ return Array.isArray(entity) ? saved : saved[0];
88
+ }
89
+
90
+ async find(options?: { where?: Partial<Entity> }): Promise<Entity[]> {
91
+ const where = options?.where;
92
+ if (!where || Object.keys(where).length === 0) {
93
+ const rows = this.db.query(`SELECT id, data FROM ${this.tableName}`).all() as any[];
94
+ return rows.map((r) => this.toEntity(r)).filter(Boolean) as Entity[];
95
+ }
96
+
97
+ const conditions: string[] = [];
98
+ const params: Record<string, any> = {};
99
+
100
+ for (const [key, val] of Object.entries(where)) {
101
+ if (key === 'id') {
102
+ conditions.push(`id = $id`);
103
+ params['$id'] = val;
104
+ } else {
105
+ conditions.push(`json_extract(data, '$.${key}') = $${key}`);
106
+ params[`$${key}`] = typeof val === 'object' ? JSON.stringify(val) : val;
107
+ }
108
+ }
109
+
110
+ const sql = `SELECT id, data FROM ${this.tableName} WHERE ${conditions.join(' AND ')}`;
111
+ const rows = this.db.query(sql).all(params) as any[];
112
+ return rows.map((r) => this.toEntity(r)).filter(Boolean) as Entity[];
113
+ }
114
+
115
+ async findOne(options: { where: Partial<Entity> }): Promise<Entity | null> {
116
+ const results = await this.find(options);
117
+ return results.length > 0 ? results[0] : null;
118
+ }
119
+
120
+ async delete(conditions: Partial<Entity> | number | string): Promise<any> {
121
+ if (typeof conditions === 'number' || typeof conditions === 'string') {
122
+ this.db.query(`DELETE FROM ${this.tableName} WHERE id = $id`).run({ $id: Number(conditions) });
123
+ return { affected: 1 };
124
+ }
125
+
126
+ const where = conditions as Record<string, any>;
127
+ if (Object.keys(where).length === 0) {
128
+ this.db.run(`DELETE FROM ${this.tableName}`);
129
+ return { affected: null };
130
+ }
131
+
132
+ const conds: string[] = [];
133
+ const params: Record<string, any> = {};
134
+
135
+ for (const [key, val] of Object.entries(where)) {
136
+ if (key === 'id') {
137
+ conds.push(`id = $id`);
138
+ params['$id'] = val;
139
+ } else {
140
+ conds.push(`json_extract(data, '$.${key}') = $${key}`);
141
+ params[`$${key}`] = val;
142
+ }
143
+ }
144
+
145
+ const sql = `DELETE FROM ${this.tableName} WHERE ${conds.join(' AND ')}`;
146
+ this.db.query(sql).run(params);
147
+ return { affected: null };
148
+ }
149
+
150
+ async update(conditions: Partial<Entity> | number | string, value: Partial<Entity>): Promise<any> {
151
+ const id = typeof conditions === 'number' || typeof conditions === 'string' ? Number(conditions) : conditions.id;
152
+
153
+ if (id !== undefined) {
154
+ const existing = await this.findOne({ where: { id } as any });
155
+ if (existing) {
156
+ const updatedData = { ...existing, ...value };
157
+ await this.save(updatedData);
158
+ }
159
+ return { affected: 1 };
160
+ }
161
+
162
+ const matches = await this.find({ where: conditions as Partial<Entity> });
163
+ for (const match of matches) {
164
+ const updatedData = { ...match, ...value };
165
+ await this.save(updatedData);
166
+ }
167
+ return { affected: matches.length };
168
+ }
169
+ }
170
+
171
+ // Unified Repository Wrapper
172
+ export class Repository<Entity extends Record<string, any>> {
173
+ private repoPromise: Promise<any>;
174
+
175
+ constructor(dbOrDs: any, private readonly entityClass: any, private readonly isNative?: boolean) {
176
+ const promise = dbOrDs instanceof Promise ? dbOrDs : Promise.resolve(dbOrDs);
177
+ const native = isNative ?? true;
178
+ if (native) {
179
+ this.repoPromise = promise.then((db) => new NativeSqliteRepository(db, entityClass));
180
+ } else {
181
+ this.repoPromise = promise.then((ds) => ds.getRepository(entityClass));
182
+ }
183
+ }
184
+
185
+ async save(entity: Entity | Entity[]): Promise<any> {
186
+ const repo = await this.repoPromise;
187
+ return await repo.save(entity);
188
+ }
189
+
190
+ async find(options?: any): Promise<Entity[]> {
191
+ const repo = await this.repoPromise;
192
+ return await repo.find(options);
193
+ }
194
+
195
+ async findOne(options: any): Promise<Entity | null> {
196
+ const repo = await this.repoPromise;
197
+ return await repo.findOne(options);
198
+ }
199
+
200
+ async delete(conditions: any): Promise<any> {
201
+ const repo = await this.repoPromise;
202
+ return await repo.delete(conditions);
203
+ }
204
+
205
+ async update(conditions: any, value: any): Promise<any> {
206
+ const repo = await this.repoPromise;
207
+ return await repo.update(conditions, value);
208
+ }
209
+ }
210
+
211
+ export function InjectRepository(entity: any): ParameterDecorator & PropertyDecorator {
212
+ return Inject(`Repository_${entity.name}`);
213
+ }
214
+
215
+ export interface TypeOrmModuleOptions {
216
+ type?: string;
217
+ database?: string;
218
+ entities?: any[];
219
+ synchronize?: boolean;
220
+ [key: string]: any;
221
+ }
222
+
223
+ @Module({})
224
+ export class TypeOrmModule {
225
+ static forRoot(options: TypeOrmModuleOptions = {}): DynamicModule {
226
+ const isUsingTypeorm = isTypeormAvailable && options.type !== 'sqlite-native';
227
+
228
+ let dbOrDsPromise: Promise<any>;
229
+ if (isUsingTypeorm) {
230
+ dbOrDsPromise = (async () => {
231
+ const { DataSource } = await import('typeorm');
232
+ const ds = new DataSource(options as any);
233
+ await ds.initialize();
234
+ return ds;
235
+ })();
236
+ } else {
237
+ dbOrDsPromise = Promise.resolve(ConnectionManager.getOrCreate(options.database));
238
+ }
239
+
240
+ return {
241
+ module: TypeOrmModule,
242
+ providers: [
243
+ {
244
+ provide: 'Calyx_Database_Connection',
245
+ useValue: dbOrDsPromise,
246
+ },
247
+ {
248
+ provide: 'Calyx_Database_IsNative',
249
+ useValue: !isUsingTypeorm,
250
+ },
251
+ ],
252
+ exports: ['Calyx_Database_Connection', 'Calyx_Database_IsNative'],
253
+ global: true,
254
+ };
255
+ }
256
+
257
+ static forFeature(entities: any[] = []): DynamicModule {
258
+ const providers = entities.map((entity) => {
259
+ return {
260
+ provide: `Repository_${entity.name}`,
261
+ useFactory: (dbOrDsPromise: Promise<any>, isNative: boolean) => {
262
+ const resolvedPromise = dbOrDsPromise ?? Promise.resolve(ConnectionManager.getOrCreate());
263
+ const resolvedIsNative = isNative !== undefined ? isNative : true;
264
+ return new Repository(resolvedPromise, entity, resolvedIsNative);
265
+ },
266
+ inject: ['Calyx_Database_Connection', 'Calyx_Database_IsNative'],
267
+ };
268
+ });
269
+
270
+ return {
271
+ module: TypeOrmModule,
272
+ providers,
273
+ exports: entities.map((entity) => `Repository_${entity.name}`),
274
+ };
275
+ }
276
+ }