@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
@@ -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
+ }
@@ -0,0 +1,93 @@
1
+ import { NestInterceptor, ExecutionContext, CallHandler } from '../lifecycle/interfaces.ts';
2
+ import { createParamDecorator } from '../http/decorators.ts';
3
+ import { Injectable } from '../core/decorators.ts';
4
+
5
+ export interface MulterFile {
6
+ fieldname: string;
7
+ originalname: string;
8
+ encoding: string;
9
+ mimetype: string;
10
+ size: number;
11
+ buffer: Buffer;
12
+ }
13
+
14
+ @Injectable()
15
+ export class FileInterceptor implements NestInterceptor {
16
+ constructor(private readonly fieldName: string) {}
17
+
18
+ async intercept(context: ExecutionContext, next: CallHandler) {
19
+ const ctx = context.switchToHttp();
20
+ const req = ctx.getRequest();
21
+ const contentType = req.headers.get('content-type') || '';
22
+
23
+ if (contentType.includes('multipart/form-data')) {
24
+ try {
25
+ const formData = await req.clone().formData();
26
+ const file = formData.get(this.fieldName);
27
+ if (file && typeof file !== 'string') {
28
+ const buffer = Buffer.from(await file.arrayBuffer());
29
+ (req as any).file = {
30
+ fieldname: this.fieldName,
31
+ originalname: file.name,
32
+ encoding: '7bit',
33
+ mimetype: file.type,
34
+ size: file.size,
35
+ buffer,
36
+ };
37
+ }
38
+ } catch (err) {
39
+ console.error('FileInterceptor parsing error:', err);
40
+ }
41
+ }
42
+
43
+ return next.handle();
44
+ }
45
+ }
46
+
47
+ @Injectable()
48
+ export class FilesInterceptor implements NestInterceptor {
49
+ constructor(private readonly fieldName: string) {}
50
+
51
+ async intercept(context: ExecutionContext, next: CallHandler) {
52
+ const ctx = context.switchToHttp();
53
+ const req = ctx.getRequest();
54
+ const contentType = req.headers.get('content-type') || '';
55
+
56
+ if (contentType.includes('multipart/form-data')) {
57
+ try {
58
+ const formData = await req.clone().formData();
59
+ const files = formData.getAll(this.fieldName);
60
+ const multerFiles: MulterFile[] = [];
61
+
62
+ for (const file of files) {
63
+ if (file && typeof file !== 'string') {
64
+ const buffer = Buffer.from(await file.arrayBuffer());
65
+ multerFiles.push({
66
+ fieldname: this.fieldName,
67
+ originalname: file.name,
68
+ encoding: '7bit',
69
+ mimetype: file.type,
70
+ size: file.size,
71
+ buffer,
72
+ });
73
+ }
74
+ }
75
+ (req as any).files = multerFiles;
76
+ } catch {
77
+ // ignore
78
+ }
79
+ }
80
+
81
+ return next.handle();
82
+ }
83
+ }
84
+
85
+ export const UploadedFile = createParamDecorator((data, ctx) => {
86
+ const req = ctx.switchToHttp().getRequest();
87
+ return (req as any).file;
88
+ });
89
+
90
+ export const UploadedFiles = createParamDecorator((data, ctx) => {
91
+ const req = ctx.switchToHttp().getRequest();
92
+ return (req as any).files;
93
+ });
@@ -0,0 +1 @@
1
+ export * from './file-upload.interceptor.ts';
@@ -0,0 +1,132 @@
1
+ import { METADATA_KEYS } from '../core/metadata.ts';
2
+
3
+ export function Resolver(nameOrClass?: any): ClassDecorator {
4
+ return (target) => {
5
+ Reflect.defineMetadata('calyx:resolver', nameOrClass || target, target);
6
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
7
+ };
8
+ }
9
+
10
+ export function Query(typeFunc?: (returns: any) => any, options?: { name?: string }): MethodDecorator {
11
+ return (target, propertyKey) => {
12
+ const queries = Reflect.getOwnMetadata('calyx:queries', target.constructor) || [];
13
+ queries.push({ propertyKey, typeFunc, options });
14
+ Reflect.defineMetadata('calyx:queries', queries, target.constructor);
15
+ };
16
+ }
17
+
18
+ export function Mutation(typeFunc?: (returns: any) => any, options?: { name?: string }): MethodDecorator {
19
+ return (target, propertyKey) => {
20
+ const mutations = Reflect.getOwnMetadata('calyx:mutations', target.constructor) || [];
21
+ mutations.push({ propertyKey, typeFunc, options });
22
+ Reflect.defineMetadata('calyx:mutations', mutations, target.constructor);
23
+ };
24
+ }
25
+
26
+ export function ResolveField(typeFunc?: (returns: any) => any, options?: { name?: string }): MethodDecorator {
27
+ return (target, propertyKey) => {
28
+ const fields = Reflect.getOwnMetadata('calyx:resolve_fields', target.constructor) || [];
29
+ fields.push({ propertyKey, typeFunc, options });
30
+ Reflect.defineMetadata('calyx:resolve_fields', fields, target.constructor);
31
+ };
32
+ }
33
+
34
+ export function Args(name?: string): ParameterDecorator {
35
+ return (target, propertyKey, parameterIndex) => {
36
+ const args = Reflect.getOwnMetadata('calyx:args', target, propertyKey!) || [];
37
+ args.push({ parameterIndex, name });
38
+ Reflect.defineMetadata('calyx:args', args, target, propertyKey!);
39
+ };
40
+ }
41
+
42
+ export function Parent(): ParameterDecorator {
43
+ return (target, propertyKey, parameterIndex) => {
44
+ const parentParams = Reflect.getOwnMetadata('calyx:parent', target, propertyKey!) || [];
45
+ parentParams.push(parameterIndex);
46
+ Reflect.defineMetadata('calyx:parent', parentParams, target, propertyKey!);
47
+ };
48
+ }
49
+
50
+ export function ObjectType(): ClassDecorator {
51
+ return (target) => {
52
+ Reflect.defineMetadata('calyx:object_type', true, target);
53
+ };
54
+ }
55
+
56
+ export function Field(typeFunc?: (returns: any) => any, options?: { nullable?: boolean }): PropertyDecorator {
57
+ return (target, propertyKey) => {
58
+ const fields = Reflect.getOwnMetadata('calyx:fields', target.constructor) || [];
59
+ fields.push({ propertyKey: String(propertyKey), typeFunc, options });
60
+ Reflect.defineMetadata('calyx:fields', fields, target.constructor);
61
+ };
62
+ }
63
+
64
+ export function InputType(options?: { description?: string }): ClassDecorator {
65
+ return (target) => {
66
+ Reflect.defineMetadata('calyx:input_type', true, target);
67
+ if (options?.description) {
68
+ Reflect.defineMetadata('calyx:description', options.description, target);
69
+ }
70
+ };
71
+ }
72
+
73
+ export function ArgsType(): ClassDecorator {
74
+ return (target) => {
75
+ Reflect.defineMetadata('calyx:args_type', true, target);
76
+ };
77
+ }
78
+
79
+ export function InterfaceType(options?: { description?: string }): ClassDecorator {
80
+ return (target) => {
81
+ Reflect.defineMetadata('calyx:interface_type', true, target);
82
+ if (options?.description) {
83
+ Reflect.defineMetadata('calyx:description', options.description, target);
84
+ }
85
+ };
86
+ }
87
+
88
+ export function UnionType(options: { name: string; types: () => any[] }): ClassDecorator {
89
+ return (target) => {
90
+ Reflect.defineMetadata('calyx:union_type', options, target);
91
+ };
92
+ }
93
+
94
+ export function createUnionType(options: { name: string; types: () => any[]; resolveType?: any }) {
95
+ return {
96
+ name: options.name,
97
+ types: options.types,
98
+ resolveType: options.resolveType,
99
+ __isUnion: true,
100
+ };
101
+ }
102
+
103
+ export function Subscription(typeFunc?: (returns: any) => any, options?: { name?: string; filter?: any; resolve?: any }): MethodDecorator {
104
+ return (target, propertyKey) => {
105
+ const subscriptions = Reflect.getOwnMetadata('calyx:subscriptions', target.constructor) || [];
106
+ subscriptions.push({ propertyKey, typeFunc, options });
107
+ Reflect.defineMetadata('calyx:subscriptions', subscriptions, target.constructor);
108
+ };
109
+ }
110
+
111
+ export function Scalar(name: string, typeFunc?: any): ClassDecorator {
112
+ return (target) => {
113
+ Reflect.defineMetadata('calyx:scalar', { name, typeFunc }, target);
114
+ Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
115
+ };
116
+ }
117
+
118
+ export function Directive(sdl: string): ClassDecorator & MethodDecorator & PropertyDecorator {
119
+ return (target: any, propertyKey?: string | symbol) => {
120
+ const key = 'calyx:directives';
121
+ if (propertyKey) {
122
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
123
+ existing.push(sdl);
124
+ Reflect.defineMetadata(key, existing, target, propertyKey);
125
+ } else {
126
+ const existing = Reflect.getOwnMetadata(key, target) || [];
127
+ existing.push(sdl);
128
+ Reflect.defineMetadata(key, existing, target);
129
+ }
130
+ };
131
+ }
132
+