@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
@@ -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';
@@ -60,3 +60,73 @@ export function Field(typeFunc?: (returns: any) => any, options?: { nullable?: b
60
60
  Reflect.defineMetadata('calyx:fields', fields, target.constructor);
61
61
  };
62
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
+
@@ -3,6 +3,10 @@ import { Module } from '../core/decorators.ts';
3
3
  import {
4
4
  GraphQLSchema,
5
5
  GraphQLObjectType,
6
+ GraphQLInputObjectType,
7
+ GraphQLScalarType,
8
+ GraphQLInterfaceType,
9
+ GraphQLUnionType,
6
10
  GraphQLString,
7
11
  GraphQLInt,
8
12
  GraphQLFloat,
@@ -24,6 +28,33 @@ export class GraphQLModule {
24
28
  }
25
29
 
26
30
  const typeMap = new Map<any, any>();
31
+ const inputTypeMap = new Map<any, any>();
32
+
33
+ function buildFieldsConfig(typeClass: any, isInput: boolean): any {
34
+ const fieldsMetadata: { propertyKey: string; typeFunc?: any; options?: any }[] =
35
+ Reflect.getMetadata('calyx:fields', typeClass) || [];
36
+
37
+ const fieldsConfig: any = {};
38
+ for (const field of fieldsMetadata) {
39
+ let returnTypeClass = field.typeFunc ? field.typeFunc(null) : undefined;
40
+ if (!returnTypeClass) {
41
+ returnTypeClass = Reflect.getMetadata('design:type', typeClass.prototype, field.propertyKey);
42
+ }
43
+ if (!returnTypeClass) {
44
+ returnTypeClass = String;
45
+ }
46
+
47
+ let gqlType = isInput ? getGraphQLInputType(returnTypeClass) : getGraphQLType(returnTypeClass);
48
+ if (Array.isArray(returnTypeClass)) {
49
+ gqlType = new GraphQLList(isInput ? getGraphQLInputType(returnTypeClass[0]) : getGraphQLType(returnTypeClass[0]));
50
+ }
51
+ if (!field.options?.nullable) {
52
+ gqlType = new GraphQLNonNull(gqlType);
53
+ }
54
+ fieldsConfig[field.propertyKey] = { type: gqlType };
55
+ }
56
+ return fieldsConfig;
57
+ }
27
58
 
28
59
  function getGraphQLType(typeClass: any): any {
29
60
  if (typeClass === String) return GraphQLString;
@@ -31,34 +62,49 @@ export class GraphQLModule {
31
62
  if (typeClass === Boolean) return GraphQLBoolean;
32
63
  if (typeMap.has(typeClass)) return typeMap.get(typeClass);
33
64
 
65
+ // Custom Scalar
66
+ if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:scalar', typeClass)) {
67
+ const scalarMeta = Reflect.getMetadata('calyx:scalar', typeClass);
68
+ const inst = container.get(typeClass) || new typeClass();
69
+ const gqlScalar = new GraphQLScalarType({
70
+ name: scalarMeta.name,
71
+ description: Reflect.getMetadata('calyx:description', typeClass),
72
+ serialize: inst.serialize ? inst.serialize.bind(inst) : (val: any) => val,
73
+ parseValue: inst.parseValue ? inst.parseValue.bind(inst) : (val: any) => val,
74
+ parseLiteral: inst.parseLiteral ? inst.parseLiteral.bind(inst) : undefined,
75
+ });
76
+ typeMap.set(typeClass, gqlScalar);
77
+ return gqlScalar;
78
+ }
79
+
80
+ // Interface
81
+ if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:interface_type', typeClass)) {
82
+ const gqlInterfaceType = new GraphQLInterfaceType({
83
+ name: typeClass.name,
84
+ description: Reflect.getMetadata('calyx:description', typeClass),
85
+ fields: () => buildFieldsConfig(typeClass, false),
86
+ });
87
+ typeMap.set(typeClass, gqlInterfaceType);
88
+ return gqlInterfaceType;
89
+ }
90
+
91
+ // Union
92
+ if (typeClass && (typeClass.__isUnion || (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:union_type', typeClass)))) {
93
+ const unionMeta = typeClass.__isUnion ? typeClass : Reflect.getMetadata('calyx:union_type', typeClass);
94
+ const gqlUnionType = new GraphQLUnionType({
95
+ name: unionMeta.name,
96
+ types: () => unionMeta.types().map((t: any) => getGraphQLType(t)),
97
+ resolveType: unionMeta.resolveType,
98
+ });
99
+ typeMap.set(typeClass, gqlUnionType);
100
+ return gqlUnionType;
101
+ }
102
+
103
+ // ObjectType
34
104
  if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:object_type', typeClass)) {
35
105
  const gqlObjectType = new GraphQLObjectType({
36
106
  name: typeClass.name,
37
- fields: () => {
38
- const fieldsMetadata: { propertyKey: string; typeFunc?: any; options?: any }[] =
39
- Reflect.getMetadata('calyx:fields', typeClass) || [];
40
-
41
- const fieldsConfig: any = {};
42
- for (const field of fieldsMetadata) {
43
- let returnTypeClass = field.typeFunc ? field.typeFunc(null) : undefined;
44
- if (!returnTypeClass) {
45
- returnTypeClass = Reflect.getMetadata('design:type', typeClass.prototype, field.propertyKey);
46
- }
47
- if (!returnTypeClass) {
48
- returnTypeClass = String;
49
- }
50
-
51
- let gqlType = getGraphQLType(returnTypeClass);
52
- if (Array.isArray(returnTypeClass)) {
53
- gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
54
- }
55
- if (!field.options?.nullable) {
56
- gqlType = new GraphQLNonNull(gqlType);
57
- }
58
- fieldsConfig[field.propertyKey] = { type: gqlType };
59
- }
60
- return fieldsConfig;
61
- },
107
+ fields: () => buildFieldsConfig(typeClass, false),
62
108
  });
63
109
 
64
110
  typeMap.set(typeClass, gqlObjectType);
@@ -68,48 +114,132 @@ export class GraphQLModule {
68
114
  return GraphQLString;
69
115
  }
70
116
 
117
+ function getGraphQLInputType(typeClass: any): any {
118
+ if (typeClass === String) return GraphQLString;
119
+ if (typeClass === Number) return GraphQLFloat;
120
+ if (typeClass === Boolean) return GraphQLBoolean;
121
+ if (inputTypeMap.has(typeClass)) return inputTypeMap.get(typeClass);
122
+
123
+ if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:input_type', typeClass)) {
124
+ const gqlInputObjectType = new GraphQLInputObjectType({
125
+ name: typeClass.name,
126
+ fields: () => buildFieldsConfig(typeClass, true),
127
+ });
128
+
129
+ inputTypeMap.set(typeClass, gqlInputObjectType);
130
+ return gqlInputObjectType;
131
+ }
132
+
133
+ return GraphQLString;
134
+ }
135
+
71
136
  const queryFields: any = {};
72
137
  const mutationFields: any = {};
138
+ const subscriptionFields: any = {};
73
139
 
74
140
  for (const resolverInstance of resolverInstances) {
75
141
  const resolverClass = resolverInstance.constructor;
76
142
 
77
- // Queries
78
- const queries: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
79
- Reflect.getMetadata('calyx:queries', resolverClass) || [];
80
- for (const query of queries) {
81
- const returnTypeClass = query.typeFunc ? query.typeFunc(null) : String;
143
+ // Compile helper for queries, mutations, subscriptions
144
+ const compileField = (fieldMeta: { propertyKey: string | symbol; typeFunc?: any; options?: any }, list: any) => {
145
+ const returnTypeClass = fieldMeta.typeFunc ? fieldMeta.typeFunc(null) : String;
82
146
  let gqlType = getGraphQLType(returnTypeClass);
83
147
  if (Array.isArray(returnTypeClass)) {
84
148
  gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
85
149
  }
86
150
 
87
151
  const argsMetadata: { parameterIndex: number; name: string }[] =
88
- Reflect.getMetadata('calyx:args', resolverInstance, query.propertyKey) || [];
152
+ Reflect.getMetadata('calyx:args', resolverInstance, fieldMeta.propertyKey) || [];
89
153
  const argsConfig: any = {};
90
154
 
91
- const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, query.propertyKey) || [];
155
+ const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldMeta.propertyKey) || [];
92
156
  for (const arg of argsMetadata) {
93
157
  const paramType = paramTypes[arg.parameterIndex] || String;
94
- argsConfig[arg.name] = { type: getGraphQLType(paramType) };
158
+ if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
159
+ const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
160
+ for (const f of fieldsMetadata) {
161
+ let fType = f.typeFunc ? f.typeFunc(null) : undefined;
162
+ if (!fType) {
163
+ fType = Reflect.getMetadata('design:type', paramType.prototype, f.propertyKey) || String;
164
+ }
165
+ let gqlFType = getGraphQLInputType(fType);
166
+ if (Array.isArray(fType)) {
167
+ gqlFType = new GraphQLList(getGraphQLInputType(fType[0]));
168
+ }
169
+ if (!f.options?.nullable) {
170
+ gqlFType = new GraphQLNonNull(gqlFType);
171
+ }
172
+ argsConfig[f.propertyKey] = { type: gqlFType };
173
+ }
174
+ } else {
175
+ const argName = arg.name || 'args';
176
+ argsConfig[argName] = { type: getGraphQLInputType(paramType) };
177
+ }
95
178
  }
96
179
 
97
- const queryName = query.options?.name || String(query.propertyKey);
180
+ const fieldName = fieldMeta.options?.name || String(fieldMeta.propertyKey);
98
181
 
99
- queryFields[queryName] = {
182
+ const resolveFn = async (parent: any, args: any, context: any) => {
183
+ const params: any[] = [];
184
+ for (const arg of argsMetadata) {
185
+ const paramType = paramTypes[arg.parameterIndex] || String;
186
+ if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
187
+ const argInst = new paramType();
188
+ const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
189
+ for (const f of fieldsMetadata) {
190
+ argInst[f.propertyKey] = args[f.propertyKey];
191
+ }
192
+ params[arg.parameterIndex] = argInst;
193
+ } else {
194
+ const argName = arg.name || 'args';
195
+ params[arg.parameterIndex] = args[argName];
196
+ }
197
+ }
198
+ const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldMeta.propertyKey) || [];
199
+ for (const idx of parentParams) {
200
+ params[idx] = parent;
201
+ }
202
+ return resolverInstance[fieldMeta.propertyKey](...params);
203
+ };
204
+
205
+ list[fieldName] = {
100
206
  type: gqlType,
101
207
  args: argsConfig,
102
- resolve: async (parent: any, args: any, context: any) => {
103
- const params: any[] = [];
104
- for (const arg of argsMetadata) {
105
- params[arg.parameterIndex] = args[arg.name];
106
- }
107
- const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, query.propertyKey) || [];
108
- for (const idx of parentParams) {
109
- params[idx] = parent;
110
- }
111
- return resolverInstance[query.propertyKey](...params);
112
- },
208
+ resolve: resolveFn,
209
+ };
210
+
211
+ return { argsMetadata, paramTypes };
212
+ };
213
+
214
+ // Queries
215
+ const queries: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
216
+ Reflect.getMetadata('calyx:queries', resolverClass) || [];
217
+ for (const query of queries) {
218
+ compileField(query, queryFields);
219
+ }
220
+
221
+ // Mutations
222
+ const mutations: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
223
+ Reflect.getMetadata('calyx:mutations', resolverClass) || [];
224
+ for (const mutation of mutations) {
225
+ compileField(mutation, mutationFields);
226
+ }
227
+
228
+ // Subscriptions
229
+ const subscriptions: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
230
+ Reflect.getMetadata('calyx:subscriptions', resolverClass) || [];
231
+ for (const sub of subscriptions) {
232
+ const { argsMetadata, paramTypes } = compileField(sub, subscriptionFields);
233
+ const subName = sub.options?.name || String(sub.propertyKey);
234
+
235
+ // Wrap with standard GraphQL subscribe / resolve
236
+ const originalResolve = subscriptionFields[subName].resolve;
237
+ subscriptionFields[subName].subscribe = originalResolve;
238
+ subscriptionFields[subName].resolve = (payload: any) => {
239
+ if (sub.options?.resolve) {
240
+ return sub.options.resolve(payload);
241
+ }
242
+ return payload;
113
243
  };
114
244
  }
115
245
 
@@ -129,8 +259,20 @@ export class GraphQLModule {
129
259
  const argsMetadata: { parameterIndex: number; name: string }[] =
130
260
  Reflect.getMetadata('calyx:args', resolverInstance, fieldRes.propertyKey) || [];
131
261
  const params: any[] = [];
262
+ const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldRes.propertyKey) || [];
132
263
  for (const arg of argsMetadata) {
133
- params[arg.parameterIndex] = args[arg.name];
264
+ const paramType = paramTypes[arg.parameterIndex] || String;
265
+ if (!arg.name && typeof paramType === 'function' && Reflect.hasMetadata('calyx:args_type', paramType)) {
266
+ const argInst = new paramType();
267
+ const fieldsMetadata = Reflect.getMetadata('calyx:fields', paramType) || [];
268
+ for (const f of fieldsMetadata) {
269
+ argInst[f.propertyKey] = args[f.propertyKey];
270
+ }
271
+ params[arg.parameterIndex] = argInst;
272
+ } else {
273
+ const argName = arg.name || 'args';
274
+ params[arg.parameterIndex] = args[argName];
275
+ }
134
276
  }
135
277
  const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldRes.propertyKey) || [];
136
278
  for (const idx of parentParams) {
@@ -161,6 +303,14 @@ export class GraphQLModule {
161
303
  }),
162
304
  }
163
305
  : {}),
306
+ ...(Object.keys(subscriptionFields).length > 0
307
+ ? {
308
+ subscription: new GraphQLObjectType({
309
+ name: 'Subscription',
310
+ fields: subscriptionFields,
311
+ }),
312
+ }
313
+ : {}),
164
314
  });
165
315
  }
166
316
  }