@martel/calyx 1.8.0 → 1.10.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 +11 -0
- package/package.json +7 -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 +70 -0
- package/src/graphql/graphql.module.ts +401 -57
- package/src/http/application.ts +434 -74
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +14 -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 +154 -0
- package/src/openapi/swagger.module.ts +172 -20
- 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 +245 -6
- package/tests/openapi.test.ts +78 -11
- package/tests/techniques.test.ts +471 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Module, DynamicModule, Inject } from '../core/decorators.ts';
|
|
2
|
+
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
3
|
+
|
|
4
|
+
export interface Job<T = any> {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
data: T;
|
|
8
|
+
opts?: any;
|
|
9
|
+
status: 'waiting' | 'active' | 'completed' | 'failed';
|
|
10
|
+
result?: any;
|
|
11
|
+
error?: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PROCESSOR_METADATA_KEY = 'calyx:processor';
|
|
15
|
+
export const PROCESS_METADATA_KEY = 'calyx:process';
|
|
16
|
+
|
|
17
|
+
export function Processor(queueName?: string): ClassDecorator {
|
|
18
|
+
return (target) => {
|
|
19
|
+
Reflect.defineMetadata(PROCESSOR_METADATA_KEY, queueName ?? target.name, target);
|
|
20
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProcessOptions {
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Process(options?: ProcessOptions | string): MethodDecorator {
|
|
29
|
+
return (target, propertyKey) => {
|
|
30
|
+
const name = typeof options === 'string' ? options : options?.name;
|
|
31
|
+
const existing = Reflect.getMetadata(PROCESS_METADATA_KEY, target.constructor) || [];
|
|
32
|
+
existing.push({ name: name ?? '', propertyKey });
|
|
33
|
+
Reflect.defineMetadata(PROCESS_METADATA_KEY, existing, target.constructor);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function InjectQueue(name: string): ParameterDecorator & PropertyDecorator {
|
|
38
|
+
return Inject(`Queue_${name}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class QueueManager {
|
|
42
|
+
private static processors = new Map<string, { instance: any; handlers: { name: string; propertyKey: string | symbol }[] }>();
|
|
43
|
+
private static queues = new Map<string, Queue>();
|
|
44
|
+
|
|
45
|
+
static registerProcessor(queueName: string, instance: any) {
|
|
46
|
+
const handlers = Reflect.getMetadata(PROCESS_METADATA_KEY, instance.constructor) || [];
|
|
47
|
+
this.processors.set(queueName, { instance, handlers });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static getOrCreateQueue(name: string): Queue {
|
|
51
|
+
let q = this.queues.get(name);
|
|
52
|
+
if (!q) {
|
|
53
|
+
q = new Queue(name);
|
|
54
|
+
this.queues.set(name, q);
|
|
55
|
+
}
|
|
56
|
+
return q;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async dispatch(queueName: string, job: Job) {
|
|
60
|
+
const proc = this.processors.get(queueName);
|
|
61
|
+
if (!proc) {
|
|
62
|
+
// No processor registered yet, mark job as completed (dummy/mock queue mode)
|
|
63
|
+
job.status = 'completed';
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
job.status = 'active';
|
|
68
|
+
const handler = proc.handlers.find((h) => h.name === job.name || (h.name === '' && !job.name));
|
|
69
|
+
if (handler) {
|
|
70
|
+
try {
|
|
71
|
+
const res = proc.instance[handler.propertyKey](job);
|
|
72
|
+
if (res instanceof Promise) {
|
|
73
|
+
job.result = await res;
|
|
74
|
+
} else {
|
|
75
|
+
job.result = res;
|
|
76
|
+
}
|
|
77
|
+
job.status = 'completed';
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
job.error = err;
|
|
80
|
+
job.status = 'failed';
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Fallback: match any handler or first one
|
|
84
|
+
const fallback = proc.handlers[0];
|
|
85
|
+
if (fallback) {
|
|
86
|
+
try {
|
|
87
|
+
const res = proc.instance[fallback.propertyKey](job);
|
|
88
|
+
if (res instanceof Promise) {
|
|
89
|
+
job.result = await res;
|
|
90
|
+
} else {
|
|
91
|
+
job.result = res;
|
|
92
|
+
}
|
|
93
|
+
job.status = 'completed';
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
job.error = err;
|
|
96
|
+
job.status = 'failed';
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
job.status = 'completed';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class Queue {
|
|
106
|
+
private jobs: Job[] = [];
|
|
107
|
+
private jobCounter = 0;
|
|
108
|
+
|
|
109
|
+
constructor(public readonly name: string) {}
|
|
110
|
+
|
|
111
|
+
async add<T = any>(name: string, data: T, opts?: any): Promise<Job<T>>;
|
|
112
|
+
async add<T = any>(data: T, opts?: any): Promise<Job<T>>;
|
|
113
|
+
async add<T = any>(first: any, second?: any, third?: any): Promise<Job<T>> {
|
|
114
|
+
let name = '';
|
|
115
|
+
let data: T;
|
|
116
|
+
let opts: any;
|
|
117
|
+
|
|
118
|
+
if (typeof first === 'string') {
|
|
119
|
+
name = first;
|
|
120
|
+
data = second;
|
|
121
|
+
opts = third;
|
|
122
|
+
} else {
|
|
123
|
+
data = first;
|
|
124
|
+
opts = second;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.jobCounter++;
|
|
128
|
+
const job: Job<T> = {
|
|
129
|
+
id: `${this.name}_job_${this.jobCounter}`,
|
|
130
|
+
name,
|
|
131
|
+
data,
|
|
132
|
+
opts,
|
|
133
|
+
status: 'waiting',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
this.jobs.push(job);
|
|
137
|
+
|
|
138
|
+
// Run dispatch asynchronously in the background
|
|
139
|
+
queueMicrotask(() => {
|
|
140
|
+
QueueManager.dispatch(this.name, job);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return job;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getJobs(): Promise<Job[]> {
|
|
147
|
+
return this.jobs;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Module({})
|
|
152
|
+
export class QueueModule {
|
|
153
|
+
static registerQueue(...configs: { name: string }[]): DynamicModule {
|
|
154
|
+
const providers = configs.map((c) => {
|
|
155
|
+
return {
|
|
156
|
+
provide: `Queue_${c.name}`,
|
|
157
|
+
useFactory: () => {
|
|
158
|
+
return QueueManager.getOrCreateQueue(c.name);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
module: QueueModule,
|
|
165
|
+
providers,
|
|
166
|
+
exports: configs.map((c) => `Queue_${c.name}`),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Automatic processor registration on initialization
|
|
171
|
+
onApplicationBootstrap() {
|
|
172
|
+
// Handled in CalyxApplication initialization dynamically
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './session.middleware.ts';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createParamDecorator } from '../http/decorators.ts';
|
|
2
|
+
import { formatCookie } from '../cookies/cookies.ts';
|
|
3
|
+
|
|
4
|
+
export interface SessionData {
|
|
5
|
+
id: string;
|
|
6
|
+
data: Record<string, any>;
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class MemorySessionStore {
|
|
11
|
+
private store = new Map<string, SessionData>();
|
|
12
|
+
|
|
13
|
+
get(sid: string): SessionData | undefined {
|
|
14
|
+
const session = this.store.get(sid);
|
|
15
|
+
if (!session) return undefined;
|
|
16
|
+
if (session.expiresAt < Date.now()) {
|
|
17
|
+
this.store.delete(sid);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return session;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(sid: string, data: Record<string, any>, ttlSeconds: number) {
|
|
24
|
+
this.store.set(sid, {
|
|
25
|
+
id: sid,
|
|
26
|
+
data,
|
|
27
|
+
expiresAt: Date.now() + ttlSeconds * 1000,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
destroy(sid: string) {
|
|
32
|
+
this.store.delete(sid);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const sessionStore = new MemorySessionStore();
|
|
37
|
+
|
|
38
|
+
export interface SessionOptions {
|
|
39
|
+
secret?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
ttl?: number; // TTL in seconds, default 1 day
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function session(options: SessionOptions = {}) {
|
|
45
|
+
const cookieName = options.name ?? 'calyx_sid';
|
|
46
|
+
const ttl = options.ttl ?? 86400; // 1 day
|
|
47
|
+
|
|
48
|
+
return (req: any, res: any, next: any) => {
|
|
49
|
+
const cookies = req.cookies || {};
|
|
50
|
+
let sid = cookies[cookieName];
|
|
51
|
+
let sessionData = sid ? sessionStore.get(sid) : undefined;
|
|
52
|
+
|
|
53
|
+
if (!sessionData) {
|
|
54
|
+
sid = crypto.randomUUID();
|
|
55
|
+
sessionData = {
|
|
56
|
+
id: sid,
|
|
57
|
+
data: {},
|
|
58
|
+
expiresAt: Date.now() + ttl * 1000,
|
|
59
|
+
};
|
|
60
|
+
sessionStore.set(sid, sessionData.data, ttl);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
req.session = sessionData.data;
|
|
64
|
+
|
|
65
|
+
// Attach finalizeSession hook to request
|
|
66
|
+
req.finalizeSession = (responseHeaders: Headers) => {
|
|
67
|
+
sessionStore.set(sid, req.session, ttl);
|
|
68
|
+
responseHeaders.append('Set-Cookie', formatCookie(cookieName, sid, {
|
|
69
|
+
maxAge: ttl,
|
|
70
|
+
httpOnly: true,
|
|
71
|
+
path: '/',
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
next();
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const Session = createParamDecorator((data, ctx) => {
|
|
80
|
+
const req = ctx.switchToHttp().getRequest();
|
|
81
|
+
return (req as any).session;
|
|
82
|
+
});
|
package/src/sse/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sse.ts';
|
package/src/sse/sse.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
3
|
+
|
|
4
|
+
export interface MessageEvent {
|
|
5
|
+
data: string | object;
|
|
6
|
+
id?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
retry?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Sse(path = ''): MethodDecorator {
|
|
12
|
+
return (target: any, propertyKey: string | symbol, descriptor: any) => {
|
|
13
|
+
// SSE routes are GET requests
|
|
14
|
+
Reflect.defineMetadata(METADATA_KEYS.HTTP_METHOD, { method: 'GET', path }, target, propertyKey);
|
|
15
|
+
Reflect.defineMetadata('calyx:sse', true, target, propertyKey);
|
|
16
|
+
return descriptor;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './streamable-file.ts';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface StreamableFileOptions {
|
|
2
|
+
type?: string;
|
|
3
|
+
disposition?: string;
|
|
4
|
+
length?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class StreamableFile {
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly streamOrBuffer: any,
|
|
10
|
+
public readonly options: StreamableFileOptions = {}
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
getStream(): any {
|
|
14
|
+
return this.streamOrBuffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getHeaders(): Record<string, string> {
|
|
18
|
+
const headers: Record<string, string> = {};
|
|
19
|
+
if (this.options.type) {
|
|
20
|
+
headers['content-type'] = this.options.type;
|
|
21
|
+
} else {
|
|
22
|
+
headers['content-type'] = 'application/octet-stream';
|
|
23
|
+
}
|
|
24
|
+
if (this.options.disposition) {
|
|
25
|
+
headers['content-disposition'] = this.options.disposition;
|
|
26
|
+
}
|
|
27
|
+
if (this.options.length !== undefined) {
|
|
28
|
+
headers['content-length'] = String(this.options.length);
|
|
29
|
+
}
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/validation/pipe.ts
CHANGED
|
@@ -3,25 +3,94 @@ import { Injectable } from '../core/decorators.ts';
|
|
|
3
3
|
import { HttpException } from '../http/exceptions.ts';
|
|
4
4
|
import { ValidationCompiler } from './compiler.ts';
|
|
5
5
|
|
|
6
|
+
// Dynamic detection of class-validator and class-transformer
|
|
7
|
+
let classValidator: any = null;
|
|
8
|
+
let classTransformer: any = null;
|
|
9
|
+
try {
|
|
10
|
+
require.resolve('class-validator');
|
|
11
|
+
require.resolve('class-transformer');
|
|
12
|
+
classValidator = require('class-validator');
|
|
13
|
+
classTransformer = require('class-transformer');
|
|
14
|
+
} catch {
|
|
15
|
+
// ignore
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ValidationPipeOptions {
|
|
19
|
+
transform?: boolean;
|
|
20
|
+
disableErrorMessages?: boolean;
|
|
21
|
+
whitelist?: boolean;
|
|
22
|
+
forbidNonWhitelisted?: boolean;
|
|
23
|
+
groups?: string[];
|
|
24
|
+
dismissDefaultMessages?: boolean;
|
|
25
|
+
validationError?: {
|
|
26
|
+
target?: boolean;
|
|
27
|
+
value?: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
@Injectable()
|
|
7
32
|
export class ValidationPipe implements PipeTransform {
|
|
33
|
+
private transformOption: boolean;
|
|
34
|
+
private whitelistOption: boolean;
|
|
35
|
+
private forbidNonWhitelistedOption: boolean;
|
|
36
|
+
|
|
37
|
+
constructor(options: ValidationPipeOptions = {}) {
|
|
38
|
+
this.transformOption = options.transform ?? true;
|
|
39
|
+
this.whitelistOption = options.whitelist ?? false;
|
|
40
|
+
this.forbidNonWhitelistedOption = options.forbidNonWhitelisted ?? false;
|
|
41
|
+
}
|
|
42
|
+
|
|
8
43
|
async transform(value: any, metadata: ArgumentMetadata) {
|
|
9
44
|
const metatype = metadata.metatype;
|
|
10
45
|
if (!metatype || this.toValidate(metatype)) {
|
|
11
46
|
return value;
|
|
12
47
|
}
|
|
13
48
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
49
|
+
if (classValidator && classTransformer) {
|
|
50
|
+
try {
|
|
51
|
+
const object = classTransformer.plainToInstance(metatype, value);
|
|
52
|
+
const errors = await classValidator.validate(object, {
|
|
53
|
+
whitelist: this.whitelistOption,
|
|
54
|
+
forbidNonWhitelisted: this.forbidNonWhitelistedOption,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (errors.length > 0) {
|
|
58
|
+
const errorMessages = errors.flatMap((err: any) => {
|
|
59
|
+
return err.constraints ? Object.values(err.constraints) : [];
|
|
60
|
+
});
|
|
61
|
+
throw new HttpException({
|
|
62
|
+
statusCode: 400,
|
|
63
|
+
message: 'Validation failed',
|
|
64
|
+
errors: errorMessages,
|
|
65
|
+
}, 400);
|
|
66
|
+
}
|
|
23
67
|
|
|
24
|
-
|
|
68
|
+
return this.transformOption ? object : value;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Fallback to JIT if dynamic call fails
|
|
71
|
+
const validate = ValidationCompiler.compile(metatype);
|
|
72
|
+
const errors = validate(value);
|
|
73
|
+
if (errors) {
|
|
74
|
+
throw new HttpException({
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
message: 'Validation failed',
|
|
77
|
+
errors,
|
|
78
|
+
}, 400);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const validate = ValidationCompiler.compile(metatype);
|
|
84
|
+
const errors = validate(value);
|
|
85
|
+
if (errors) {
|
|
86
|
+
throw new HttpException({
|
|
87
|
+
statusCode: 400,
|
|
88
|
+
message: 'Validation failed',
|
|
89
|
+
errors,
|
|
90
|
+
}, 400);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
25
94
|
}
|
|
26
95
|
|
|
27
96
|
private toValidate(metatype: Function): boolean {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { SetMetadata } from '../core/decorators.ts';
|
|
3
|
+
|
|
4
|
+
export enum VersioningType {
|
|
5
|
+
URI = 'uri',
|
|
6
|
+
HEADER = 'header',
|
|
7
|
+
MEDIA_TYPE = 'media-type',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface VersioningOptions {
|
|
11
|
+
type: VersioningType;
|
|
12
|
+
defaultVersion?: string | string[];
|
|
13
|
+
header?: string; // For HEADER type, e.g., 'X-API-Version'
|
|
14
|
+
key?: string; // For MEDIA_TYPE type, e.g., 'v'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const VERSION_METADATA_KEY = 'calyx:version';
|
|
18
|
+
|
|
19
|
+
export function Version(version: string | string[]): MethodDecorator & ClassDecorator {
|
|
20
|
+
return (target: any, key?: string | symbol, descriptor?: any) => {
|
|
21
|
+
if (descriptor) {
|
|
22
|
+
Reflect.defineMetadata(VERSION_METADATA_KEY, version, descriptor.value);
|
|
23
|
+
return descriptor;
|
|
24
|
+
}
|
|
25
|
+
Reflect.defineMetadata(VERSION_METADATA_KEY, version, target);
|
|
26
|
+
return target;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class VersionExtractor {
|
|
31
|
+
static extract(req: Request, type: VersioningType, options: VersioningOptions): string | undefined {
|
|
32
|
+
if (type === VersioningType.HEADER) {
|
|
33
|
+
const headerName = options.header ?? 'x-api-version';
|
|
34
|
+
return req.headers.get(headerName.toLowerCase()) || undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (type === VersioningType.MEDIA_TYPE) {
|
|
38
|
+
const accept = req.headers.get('accept') || '';
|
|
39
|
+
const key = options.key ?? 'v';
|
|
40
|
+
const match = accept.match(new RegExp(`${key}=([a-zA-Z0-9_-]+)`));
|
|
41
|
+
return match ? match[1] : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|