@martel/calyx 1.6.0 → 1.8.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.8.0](https://github.com/bmartel/calyx/compare/v1.7.0...v1.8.0) (2026-07-01)
2
+
3
+
4
+ ### Features
5
+
6
+ * **graphql,openapi:** implement native code-first GraphQLModule and SwaggerModule spec/UI serving ([705c0c0](https://github.com/bmartel/calyx/commit/705c0c0a821cb9e7133d2ad1f1b2103f3aa57572))
7
+
8
+ # [1.7.0](https://github.com/bmartel/calyx/compare/v1.6.0...v1.7.0) (2026-07-01)
9
+
10
+
11
+ ### Features
12
+
13
+ * **cache,validation:** implement SQLite CacheModule, JIT ValidationPipe, and JIT Response Serializer ([1d37b68](https://github.com/bmartel/calyx/commit/1d37b68c2a7ba52e7878810b63f75ee31becc433))
14
+
1
15
  # [1.6.0](https://github.com/bmartel/calyx/compare/v1.5.0...v1.6.0) (2026-07-01)
2
16
 
3
17
 
package/bun.lock CHANGED
@@ -5,6 +5,7 @@
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": {
@@ -322,6 +323,8 @@
322
323
 
323
324
  "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
324
325
 
326
+ "graphql": ["graphql@17.0.1", "", {}, "sha512-8eWbg5Zcv/8o20nzEjHUGPTj20MLFJjc5kagbIPxbaeGxvFwpitJhemEC/k17n5+UD4M/9ea5rTuce78mELujQ=="],
327
+
325
328
  "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
329
 
327
330
  "has-async-hooks": ["has-async-hooks@1.0.0", "", {}, "sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw=="],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martel/calyx",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "High-performance Bun-native NestJS-compatible framework",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -12,6 +12,7 @@
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": {
@@ -0,0 +1,32 @@
1
+ import { NestInterceptor, ExecutionContext, CallHandler } from '../lifecycle/interfaces.ts';
2
+ import { Injectable } from '../core/decorators.ts';
3
+ import { CacheService } from './cache.service.ts';
4
+
5
+ @Injectable()
6
+ export class CacheInterceptor implements NestInterceptor {
7
+ constructor(private readonly cacheService: CacheService) {}
8
+
9
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
10
+ const type = context.getType();
11
+ if (type !== 'http') {
12
+ return next.handle();
13
+ }
14
+
15
+ const req = context.switchToHttp().getRequest<Request>();
16
+ if (req.method !== 'GET') {
17
+ return next.handle();
18
+ }
19
+
20
+ const url = new URL(req.url);
21
+ const key = `http_cache::${url.pathname}${url.search}`;
22
+
23
+ const cached = await this.cacheService.get(key);
24
+ if (cached !== undefined) {
25
+ return cached;
26
+ }
27
+
28
+ const result = await next.handle();
29
+ await this.cacheService.set(key, result);
30
+ return result;
31
+ }
32
+ }
@@ -0,0 +1,31 @@
1
+ import { Module, DynamicModule } from '../core/decorators.ts';
2
+ import { CacheService } from './cache.service.ts';
3
+ import { CacheInterceptor } from './cache.interceptor.ts';
4
+
5
+ export interface CacheModuleOptions {
6
+ dbPath?: string;
7
+ defaultTtl?: number;
8
+ isGlobal?: boolean;
9
+ }
10
+
11
+ @Module({
12
+ providers: [CacheService, CacheInterceptor],
13
+ exports: [CacheService, CacheInterceptor],
14
+ })
15
+ export class CacheModule {
16
+ static register(options: CacheModuleOptions = {}): DynamicModule {
17
+ const cacheServiceInstance = new CacheService(options);
18
+
19
+ const cacheServiceProvider = {
20
+ provide: CacheService,
21
+ useValue: cacheServiceInstance,
22
+ };
23
+
24
+ return {
25
+ module: CacheModule,
26
+ providers: [cacheServiceProvider, CacheInterceptor],
27
+ exports: [CacheService, CacheInterceptor],
28
+ global: options.isGlobal ?? false,
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,86 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { Injectable } from '../core/decorators.ts';
3
+
4
+ @Injectable()
5
+ export class CacheService {
6
+ private db!: Database;
7
+ private pruneTimer: any;
8
+ private readonly dbPath: string;
9
+ private readonly defaultTtl: number;
10
+
11
+ constructor(options: { dbPath?: string; defaultTtl?: number } = {}) {
12
+ this.dbPath = options.dbPath ?? ':memory:';
13
+ this.defaultTtl = options.defaultTtl ?? 5;
14
+ }
15
+
16
+ onModuleInit() {
17
+ this.db = new Database(this.dbPath);
18
+ this.db.run(`
19
+ CREATE TABLE IF NOT EXISTS cache (
20
+ key TEXT PRIMARY KEY,
21
+ value TEXT,
22
+ expires_at INTEGER
23
+ )
24
+ `);
25
+ this.db.run(`CREATE INDEX IF NOT EXISTS idx_expires_at ON cache (expires_at)`);
26
+
27
+ this.pruneTimer = setInterval(() => this.prune(), 30000);
28
+ }
29
+
30
+ onModuleDestroy() {
31
+ if (this.pruneTimer) {
32
+ clearInterval(this.pruneTimer);
33
+ }
34
+ if (this.db) {
35
+ this.db.close();
36
+ }
37
+ }
38
+
39
+ async get<T = any>(key: string): Promise<T | undefined> {
40
+ const row = this.db.query('SELECT value, expires_at FROM cache WHERE key = $key').get({ $key: key }) as any;
41
+ if (!row) return undefined;
42
+
43
+ if (row.expires_at !== null && row.expires_at < Date.now()) {
44
+ this.db.query('DELETE FROM cache WHERE key = $key').run({ $key: key });
45
+ return undefined;
46
+ }
47
+
48
+ try {
49
+ return JSON.parse(row.value) as T;
50
+ } catch {
51
+ return row.value as unknown as T;
52
+ }
53
+ }
54
+
55
+ async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {
56
+ const ttlSeconds = ttl !== undefined ? ttl : this.defaultTtl;
57
+ const expiresAt = ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : null;
58
+ const valStr = JSON.stringify(value);
59
+
60
+ this.db.query(`
61
+ INSERT INTO cache (key, value, expires_at)
62
+ VALUES ($key, $val, $expiresAt)
63
+ ON CONFLICT(key) DO UPDATE SET value = $val, expires_at = $expiresAt
64
+ `).run({
65
+ $key: key,
66
+ $val: valStr,
67
+ $expiresAt: expiresAt,
68
+ });
69
+ }
70
+
71
+ async del(key: string): Promise<void> {
72
+ this.db.query('DELETE FROM cache WHERE key = $key').run({ $key: key });
73
+ }
74
+
75
+ async reset(): Promise<void> {
76
+ this.db.run('DELETE FROM cache');
77
+ }
78
+
79
+ private prune() {
80
+ try {
81
+ this.db.query('DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at < $now').run({ $now: Date.now() });
82
+ } catch {
83
+ // ignore
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ export * from './cache.service.ts';
2
+ export * from './cache.interceptor.ts';
3
+ export * from './cache.module.ts';
@@ -0,0 +1,62 @@
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
+ }
@@ -0,0 +1,166 @@
1
+ import { CalyxContainer } from '../core/container.ts';
2
+ import { Module } from '../core/decorators.ts';
3
+ import {
4
+ GraphQLSchema,
5
+ GraphQLObjectType,
6
+ GraphQLString,
7
+ GraphQLInt,
8
+ GraphQLFloat,
9
+ GraphQLBoolean,
10
+ GraphQLList,
11
+ GraphQLNonNull,
12
+ } from 'graphql';
13
+
14
+ @Module({})
15
+ export class GraphQLModule {
16
+ static buildSchema(container: CalyxContainer): GraphQLSchema | null {
17
+ const instances = container.getProviderAndControllerInstances();
18
+ const resolverInstances = instances.filter(
19
+ (inst) => inst && inst.constructor && Reflect.hasMetadata('calyx:resolver', inst.constructor)
20
+ );
21
+
22
+ if (resolverInstances.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ const typeMap = new Map<any, any>();
27
+
28
+ function getGraphQLType(typeClass: any): any {
29
+ if (typeClass === String) return GraphQLString;
30
+ if (typeClass === Number) return GraphQLFloat;
31
+ if (typeClass === Boolean) return GraphQLBoolean;
32
+ if (typeMap.has(typeClass)) return typeMap.get(typeClass);
33
+
34
+ if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:object_type', typeClass)) {
35
+ const gqlObjectType = new GraphQLObjectType({
36
+ 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
+ },
62
+ });
63
+
64
+ typeMap.set(typeClass, gqlObjectType);
65
+ return gqlObjectType;
66
+ }
67
+
68
+ return GraphQLString;
69
+ }
70
+
71
+ const queryFields: any = {};
72
+ const mutationFields: any = {};
73
+
74
+ for (const resolverInstance of resolverInstances) {
75
+ const resolverClass = resolverInstance.constructor;
76
+
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;
82
+ let gqlType = getGraphQLType(returnTypeClass);
83
+ if (Array.isArray(returnTypeClass)) {
84
+ gqlType = new GraphQLList(getGraphQLType(returnTypeClass[0]));
85
+ }
86
+
87
+ const argsMetadata: { parameterIndex: number; name: string }[] =
88
+ Reflect.getMetadata('calyx:args', resolverInstance, query.propertyKey) || [];
89
+ const argsConfig: any = {};
90
+
91
+ const paramTypes = Reflect.getMetadata('design:paramtypes', resolverInstance, query.propertyKey) || [];
92
+ for (const arg of argsMetadata) {
93
+ const paramType = paramTypes[arg.parameterIndex] || String;
94
+ argsConfig[arg.name] = { type: getGraphQLType(paramType) };
95
+ }
96
+
97
+ const queryName = query.options?.name || String(query.propertyKey);
98
+
99
+ queryFields[queryName] = {
100
+ type: gqlType,
101
+ 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
+ },
113
+ };
114
+ }
115
+
116
+ // Resolve fields mapping
117
+ const resolvedType = Reflect.getMetadata('calyx:resolver', resolverClass);
118
+ const fieldResolvers: { propertyKey: string | symbol; typeFunc?: any; options?: any }[] =
119
+ Reflect.getMetadata('calyx:resolve_fields', resolverClass) || [];
120
+
121
+ if (resolvedType && fieldResolvers.length > 0) {
122
+ const targetGqlType = getGraphQLType(resolvedType);
123
+ if (targetGqlType && typeof targetGqlType.getFields === 'function') {
124
+ const targetFields = targetGqlType.getFields();
125
+ for (const fieldRes of fieldResolvers) {
126
+ const fieldName = fieldRes.options?.name || String(fieldRes.propertyKey);
127
+ if (targetFields[fieldName]) {
128
+ targetFields[fieldName].resolve = async (parent: any, args: any, context: any) => {
129
+ const argsMetadata: { parameterIndex: number; name: string }[] =
130
+ Reflect.getMetadata('calyx:args', resolverInstance, fieldRes.propertyKey) || [];
131
+ const params: any[] = [];
132
+ for (const arg of argsMetadata) {
133
+ params[arg.parameterIndex] = args[arg.name];
134
+ }
135
+ const parentParams = Reflect.getMetadata('calyx:parent', resolverInstance, fieldRes.propertyKey) || [];
136
+ for (const idx of parentParams) {
137
+ params[idx] = parent;
138
+ }
139
+ return resolverInstance[fieldRes.propertyKey](...params);
140
+ };
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ if (Object.keys(queryFields).length === 0) {
148
+ return null;
149
+ }
150
+
151
+ return new GraphQLSchema({
152
+ query: new GraphQLObjectType({
153
+ name: 'Query',
154
+ fields: queryFields,
155
+ }),
156
+ ...(Object.keys(mutationFields).length > 0
157
+ ? {
158
+ mutation: new GraphQLObjectType({
159
+ name: 'Mutation',
160
+ fields: mutationFields,
161
+ }),
162
+ }
163
+ : {}),
164
+ });
165
+ }
166
+ }
@@ -0,0 +1,2 @@
1
+ export * from './decorators.ts';
2
+ export * from './graphql.module.ts';
@@ -10,6 +10,7 @@ import { EventEmitter } from '../event-emitter/event-emitter.ts';
10
10
  import { cors, CorsOptions } from '../security/cors.middleware.ts';
11
11
  import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
12
12
  import { CronMatcher } from '../schedule/cron.matcher.ts';
13
+ import { SerializationCompiler } from '../validation/compiler.ts';
13
14
 
14
15
  class ObjectPool<T> {
15
16
  private pool: T[] = [];
@@ -102,6 +103,8 @@ export class CalyxApplication {
102
103
  private sharedWebSockets: any[] = [];
103
104
  private hasWebSockets = false;
104
105
  private serverPort = 3000;
106
+ private graphqlSchema: any = null;
107
+ private isInitialized = false;
105
108
 
106
109
  use(...middlewares: any[]) {
107
110
  this.globalMiddlewares.push(...middlewares);
@@ -137,6 +140,9 @@ export class CalyxApplication {
137
140
  constructor(private rootModule: any) {}
138
141
 
139
142
  async init() {
143
+ if (this.isInitialized) return;
144
+ this.isInitialized = true;
145
+
140
146
  // Bootstrap the dependency injection container
141
147
  this.container.bootstrap(this.rootModule);
142
148
 
@@ -160,9 +166,18 @@ export class CalyxApplication {
160
166
 
161
167
  // Call OnApplicationBootstrap hooks
162
168
  await this.runOnApplicationBootstrap();
169
+
170
+ // Build GraphQL Schema if GraphQLModule is loaded
171
+ try {
172
+ const { GraphQLModule } = await import('../graphql/graphql.module.ts');
173
+ this.graphqlSchema = GraphQLModule.buildSchema(this.container);
174
+ } catch {
175
+ // ignore
176
+ }
163
177
  }
164
178
 
165
179
  private buildRoutes() {
180
+ this.router.clear();
166
181
  const modules = this.container.getModules();
167
182
  for (const [moduleClass, record] of modules.entries()) {
168
183
  for (const controllerClass of record.controllers) {
@@ -397,6 +412,10 @@ export class CalyxApplication {
397
412
  pathname = pathname.substring(0, queryIdx);
398
413
  }
399
414
 
415
+ if (this.graphqlSchema && pathname === '/graphql' && req.method === 'POST') {
416
+ return this.handleGraphQLRequest(req);
417
+ }
418
+
400
419
  const matched = this.router.match(req.method, pathname);
401
420
  if (!matched) {
402
421
  if (this.globalMiddlewares.length > 0) {
@@ -888,6 +907,15 @@ export class CalyxApplication {
888
907
 
889
908
  if (typeof result === 'object') {
890
909
  responseHeaders['content-type'] = 'application/json';
910
+ const constructor = result.constructor;
911
+ if (constructor) {
912
+ const hasRules = Reflect.hasMetadata('calyx:validation_rules', constructor);
913
+ const hasExpose = Reflect.hasMetadata('calyx:expose_properties', constructor);
914
+ if (hasRules || hasExpose) {
915
+ const serialize = SerializationCompiler.compile(constructor);
916
+ return new Response(serialize(result), { status, headers: responseHeaders });
917
+ }
918
+ }
891
919
  return new Response(JSON.stringify(result), { status, headers: responseHeaders });
892
920
  }
893
921
 
@@ -933,8 +961,36 @@ export class CalyxApplication {
933
961
  );
934
962
  }
935
963
 
964
+ private async handleGraphQLRequest(req: Request): Promise<Response> {
965
+ try {
966
+ const body = await req.json() as any;
967
+ const { query, variables } = body;
968
+
969
+ const { graphql } = await import('graphql');
970
+ const result = await graphql({
971
+ schema: this.graphqlSchema,
972
+ source: query,
973
+ variableValues: variables,
974
+ });
975
+
976
+ return new Response(JSON.stringify(result), {
977
+ status: 200,
978
+ headers: { 'content-type': 'application/json' },
979
+ });
980
+ } catch (err: any) {
981
+ return new Response(
982
+ JSON.stringify({ errors: [{ message: err.message }] }),
983
+ {
984
+ status: 200,
985
+ headers: { 'content-type': 'application/json' },
986
+ }
987
+ );
988
+ }
989
+ }
990
+
936
991
  async listen(port: number): Promise<any> {
937
992
  this.serverPort = port;
993
+ this.buildRoutes();
938
994
  await this.init();
939
995
 
940
996
  const fetchHandler = (req: Request, server: any) => {
@@ -1320,6 +1376,10 @@ export class CalyxApplication {
1320
1376
  // ignore non-json
1321
1377
  }
1322
1378
  }
1379
+
1380
+ getRoutes() {
1381
+ return this.router.getRoutes();
1382
+ }
1323
1383
  }
1324
1384
 
1325
1385
 
@@ -5,6 +5,7 @@ import { MicroserviceOptions } from '../microservices/interfaces.ts';
5
5
  export class CalyxFactory {
6
6
  static async create(rootModule: any): Promise<CalyxApplication> {
7
7
  const app = new CalyxApplication(rootModule);
8
+ await app.init();
8
9
  return app;
9
10
  }
10
11
 
@@ -16,8 +16,21 @@ export class RadixRouter<T> {
16
16
  private staticRoutes = new Map<string, T>();
17
17
  private handlersArray: T[] = [];
18
18
  private compiledMatch: ((method: string, path: string) => RouteMatch<T> | null) | null = null;
19
+ private routesList: { method: string; path: string; handler: T }[] = [];
20
+
21
+ getRoutes() {
22
+ return this.routesList;
23
+ }
24
+
25
+ clear() {
26
+ this.root = new RouterNode<T>();
27
+ this.staticRoutes.clear();
28
+ this.routesList = [];
29
+ this.compiledMatch = null;
30
+ }
19
31
 
20
32
  insert(method: string, path: string, handler: T) {
33
+ this.routesList.push({ method, path, handler });
21
34
  const hasParams = path.includes(':') || path.includes('*');
22
35
  if (!hasParams) {
23
36
  this.staticRoutes.set(method.toUpperCase() + ' ' + path, handler);
package/src/index.ts CHANGED
@@ -8,3 +8,6 @@ export * from './security/index.ts';
8
8
  export * from './schedule/index.ts';
9
9
  export * from './websockets/index.ts';
10
10
  export * from './microservices/index.ts';
11
+ export * from './cache/index.ts';
12
+ export * from './validation/index.ts';
13
+ export * from './openapi/index.ts';
@@ -0,0 +1,49 @@
1
+ import 'reflect-metadata';
2
+
3
+ export function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {
4
+ return (target: any, propertyKey?: string | symbol) => {
5
+ const key = 'calyx:api_tags';
6
+ if (propertyKey) {
7
+ Reflect.defineMetadata(key, tags, target, propertyKey);
8
+ } else {
9
+ Reflect.defineMetadata(key, tags, target);
10
+ }
11
+ };
12
+ }
13
+
14
+ export function ApiOperation(options: { summary?: string; description?: string }): MethodDecorator {
15
+ return (target, propertyKey) => {
16
+ Reflect.defineMetadata('calyx:api_operation', options, target, propertyKey);
17
+ };
18
+ }
19
+
20
+ export function ApiResponse(options: {
21
+ status: number;
22
+ description: string;
23
+ type?: any;
24
+ }): MethodDecorator & ClassDecorator {
25
+ return (target: any, propertyKey?: string | symbol) => {
26
+ const key = 'calyx:api_responses';
27
+ if (propertyKey) {
28
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
29
+ existing.push(options);
30
+ Reflect.defineMetadata(key, existing, target, propertyKey);
31
+ } else {
32
+ const existing = Reflect.getOwnMetadata(key, target) || [];
33
+ existing.push(options);
34
+ Reflect.defineMetadata(key, existing, target);
35
+ }
36
+ };
37
+ }
38
+
39
+ export function ApiProperty(options: {
40
+ description?: string;
41
+ type?: any;
42
+ required?: boolean;
43
+ } = {}): PropertyDecorator {
44
+ return (target, propertyKey) => {
45
+ const properties = Reflect.getOwnMetadata('calyx:api_properties', target.constructor) || [];
46
+ properties.push({ propertyKey: String(propertyKey), ...options });
47
+ Reflect.defineMetadata('calyx:api_properties', properties, target.constructor);
48
+ };
49
+ }
@@ -0,0 +1,2 @@
1
+ export * from './decorators.ts';
2
+ export * from './swagger.module.ts';