@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.
- 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 +14 -0
- package/package.json +8 -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 +132 -0
- package/src/graphql/graphql.module.ts +316 -0
- package/src/graphql/index.ts +2 -0
- package/src/http/application.ts +380 -70
- package/src/http/factory.ts +1 -0
- package/src/http/router.ts +13 -0
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +15 -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 +203 -0
- package/src/openapi/index.ts +2 -0
- package/src/openapi/swagger.module.ts +326 -0
- 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 +176 -0
- package/tests/openapi.test.ts +162 -0
- package/tests/techniques.test.ts +471 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { CalyxContainer } from '../core/container.ts';
|
|
2
|
+
import { Module } from '../core/decorators.ts';
|
|
3
|
+
import {
|
|
4
|
+
GraphQLSchema,
|
|
5
|
+
GraphQLObjectType,
|
|
6
|
+
GraphQLInputObjectType,
|
|
7
|
+
GraphQLScalarType,
|
|
8
|
+
GraphQLInterfaceType,
|
|
9
|
+
GraphQLUnionType,
|
|
10
|
+
GraphQLString,
|
|
11
|
+
GraphQLInt,
|
|
12
|
+
GraphQLFloat,
|
|
13
|
+
GraphQLBoolean,
|
|
14
|
+
GraphQLList,
|
|
15
|
+
GraphQLNonNull,
|
|
16
|
+
} from 'graphql';
|
|
17
|
+
|
|
18
|
+
@Module({})
|
|
19
|
+
export class GraphQLModule {
|
|
20
|
+
static buildSchema(container: CalyxContainer): GraphQLSchema | null {
|
|
21
|
+
const instances = container.getProviderAndControllerInstances();
|
|
22
|
+
const resolverInstances = instances.filter(
|
|
23
|
+
(inst) => inst && inst.constructor && Reflect.hasMetadata('calyx:resolver', inst.constructor)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (resolverInstances.length === 0) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
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
|
+
}
|
|
58
|
+
|
|
59
|
+
function getGraphQLType(typeClass: any): any {
|
|
60
|
+
if (typeClass === String) return GraphQLString;
|
|
61
|
+
if (typeClass === Number) return GraphQLFloat;
|
|
62
|
+
if (typeClass === Boolean) return GraphQLBoolean;
|
|
63
|
+
if (typeMap.has(typeClass)) return typeMap.get(typeClass);
|
|
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
|
|
104
|
+
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:object_type', typeClass)) {
|
|
105
|
+
const gqlObjectType = new GraphQLObjectType({
|
|
106
|
+
name: typeClass.name,
|
|
107
|
+
fields: () => buildFieldsConfig(typeClass, false),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
typeMap.set(typeClass, gqlObjectType);
|
|
111
|
+
return gqlObjectType;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return GraphQLString;
|
|
115
|
+
}
|
|
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
|
+
|
|
136
|
+
const queryFields: any = {};
|
|
137
|
+
const mutationFields: any = {};
|
|
138
|
+
const subscriptionFields: any = {};
|
|
139
|
+
|
|
140
|
+
for (const resolverInstance of resolverInstances) {
|
|
141
|
+
const resolverClass = resolverInstance.constructor;
|
|
142
|
+
|
|
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;
|
|
146
|
+
let gqlType = getGraphQLType(returnTypeClass);
|
|
147
|
+
if (Array.isArray(returnTypeClass)) {
|
|
148
|
+
gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const argsMetadata: { parameterIndex: number; name: string }[] =
|
|
152
|
+
Reflect.getMetadata('calyx:args', resolverInstance, fieldMeta.propertyKey) || [];
|
|
153
|
+
const argsConfig: any = {};
|
|
154
|
+
|
|
155
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldMeta.propertyKey) || [];
|
|
156
|
+
for (const arg of argsMetadata) {
|
|
157
|
+
const paramType = paramTypes[arg.parameterIndex] || String;
|
|
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
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fieldName = fieldMeta.options?.name || String(fieldMeta.propertyKey);
|
|
181
|
+
|
|
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] = {
|
|
206
|
+
type: gqlType,
|
|
207
|
+
args: argsConfig,
|
|
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;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Resolve fields mapping
|
|
247
|
+
const resolvedType = Reflect.getMetadata('calyx:resolver', resolverClass);
|
|
248
|
+
const fieldResolvers: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
|
|
249
|
+
Reflect.getMetadata('calyx:resolve_fields', resolverClass) || [];
|
|
250
|
+
|
|
251
|
+
if (resolvedType && fieldResolvers.length > 0) {
|
|
252
|
+
const targetGqlType = getGraphQLType(resolvedType);
|
|
253
|
+
if (targetGqlType && typeof targetGqlType.getFields === 'function') {
|
|
254
|
+
const targetFields = targetGqlType.getFields();
|
|
255
|
+
for (const fieldRes of fieldResolvers) {
|
|
256
|
+
const fieldName = fieldRes.options?.name || String(fieldRes.propertyKey);
|
|
257
|
+
if (targetFields[fieldName]) {
|
|
258
|
+
targetFields[fieldName].resolve = async (parent: any, args: any, context: any) => {
|
|
259
|
+
const argsMetadata: { parameterIndex: number; name: string }[] =
|
|
260
|
+
Reflect.getMetadata('calyx:args', resolverInstance, fieldRes.propertyKey) || [];
|
|
261
|
+
const params: any[] = [];
|
|
262
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, fieldRes.propertyKey) || [];
|
|
263
|
+
for (const arg of argsMetadata) {
|
|
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
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldRes.propertyKey) || [];
|
|
278
|
+
for (const idx of parentParams) {
|
|
279
|
+
params[idx] = parent;
|
|
280
|
+
}
|
|
281
|
+
return resolverInstance[fieldRes.propertyKey](...params);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (Object.keys(queryFields).length === 0) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return new GraphQLSchema({
|
|
294
|
+
query: new GraphQLObjectType({
|
|
295
|
+
name: 'Query',
|
|
296
|
+
fields: queryFields,
|
|
297
|
+
}),
|
|
298
|
+
...(Object.keys(mutationFields).length > 0
|
|
299
|
+
? {
|
|
300
|
+
mutation: new GraphQLObjectType({
|
|
301
|
+
name: 'Mutation',
|
|
302
|
+
fields: mutationFields,
|
|
303
|
+
}),
|
|
304
|
+
}
|
|
305
|
+
: {}),
|
|
306
|
+
...(Object.keys(subscriptionFields).length > 0
|
|
307
|
+
? {
|
|
308
|
+
subscription: new GraphQLObjectType({
|
|
309
|
+
name: 'Subscription',
|
|
310
|
+
fields: subscriptionFields,
|
|
311
|
+
}),
|
|
312
|
+
}
|
|
313
|
+
: {}),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|