Showing 67 of 692 files from the diff.
Other files ignored by Codecov
e2e_test.sh has changed.
benchmarks/run.sh has changed.
package-lock.json has changed.
tslint.json has changed.
unit_test.sh has changed.
docs/README.md has changed.
docs/SUMMARY.md has changed.
package.json has changed.
README.md has changed.
lerna.json has changed.

@@ -1,10 +1,11 @@
Loading
1 1
// 3p
2 2
import { existsSync, readFileSync } from 'fs';
3 +
import { join } from 'path';
3 4
4 5
// FoalTS
5 6
import { ConfigNotFoundError } from './config-not-found.error';
6 7
import { ConfigTypeError } from './config-type.error';
7 -
import { dotToUnderscore } from './utils';
8 +
import { Env } from './env';
8 9
9 10
type ValueStringType = 'string'|'number'|'boolean'|'boolean|string'|'number|string'|'any';
10 11
@@ -26,42 +27,6 @@
Loading
26 27
 */
27 28
export class Config {
28 29
29 -
  /**
30 -
   * Access environment variables and configuration files.
31 -
   *
32 -
   * For example, if it is called with the string `settings.session.secret`,
33 -
   * the method will go through these steps:
34 -
   *
35 -
   * 1. If the environment variable `SETTINGS_SESSION_SECRET` exists, then return its value.
36 -
   * 2. If `.env` exists and has a line `SETTINGS_SESSION_SECRET=`, then return its value.
37 -
   * 3. If `config/${NODE_ENV}.json` exists and the property `config['settings']['session']['secret']`
38 -
   * does too, then return its value.
39 -
   * 4. Same with `config/${NODE_ENV}.yml`.
40 -
   * 5. If `config/default.json` exists and the property `config['settings']['session']['secret']`
41 -
   * does too, then return its value.
42 -
   * 6. Same with `config/default.yml`.
43 -
   *
44 -
   * If none value is found, then the method returns the default value provided as second argument
45 -
   * to the function. If none was given, it returns undefined.
46 -
   *
47 -
   * @static
48 -
   * @template T - TypeScript type of the returned value. Default is `any`.
49 -
   * @param {string} key - Name of the config key using dots and camel case.
50 -
   * @param {T} [defaultValue] - Default value to return if no configuration is found with that key.
51 -
   * @returns {T} The configuration value.
52 -
   * @memberof Config
53 -
   */
54 -
  static get<T = any>(key: string, defaultValue?: T): T {
55 -
    let value = this.readConfigValue(key);
56 -
    if (typeof value === 'string') {
57 -
      value = this.convertType(value);
58 -
    }
59 -
    if (value !== undefined) {
60 -
      return value;
61 -
    }
62 -
    return defaultValue as T;
63 -
  }
64 -
65 30
  /**
66 31
   * Read the configuration value associated with the given key. Optionaly check its type.
67 32
   *
@@ -73,40 +38,29 @@
Loading
73 38
   * @returns {ValueType<T>|undefined} The configuration value
74 39
   * @memberof Config
75 40
   */
76 -
  static get2<T extends ValueStringType>(key: string, type: T, defaultValue: ValueType<T>): ValueType<T>;
77 -
  static get2<T extends ValueStringType>(key: string, type?: T): ValueType<T>|undefined;
78 -
  static get2<T extends ValueStringType>(key: string, type?: T, defaultValue?: ValueType<T>): ValueType<T>|undefined {
41 +
  static get<T extends ValueStringType>(key: string, type: T, defaultValue: ValueType<T>): ValueType<T>;
42 +
  static get<T extends ValueStringType>(key: string, type?: T): ValueType<T>|undefined;
43 +
  static get<T extends ValueStringType>(key: string, type?: T, defaultValue?: ValueType<T>): ValueType<T>|undefined {
79 44
    const value = this.readConfigValue(key);
80 45
81 46
    if (value === undefined) {
82 47
      return defaultValue;
83 48
    }
84 49
85 -
    if (type === 'boolean|string' && typeof value !== 'boolean') {
50 +
    if (type === 'string' && typeof value !== 'string') {
51 +
      throw new ConfigTypeError(key, 'string', typeof value);
52 +
    }
53 +
54 +
    if (type === 'boolean' && typeof value !== 'boolean') {
86 55
      if (value === 'true') {
87 56
        return true as any;
88 57
      }
89 58
      if (value === 'false') {
90 59
        return false as any;
91 60
      }
92 -
      if (typeof value !== 'string') {
93 -
        throw new ConfigTypeError(key, 'boolean|string', typeof value);
94 -
      }
95 -
    }
96 -
    if (type === 'number|string' && typeof value !== 'number') {
97 -
      if (typeof value !== 'string') {
98 -
        throw new ConfigTypeError(key, 'number|string', typeof value);
99 -
      }
100 -
      if (value.replace(/ /g, '') !== '') {
101 -
        const n = Number(value);
102 -
        if (!isNaN(n)) {
103 -
            return n as any;
104 -
          }
105 -
      }
106 -
    }
107 -
    if (type === 'string' && typeof value !== 'string') {
108 -
      throw new ConfigTypeError(key, 'string', typeof value);
61 +
      throw new ConfigTypeError(key, 'boolean', typeof value);
109 62
    }
63 +
110 64
    if (type === 'number' && typeof value !== 'number') {
111 65
      if (typeof value === 'string' && value.replace(/ /g, '') !== '') {
112 66
        const n = Number(value);
@@ -116,14 +70,29 @@
Loading
116 70
      }
117 71
      throw new ConfigTypeError(key, 'number', typeof value);
118 72
    }
119 -
    if (type === 'boolean' && typeof value !== 'boolean') {
73 +
74 +
    if (type === 'boolean|string' && typeof value !== 'boolean') {
120 75
      if (value === 'true') {
121 76
        return true as any;
122 77
      }
123 78
      if (value === 'false') {
124 79
        return false as any;
125 80
      }
126 -
      throw new ConfigTypeError(key, 'boolean', typeof value);
81 +
      if (typeof value !== 'string') {
82 +
        throw new ConfigTypeError(key, 'boolean|string', typeof value);
83 +
      }
84 +
    }
85 +
86 +
    if (type === 'number|string' && typeof value !== 'number') {
87 +
      if (typeof value !== 'string') {
88 +
        throw new ConfigTypeError(key, 'number|string', typeof value);
89 +
      }
90 +
      if (value.replace(/ /g, '') !== '') {
91 +
        const n = Number(value);
92 +
        if (!isNaN(n)) {
93 +
          return n as any;
94 +
        }
95 +
      }
127 96
    }
128 97
129 98
    return value;
@@ -143,7 +112,7 @@
Loading
143 112
   * @memberof Config
144 113
   */
145 114
  static getOrThrow<T extends ValueStringType>(key: string, type?: T, msg?: string): ValueType<T> {
146 -
    const value = this.get2(key, type);
115 +
    const value = this.get(key, type);
147 116
    if (value === undefined) {
148 117
      throw new ConfigNotFoundError(key, msg);
149 118
    }
@@ -157,112 +126,110 @@
Loading
157 126
   * @memberof Config
158 127
   */
159 128
  static clearCache() {
160 -
    this.cache = {
161 -
      dotEnv: undefined,
162 -
      json: {},
163 -
      yaml: {},
164 -
    };
129 +
    this.config = null;
130 +
    this.testConfig.clear();
165 131
  }
166 132
167 -
  private static yaml: any;
168 -
  private static cache: { dotEnv: any, json: any, yaml: any } = {
169 -
    dotEnv: undefined,
170 -
    json: {},
171 -
    yaml: {},
172 -
  };
133 +
  static set(key: string, value: string|number|boolean): void {
134 +
    this.testConfig.set(key, value);
135 +
  }
173 136
174 -
  private static readConfigValue(key: string): any {
175 -
    const underscoreName = dotToUnderscore(key);
137 +
  static remove(key: string): void {
138 +
    this.testConfig.delete(key);
139 +
  }
176 140
177 -
    const envValue = process.env[underscoreName];
178 -
    if (envValue !== undefined) {
179 -
      return envValue;
180 -
    }
141 +
  private static yaml: any;
142 +
  private static config: { [key: string ]: any } | null = null;
143 +
  private static testConfig: Map<string, string|number|boolean> = new Map();
181 144
182 -
    const dotEnvValue = this.readDotEnvValue(underscoreName);
183 -
    if (dotEnvValue !== undefined) {
184 -
      return dotEnvValue;
145 +
  private static readJSON(path: string): { [key: string ]: any } {
146 +
    if (!existsSync(path)) {
147 +
      return {};
185 148
    }
186 149
187 -
    const envJSONFilePath = `config/${process.env.NODE_ENV || 'development'}.json`;
188 -
    const envJSONValue = this.readJSONValue(envJSONFilePath, key);
189 -
    if (envJSONValue !== undefined) {
190 -
      return envJSONValue;
191 -
    }
150 +
    const fileContent = readFileSync(path, 'utf8');
151 +
    return JSON.parse(fileContent);
152 +
  }
192 153
193 -
    const envYamlFilePath = `config/${process.env.NODE_ENV || 'development'}.yml`;
194 -
    const envYAMLValue = this.readYAMLValue(envYamlFilePath, key);
195 -
    if (envYAMLValue !== undefined) {
196 -
      return envYAMLValue;
154 +
  private static readYAML(path: string): { [key: string ]: any } {
155 +
    if (!existsSync(path)) {
156 +
      return {};
197 157
    }
198 158
199 -
    const defaultJSONValue = this.readJSONValue('config/default.json', key);
200 -
    if (defaultJSONValue !== undefined) {
201 -
      return defaultJSONValue;
159 +
    const yaml = this.getYAMLInstance();
160 +
    if (!yaml) {
161 +
      console.log(`Impossible to read ${path}. The package "yamljs" is not installed.`);
162 +
      return {};
202 163
    }
203 164
204 -
    const defaultYAMLValue = this.readYAMLValue('config/default.yml', key);
205 -
    if (defaultYAMLValue !== undefined) {
206 -
      return defaultYAMLValue;
207 -
    }
165 +
    const fileContent = readFileSync(path, 'utf8');
166 +
    return yaml.parse(fileContent);
208 167
  }
209 168
210 -
  private static readDotEnvValue(name: string): string | undefined {
211 -
    if (!this.cache.dotEnv) {
212 -
      if (!existsSync('.env')) {
213 -
        return;
214 -
      }
169 +
  private static readJS(path: string): { [key: string ]: any } {
170 +
    if (!existsSync(path)) {
171 +
      return {};
172 +
    }
173 +
174 +
    return require(join(process.cwd(), path));
175 +
  }
215 176
216 -
      const envFileContent = readFileSync('.env', 'utf8');
217 -
      this.cache.dotEnv = {};
218 -
      envFileContent.replace(/\r\n/g, '\n').split('\n').forEach(line => {
219 -
        const [ key, ...values ] = line.split('=');
220 -
        const value = values.join('=');
221 -
        this.cache.dotEnv[key] = value;
222 -
      });
177 +
  private static readConfigValue(key: string): any {
178 +
    if (this.testConfig.has(key)) {
179 +
      return this.testConfig.get(key);
223 180
    }
224 181
225 -
    if (this.cache.dotEnv[name] !== undefined) {
226 -
      return this.cache.dotEnv[name];
182 +
    if (this.config === null) {
183 +
      this.config = [
184 +
        this.readJS('config/default.js'),
185 +
        this.readYAML('config/default.yml'),
186 +
        this.readJSON('config/default.json'),
187 +
        this.readJS(`config/${process.env.NODE_ENV || 'development'}.js`),
188 +
        this.readYAML(`config/${process.env.NODE_ENV || 'development'}.yml`),
189 +
        this.readJSON(`config/${process.env.NODE_ENV || 'development'}.json`),
190 +
      ].reduce((config1, config2) => this.mergeDeep(config1, config2));
227 191
    }
228 -
  }
229 192
230 -
  private static readJSONValue(path: string, key: string): any {
231 -
    if (!this.cache.json[path]) {
232 -
      if (!existsSync(path)) {
233 -
        return;
193 +
    const properties = key.split('.');
194 +
    let result: any = this.config;
195 +
    for (const property of properties) {
196 +
      result = result[property];
197 +
      if (result === undefined) {
198 +
        break;
234 199
      }
200 +
    }
235 201
236 -
      const fileContent = readFileSync(path, 'utf8');
237 -
      this.cache.json[path] = JSON.parse(fileContent);
202 +
    if (typeof result === 'string' && result.startsWith('env(') && result.endsWith(')')) {
203 +
      const envVarName = result.substr(4, result.length - 5);
204 +
      return Env.get(envVarName);
238 205
    }
239 206
240 -
    return this.getValue(this.cache.json[path], key);
207 +
    return result;
241 208
  }
242 209
243 -
  private static readYAMLValue(path: string, key: string): any {
244 -
    if (!this.cache.yaml[path]) {
245 -
      if (!existsSync(path)) {
246 -
        return;
247 -
      }
210 +
  private static mergeDeep(target: { [key: string]: any }, source: { [key: string]: any } ): { [key: string]: any } {
211 +
    // TODO: improve the tests of this function.
212 +
    function isObject(o: any): o is { [key: string]: any } {
213 +
      return typeof o === 'object' && o !== null;
214 +
    }
248 215
249 -
      const yaml = this.getYAMLInstance();
250 -
      if (!yaml) {
251 -
        console.log(`Impossible to read ${path}. The package "yamljs" is not installed.`);
252 -
        return;
216 +
    for (const key in source) {
217 +
      if (isObject(target[key]) && isObject(source[key])) {
218 +
        this.mergeDeep(target[key], source[key]);
219 +
      } else {
220 +
        target[key] = source[key];
253 221
      }
254 -
255 -
      const fileContent = readFileSync(path, 'utf8');
256 -
      this.cache.yaml[path] = yaml.parse(fileContent);
257 222
    }
258 223
259 -
    return this.getValue(this.cache.yaml[path], key);
224 +
    return target;
260 225
  }
261 226
262 227
  private static getYAMLInstance(): false | any {
228 +
    // TODO: test this method (hard).
263 229
    if (this.yaml === false) {
264 230
      return false;
265 231
    }
232 +
266 233
    try {
267 234
      this.yaml = require('yamljs');
268 235
    } catch (err) {
@@ -274,61 +241,4 @@
Loading
274 241
    return this.yaml;
275 242
  }
276 243
277 -
  private static convertType(value: string): boolean | number | string {
278 -
    if (value === 'true') {
279 -
      return true;
280 -
    }
281 -
    if (value === 'false') {
282 -
      return false;
283 -
    }
284 -
    if (value.replace(/ /g, '') === '') {
285 -
      return value;
286 -
    }
287 -
    const n = Number(value);
288 -
    if (!isNaN(n)) {
289 -
      return n;
290 -
    }
291 -
    return value;
292 -
  }
293 -
294 -
  private static getValue(config: any, propertyPath: string): any {
295 -
    const properties = propertyPath.split('.');
296 -
    let result = config;
297 -
    for (const property of properties) {
298 -
      result = result[property];
299 -
      if (result === undefined) {
300 -
        break;
301 -
      }
302 -
    }
303 -
    return result;
304 -
  }
305 -
306 -
  /**
307 -
   * Access environment variables and configuration files.
308 -
   *
309 -
   * For example, if it is called with the string `settings.session.secret`,
310 -
   * the method will go through these steps:
311 -
   *
312 -
   * 1. If the environment variable `SETTINGS_SESSION_SECRET` exists, then return its value.
313 -
   * 2. If `.env` exists and has a line `SETTINGS_SESSION_SECRET=`, then return its value.
314 -
   * 3. If `config/${NODE_ENV}.json` exists and the property `config['settings']['session']['secret']`
315 -
   * does too, then return its value.
316 -
   * 4. Same with `config/${NODE_ENV}.yml`.
317 -
   * 5. If `config/default.json` exists and the property `config['settings']['session']['secret']`
318 -
   * does too, then return its value.
319 -
   * 6. Same with `config/default.yml`.
320 -
   *
321 -
   * If none value is found, then the method returns the default value provided as second argument
322 -
   * to the function. If none was given, it returns undefined.
323 -
   *
324 -
   * @template T - TypeScript type of the returned value. Default is `any`.
325 -
   * @param {string} key - Name of the config key using dots and camel case.
326 -
   * @param {T} [defaultValue] - Default value to return if no configuration is found with that key.
327 -
   * @returns {T} The configuration value.
328 -
   * @memberof Config
329 -
   */
330 -
  get<T = any>(key: string, defaultValue?: T): T {
331 -
    return Config.get<T>(key, defaultValue);
332 -
  }
333 -
334 244
}

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiResponses } from '../interfaces';
4 4
import { Dynamic } from '../utils';
5 5
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-security.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-security.spec.ts
9 9
imilarity index 76%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-security.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-security.ts

@@ -0,0 +1,20 @@
Loading
1 +
import { ApiResponse, Context, Hook, HookDecorator, HttpResponseRedirect, HttpResponseUnauthorized } from '../../core';
2 +
3 +
export function UserRequired(options: { redirectTo?: string, openapi?: boolean } = {}): HookDecorator {
4 +
  function hook(ctx: Context) {
5 +
    if (ctx.user === undefined || ctx.user === null) {
6 +
      if (options.redirectTo) {
7 +
        return new HttpResponseRedirect(options.redirectTo);
8 +
      }
9 +
      return new HttpResponseUnauthorized();
10 +
    }
11 +
  }
12 +
13 +
  const openapi = [
14 +
    options.redirectTo ?
15 +
      ApiResponse(302, { description: 'Unauthenticated request.' }) :
16 +
      ApiResponse(401, { description: 'Unauthenticated request.' })
17 +
  ];
18 +
19 +
  return Hook(hook, openapi, { openapi: options.openapi });
20 +
}

@@ -1,4 +1,3 @@
Loading
1 -
export * from './errors';
2 1
export * from './hooks';
3 2
export * from './tokens';
4 3
export * from './utils';

@@ -1,10 +1,7 @@
Loading
1 1
export { Log, LogOptions } from './log.hook';
2 +
export { UserRequired } from './user-required.hook';
2 3
export { ValidateBody } from './validate-body.hook';
3 4
export { ValidateCookie } from './validate-cookie.hook';
4 -
export { ValidateCookies } from './validate-cookies.hook';
5 5
export { ValidateHeader } from './validate-header.hook';
6 -
export { ValidateHeaders } from './validate-headers.hook';
7 6
export { ValidatePathParam } from './validate-path-param.hook';
8 -
export { ValidateParams } from './validate-params.hook';
9 7
export { ValidateQueryParam } from './validate-query-param.hook';
10 -
export { ValidateQuery } from './validate-query.hook';

@@ -1,6 +1,16 @@
Loading
1 1
// FoalTS
2 -
import { Config, Context, Hook, HookDecorator, HttpResponseBadRequest } from '../../core';
3 -
import { ApiParameter, ApiResponse, IApiHeaderParameter, IApiSchema } from '../../openapi';
2 +
import { ValidateFunction } from 'ajv';
3 +
import {
4 +
  ApiParameter,
5 +
  ApiResponse,
6 +
  Context,
7 +
  Hook,
8 +
  HookDecorator,
9 +
  HttpResponseBadRequest,
10 +
  IApiHeaderParameter,
11 +
  OpenApi,
12 +
  ServiceManager
13 +
} from '../../core';
4 14
import { getAjvInstance } from '../utils';
5 15
import { isFunction } from './is-function.util';
6 16
@@ -21,43 +31,43 @@
Loading
21 31
  schema: object | ((controller: any) => object) = { type: 'string' },
22 32
  options: { openapi?: boolean, required?: boolean } = {}
23 33
): HookDecorator {
24 -
  const ajv = getAjvInstance();
25 -
  const required = options.required !== false;
34 +
  // tslint:disable-next-line
35 +
  const required = options.required ?? true;
26 36
  name = name.toLowerCase();
27 37
28 -
  function validate(this: any, ctx: Context) {
29 -
    const headersSchema = {
30 -
      properties: {
31 -
        [name]: isFunction(schema) ? schema(this) : schema
32 -
      },
33 -
      required: required ? [ name ] : [],
34 -
      type: 'object',
35 -
    };
36 -
    if (!ajv.validate(headersSchema, ctx.request.headers)) {
37 -
      return new HttpResponseBadRequest({ headers: ajv.errors });
38 -
    }
39 -
  }
38 +
  let validateSchema: ValidateFunction|undefined;
40 39
41 -
  return (target: any, propertyKey?: string) =>  {
42 -
    Hook(validate)(target, propertyKey);
40 +
  function validate(this: any, ctx: Context, services: ServiceManager) {
41 +
    if (!validateSchema) {
42 +
      const ajvSchema = isFunction(schema) ? schema(this) : schema;
43 +
      const components = services.get(OpenApi).getComponents(this);
43 44
44 -
    if (options.openapi === false ||
45 -
      (options.openapi === undefined && !Config.get2('settings.openapi.useHooks', 'boolean'))
46 -
    ) {
47 -
      return;
45 +
      validateSchema = getAjvInstance().compile({
46 +
        components,
47 +
        properties: {
48 +
          [name]: ajvSchema
49 +
        },
50 +
        required: required ? [ name ] : [],
51 +
        type: 'object',
52 +
      });
48 53
    }
49 -
50 -
    function makeParameter(schema: IApiSchema): IApiHeaderParameter {
51 -
      const result: IApiHeaderParameter = { in: 'header', name, schema };
52 -
      if (required) {
53 -
        result.required = required;
54 -
      }
55 -
      return result;
54 +
    if (!validateSchema(ctx.request.headers)) {
55 +
      return new HttpResponseBadRequest({ headers: validateSchema.errors });
56 56
    }
57 +
  }
58 +
59 +
  const param: IApiHeaderParameter = { in: 'header', name };
60 +
  if (required) {
61 +
    param.required = required;
62 +
  }
57 63
58 -
    const apiHeaderParameter = isFunction(schema) ? (c: any) => makeParameter(schema(c)) : makeParameter(schema);
64 +
  const openapi = [
65 +
    ApiParameter((c: any) => ({
66 +
      ...param,
67 +
      schema: isFunction(schema) ? schema(c) : schema
68 +
    })),
69 +
    ApiResponse(400, { description: 'Bad request.' })
70 +
  ];
59 71
60 -
    ApiParameter(apiHeaderParameter)(target, propertyKey);
61 -
    ApiResponse(400, { description: 'Bad request.' })(target, propertyKey);
62 -
  };
72 +
  return Hook(validate, openapi, options);
63 73
}

@@ -1,6 +1,17 @@
Loading
1 +
// 3p
2 +
import { ValidateFunction } from 'ajv';
3 +
1 4
// FoalTS
2 -
import { Config, Context, Hook, HookDecorator, HttpResponseBadRequest } from '../../core';
3 -
import { ApiRequestBody, ApiResponse, IApiRequestBody, IApiSchema } from '../../openapi';
5 +
import {
6 +
  ApiRequestBody,
7 +
  ApiResponse,
8 +
  Context,
9 +
  Hook,
10 +
  HookDecorator,
11 +
  HttpResponseBadRequest,
12 +
  OpenApi,
13 +
  ServiceManager
14 +
} from '../../core';
4 15
import { getAjvInstance } from '../utils';
5 16
import { isFunction } from './is-function.util';
6 17
@@ -9,47 +20,41 @@
Loading
9 20
 *
10 21
 * @export
11 22
 * @param {(object | ((controller: any) => object))} schema - Schema used to validate the body request.
12 -
 * @param {{ openapi?: boolean }} [options={}] - Options to add openapi metadata
23 +
 * @param {{ openapi?: boolean }} [options] - Options to add openapi metadata
13 24
 * @returns {HookDecorator} - The hook.
14 25
 */
15 26
export function ValidateBody(
16 -
  schema: object | ((controller: any) => object), options: { openapi?: boolean } = {}
27 +
  schema: object | ((controller: any) => object), options?: { openapi?: boolean }
17 28
): HookDecorator {
18 -
  const ajv = getAjvInstance();
19 -
20 -
  function validate(this: any, ctx: Context) {
21 -
    const ajvSchema = isFunction(schema) ? schema(this) : schema;
22 -
    if (!ajv.validate(ajvSchema, ctx.request.body)) {
23 -
      return new HttpResponseBadRequest({ body: ajv.errors });
24 -
    }
25 -
  }
29 +
  let validateSchema: ValidateFunction|undefined;
26 30
27 -
  return (target: any, propertyKey?: string) =>  {
28 -
    Hook(validate)(target, propertyKey);
31 +
  function validate(this: any, ctx: Context, services: ServiceManager) {
32 +
    if (!validateSchema) {
33 +
      const ajvSchema = isFunction(schema) ? schema(this) : schema;
34 +
      const components = services.get(OpenApi).getComponents(this);
29 35
30 -
    if (options.openapi === false ||
31 -
      (options.openapi === undefined && !Config.get2('settings.openapi.useHooks', 'boolean'))
32 -
    ) {
33 -
      return;
36 +
      validateSchema = getAjvInstance().compile({
37 +
        ...ajvSchema,
38 +
        components
39 +
      });
34 40
    }
35 41
36 -
    function makeRequestBody(schema: IApiSchema): IApiRequestBody {
37 -
      return {
38 -
        content: {
39 -
          'application/json': { schema }
40 -
        },
41 -
        required: true
42 -
      };
43 -
    }
44 -
45 -
    const requestBody = isFunction(schema) ? (c: any) => makeRequestBody(schema(c)) : makeRequestBody(schema);
46 -
47 -
    if (propertyKey) {
48 -
      ApiRequestBody(requestBody)(target, propertyKey);
49 -
    } else {
50 -
      ApiRequestBody(requestBody)(target);
42 +
    if (!validateSchema(ctx.request.body)) {
43 +
      return new HttpResponseBadRequest({ body: validateSchema.errors });
51 44
    }
45 +
  }
52 46
53 -
    ApiResponse(400, { description: 'Bad request.' })(target, propertyKey);
54 -
  };
47 +
  const openapi = [
48 +
    ApiRequestBody((c: any) => ({
49 +
      content: {
50 +
        'application/json': {
51 +
          schema: isFunction(schema) ? schema(c) : schema
52 +
        }
53 +
      },
54 +
      required: true
55 +
    })),
56 +
    ApiResponse(400, { description: 'Bad request.' })
57 +
  ];
58 +
59 +
  return Hook(validate, openapi, options);
55 60
}

@@ -1,19 +1,96 @@
Loading
1 -
import { dotToUnderscore } from './utils';
1 +
import { makeBox } from './utils';
2 2
3 3
export class ConfigNotFoundError extends Error {
4 4
  readonly name = 'ConfigNotFoundError';
5 5
6 6
  constructor(readonly key: string, readonly msg?: string) {
7 -
    super(
8 -
      `No value found for the configuration key "${key}".\n\n`
9 -
      + (msg ? `${msg}\n\n` : '')
10 -
      + 'To pass a value, you can use:\n'
11 -
      + `- the environment variable ${dotToUnderscore(key)},\n`
12 -
      + `- the ".env" file with the variable ${dotToUnderscore(key)},\n`
13 -
      + `- the JSON file "config/${process.env.NODE_ENV || 'development'}.json" with the path "${key}",\n`
14 -
      + `- the YAML file "config/${process.env.NODE_ENV || 'development'}.yml" with the path "${key}",\n`
15 -
      + `- the JSON file "config/default.json" with the path "${key}", or\n`
16 -
      + `- the YAML file "config/default.yml" with the path "${key}".`
17 -
    );
7 +
    super();
8 +
    const keywords = key.split('.');
9 +
10 +
    function generateContent(type: 'JSON'|'YAML'|'JS'): string[] {
11 +
      const lines: string[] = [];
12 +
13 +
      if (type === 'JS') {
14 +
        lines.push('  const { Env } = require(\'@foal/core\');');
15 +
        lines.push('');
16 +
      }
17 +
18 +
      if (type !== 'YAML') {
19 +
        lines.push('  {');
20 +
      }
21 +
22 +
      keywords.forEach((keyword, index) => {
23 +
        if (type === 'JSON') {
24 +
          keyword = `"${keyword}"`;
25 +
        }
26 +
27 +
        const spaces = '  '.repeat(index + (type === 'YAML' ? 1 : 2));
28 +
29 +
        if (index === keywords.length - 1) {
30 +
          lines.push(spaces + keyword + ': <your_value>');
31 +
          if (type === 'YAML') {
32 +
            lines.push(spaces + '# OR with an environment variable: ');
33 +
          } else {
34 +
            lines.push(spaces + '// OR with an environment variable: ');
35 +
          }
36 +
37 +
          let envValue = '';
38 +
          switch (type) {
39 +
            case 'JS':
40 +
              envValue = 'Env.get(\'<YOUR_ENVIRONMENT_VARIABLE>\')';
41 +
              break;
42 +
            case 'JSON':
43 +
              envValue = '"env(<YOUR_ENVIRONMENT_VARIABLE>)"';
44 +
              break;
45 +
            case 'YAML':
46 +
              envValue = 'env(<YOUR_ENVIRONMENT_VARIABLE>)';
47 +
              break;
48 +
          }
49 +
          lines.push(spaces + keyword + ': ' + envValue);
50 +
        } else {
51 +
          if (type === 'YAML') {
52 +
            lines.push(spaces + keyword + ':');
53 +
          } else {
54 +
            lines.push(spaces + keyword + ': {');
55 +
          }
56 +
        }
57 +
      });
58 +
59 +
      if (type !== 'YAML') {
60 +
        keywords.forEach((_, index) => {
61 +
          if (index === keywords.length - 1) {
62 +
            return;
63 +
          }
64 +
          const spaces = '  '.repeat(keywords.length - index);
65 +
          lines.push(spaces + '}');
66 +
        });
67 +
68 +
        lines.push('  }');
69 +
      }
70 +
71 +
      return lines;
72 +
    }
73 +
74 +
    this.message = '\n\n'
75 +
    + makeBox(
76 +
      'JSON file (config/default.json, config/test.json, ...)',
77 +
      generateContent('JSON'),
78 +
    )
79 +
    + '\n'
80 +
    + makeBox(
81 +
      'YAML file (config/default.yml, config/test.yml, ...)',
82 +
      generateContent('YAML'),
83 +
    )
84 +
    + '\n'
85 +
    + makeBox(
86 +
      'JS file (config/default.js, config/test.js, ...)',
87 +
      generateContent('JS'),
88 +
    )
89 +
    + '\n'
90 +
    + `No value found for the configuration key "${key}".\n`
91 +
    + (msg === undefined ? '' : `\n${msg}\n`)
92 +
    + '\n'
93 +
    + `To pass a value, use one of the examples above.\n`;
18 94
  }
95 +
19 96
}

@@ -1,26 +1,50 @@
Loading
1 1
// FoalTS
2 2
import { Config, CookieOptions, HttpResponse } from '../core';
3 3
import {
4 -
  SESSION_DEFAULT_COOKIE_HTTP_ONLY, SESSION_DEFAULT_COOKIE_NAME, SESSION_DEFAULT_COOKIE_PATH
4 +
  SESSION_DEFAULT_COOKIE_HTTP_ONLY,
5 +
  SESSION_DEFAULT_COOKIE_NAME,
6 +
  SESSION_DEFAULT_COOKIE_PATH,
7 +
  SESSION_DEFAULT_CSRF_COOKIE_NAME,
8 +
  SESSION_DEFAULT_SAME_SITE_ON_CSRF_ENABLED,
5 9
} from './constants';
6 -
import { SessionStore } from './session-store';
10 +
import { Session } from './session';
7 11
8 12
/**
9 -
 * Send the session token in a cookie.
13 +
 * Sends the session token in a cookie.
14 +
 *
15 +
 * If the CSRF protection is enabled, it also sends the CSRF token in a CSRF cookie.
10 16
 *
11 17
 * @export
12 -
 * @param {HttpResponse} response - The HTTP response
13 -
 * @param {string} token - The session token
18 +
 * @param {HttpResponse} response - The HTTP response.
19 +
 * @param {Session} session - The session object.
14 20
 */
15 -
export function setSessionCookie(response: HttpResponse, token: string): void {
16 -
  const cookieName = Config.get2('settings.session.cookie.name', 'string', SESSION_DEFAULT_COOKIE_NAME);
21 +
export function setSessionCookie(response: HttpResponse, session: Session): void {
22 +
  const cookieName = Config.get('settings.session.cookie.name', 'string', SESSION_DEFAULT_COOKIE_NAME);
23 +
24 +
  const csrfEnabled = Config.get('settings.session.csrf.enabled', 'boolean', false);
25 +
  let sameSite = Config.get('settings.session.cookie.sameSite', 'string') as 'strict'|'lax'|'none'|undefined;
26 +
  if (csrfEnabled && sameSite === undefined) {
27 +
    sameSite = SESSION_DEFAULT_SAME_SITE_ON_CSRF_ENABLED;
28 +
  }
29 +
17 30
  const options: CookieOptions = {
18 -
    domain: Config.get2('settings.session.cookie.domain', 'string'),
19 -
    httpOnly: Config.get2('settings.session.cookie.httpOnly', 'boolean', SESSION_DEFAULT_COOKIE_HTTP_ONLY),
20 -
    maxAge: SessionStore.getExpirationTimeouts().inactivity,
21 -
    path: Config.get2('settings.session.cookie.path', 'string', SESSION_DEFAULT_COOKIE_PATH),
22 -
    sameSite: Config.get2('settings.session.cookie.sameSite', 'string') as 'strict'|'lax'|'none'|undefined,
23 -
    secure: Config.get2('settings.session.cookie.secure', 'boolean')
31 +
    domain: Config.get('settings.session.cookie.domain', 'string'),
32 +
    expires: new Date(session.expirationTime * 1000),
33 +
    path: Config.get('settings.session.cookie.path', 'string', SESSION_DEFAULT_COOKIE_PATH),
34 +
    sameSite,
35 +
    secure: Config.get('settings.session.cookie.secure', 'boolean')
24 36
  };
25 -
  response.setCookie(cookieName, token, options);
37 +
38 +
  response.setCookie(cookieName, session.getToken(), {
39 +
    ...options,
40 +
    httpOnly: Config.get('settings.session.cookie.httpOnly', 'boolean', SESSION_DEFAULT_COOKIE_HTTP_ONLY),
41 +
  });
42 +
43 +
  if (csrfEnabled) {
44 +
    const csrfCookieName = Config.get('settings.session.csrf.cookie.name', 'string', SESSION_DEFAULT_CSRF_COOKIE_NAME);
45 +
    response.setCookie(csrfCookieName, session.get<string|undefined>('csrfToken') || '', {
46 +
      ...options,
47 +
      httpOnly: false,
48 +
    });
49 +
  }
26 50
}

@@ -7,30 +7,29 @@
Loading
7 7
import { Config, Context, HttpResponseInternalServerError } from '../../core';
8 8
import { renderToString } from './render.util';
9 9
10 -
const page500 = '<html><head><title>INTERNAL SERVER ERROR</title></head><body>'
11 -
                + '<h1>500 - INTERNAL SERVER ERROR</h1></body></html>';
12 -
13 10
/**
14 -
 * Render the default HTML page when an error is thrown or rejected in the application.
11 +
 * Renders the default HTML page when an error is thrown or rejected in the application.
15 12
 *
16 13
 * The page is different depending on if the configuration key `settings.debug` is
17 14
 * true or false.
18 15
 *
19 16
 * @export
20 17
 * @param {Error} error - The error thrown or rejected.
21 -
 * @param {Context} ctx - The Context. object.
18 +
 * @param {Context} ctx - The Context object.
22 19
 * @returns {Promise<HttpResponseInternalServerError>} The HTTP response.
23 20
 */
24 21
export async function renderError(error: Error, ctx: Context): Promise<HttpResponseInternalServerError> {
25 -
  const template = await promisify(readFile)(join(__dirname, '500.debug.html'), 'utf8');
22 +
  let body = '<html><head><title>INTERNAL SERVER ERROR</title></head><body>'
23 +
  + '<h1>500 - INTERNAL SERVER ERROR</h1></body></html>';
26 24
27 -
  if (!Config.get2('settings.debug', 'boolean')) {
28 -
    return new HttpResponseInternalServerError(page500);
25 +
  if (Config.get('settings.debug', 'boolean')) {
26 +
    const template = await promisify(readFile)(join(__dirname, '500.debug.html'), 'utf8');
27 +
    body = renderToString(template, {
28 +
      message: error.message,
29 +
      name: error.name,
30 +
      stack: error.stack
31 +
    });
29 32
  }
30 33
31 -
  return new HttpResponseInternalServerError(renderToString(template, {
32 -
    message: error.message,
33 -
    name: error.name,
34 -
    stack: error.stack
35 -
  }));
34 +
  return new HttpResponseInternalServerError(body, { ctx, error });
36 35
}

@@ -0,0 +1,23 @@
Loading
1 +
import { generateToken } from '../common';
2 +
import { Session } from './session';
3 +
import { SessionState } from './session-state.interface';
4 +
import { SessionStore } from './session-store';
5 +
6 +
export async function createSession(store: SessionStore): Promise<Session> {
7 +
  const date = Math.floor(Date.now() / 1000);
8 +
9 +
  const state: SessionState = {
10 +
    content: {
11 +
      csrfToken: await generateToken(),
12 +
    },
13 +
    createdAt: date,
14 +
    // The below line cannot be tested because of the encapsulation of Session.
15 +
    flash: {},
16 +
    id: await generateToken(),
17 +
    // Any value here is fine. updatedAt is set by Session.commit().
18 +
    updatedAt: date,
19 +
    userId: null,
20 +
  };
21 +
22 +
  return new Session(store, state, { exists: false });
23 +
}

@@ -10,8 +10,7 @@
Loading
10 10
11 11
export interface IDependency {
12 12
  propertyKey: string;
13 -
  // Service class or service ID.
14 -
  serviceClass: string|Class;
13 +
  serviceClassOrID: string|Class;
15 14
}
16 15
17 16
/**
@@ -22,7 +21,7 @@
Loading
22 21
export function Dependency(id: string) {
23 22
  return (target: any, propertyKey: string) => {
24 23
    const dependencies: IDependency[] = [ ...(Reflect.getMetadata('dependencies', target) || []) ];
25 -
    dependencies.push({ propertyKey, serviceClass: id });
24 +
    dependencies.push({ propertyKey, serviceClassOrID: id });
26 25
    Reflect.defineMetadata('dependencies', dependencies, target);
27 26
  };
28 27
}
@@ -35,7 +34,7 @@
Loading
35 34
export function dependency(target: any, propertyKey: string) {
36 35
  const serviceClass = Reflect.getMetadata('design:type', target, propertyKey);
37 36
  const dependencies: IDependency[] = [ ...(Reflect.getMetadata('dependencies', target) || []) ];
38 -
  dependencies.push({ propertyKey, serviceClass });
37 +
  dependencies.push({ propertyKey, serviceClassOrID: serviceClass });
39 38
  Reflect.defineMetadata('dependencies', dependencies, target);
40 39
}
41 40
@@ -45,30 +44,27 @@
Loading
45 44
 * @export
46 45
 * @template Service
47 46
 * @param {ClassOrAbstractClass<Service>} serviceClass - The service class.
48 -
 * @param {(object|ServiceManager)} [dependencies] - Either a ServiceManager or an
49 -
 * object which key/values are the service properties/instances.
47 +
 * @param {object} [dependencies] - An object which key/values are the service properties/instances.
50 48
 * @returns {Service} - The created service.
51 49
 */
52 50
export function createService<Service>(
53 -
  serviceClass: ClassOrAbstractClass<Service>, dependencies?: object|ServiceManager
51 +
  serviceClass: ClassOrAbstractClass<Service>, dependencies?: object
54 52
): Service {
55 53
  return createControllerOrService(serviceClass, dependencies);
56 54
}
57 55
58 56
export function createControllerOrService<T>(
59 -
  serviceClass: ClassOrAbstractClass<T>, dependencies?: object|ServiceManager
57 +
  serviceClass: ClassOrAbstractClass<T>, dependencies?: object
60 58
): T {
61 59
  const metadata: IDependency[] = Reflect.getMetadata('dependencies', serviceClass.prototype) || [];
62 60
63 -
  let serviceManager = new ServiceManager();
61 +
  const serviceManager = new ServiceManager();
64 62
65 -
  if (dependencies instanceof ServiceManager) {
66 -
    serviceManager = dependencies;
67 -
  } else if (typeof dependencies === 'object') {
63 +
  if (dependencies) {
68 64
    metadata.forEach(dep => {
69 65
      const serviceMock = (dependencies as any)[dep.propertyKey];
70 66
      if (serviceMock) {
71 -
        serviceManager.set(dep.serviceClass, serviceMock);
67 +
        serviceManager.set(dep.serviceClassOrID, serviceMock);
72 68
      }
73 69
    });
74 70
  }
@@ -170,7 +166,7 @@
Loading
170 166
    const service = new (identifier as Class)();
171 167
172 168
    for (const dependency of dependencies) {
173 -
      (service as any)[dependency.propertyKey] = this.get(dependency.serviceClass as any);
169 +
      (service as any)[dependency.propertyKey] = this.get(dependency.serviceClassOrID as any);
174 170
    }
175 171
176 172
    // Save the service.
@@ -204,7 +200,7 @@
Loading
204 200
205 201
    let concreteClassPath: string;
206 202
    if (cls.hasOwnProperty('defaultConcreteClassPath')) {
207 -
      concreteClassPath = Config.get2(concreteClassConfigPath, 'string', 'local');
203 +
      concreteClassPath = Config.get(concreteClassConfigPath, 'string', 'local');
208 204
    } else {
209 205
      concreteClassPath = Config.getOrThrow(concreteClassConfigPath, 'string');
210 206
    }

@@ -6,6 +6,11 @@
Loading
6 6
export const SESSION_DEFAULT_COOKIE_HTTP_ONLY: boolean = true;
7 7
export const SESSION_DEFAULT_COOKIE_NAME: string = 'sessionID';
8 8
9 +
export const SESSION_DEFAULT_CSRF_COOKIE_NAME: string = 'XSRF-TOKEN';
10 +
export const SESSION_DEFAULT_SAME_SITE_ON_CSRF_ENABLED: 'strict'|'lax'|'none' = 'lax';
11 +
9 12
// Expiration timeouts in seconds
10 13
export const SESSION_DEFAULT_INACTIVITY_TIMEOUT = 15 * 60; // 15 minutes
11 14
export const SESSION_DEFAULT_ABSOLUTE_TIMEOUT = 7 * 24 * 60 * 60; // 1 week
15 +
16 +
export const SESSION_DEFAULT_GARBAGE_COLLECTOR_PERIODICITY = 50;

@@ -0,0 +1,11 @@
Loading
1 +
import { Class } from '../class.interface';
2 +
import { makeControllerRoutes } from '../routes';
3 +
import { ServiceManager } from '../service-manager';
4 +
import { IOpenAPI } from './interfaces';
5 +
import { OpenApi } from './openapi.service';
6 +
7 +
export function createOpenApiDocument(controllerClass: Class): IOpenAPI {
8 +
  const services = new ServiceManager();
9 +
  Array.from(makeControllerRoutes(controllerClass, services));
10 +
  return services.get(OpenApi).getDocument(controllerClass);
11 +
}
0 12
imilarity index 99%
1 13
ename from packages/core/src/openapi/decorators.spec.ts
2 14
ename to packages/core/src/core/openapi/decorators.spec.ts

@@ -0,0 +1,206 @@
Loading
1 +
import {
2 +
  ApiDefineSecurityScheme,
3 +
  ApiResponse,
4 +
  ApiSecurityRequirement,
5 +
  Class,
6 +
  ClassOrAbstractClass,
7 +
  Config,
8 +
  Context,
9 +
  Hook,
10 +
  HookDecorator,
11 +
  HttpResponse,
12 +
  HttpResponseBadRequest,
13 +
  HttpResponseForbidden,
14 +
  HttpResponseRedirect,
15 +
  HttpResponseUnauthorized,
16 +
  IApiSecurityScheme,
17 +
  isHttpResponseInternalServerError,
18 +
  ServiceManager
19 +
} from '../core';
20 +
import { SESSION_DEFAULT_COOKIE_NAME } from './constants';
21 +
import { createSession } from './create-session';
22 +
import { readSession } from './read-session';
23 +
import { removeSessionCookie } from './remove-session-cookie';
24 +
import { SessionStore } from './session-store';
25 +
import { setSessionCookie } from './set-session-cookie';
26 +
27 +
export interface UseSessionOptions {
28 +
  user?: (id: string|number) => Promise<any|undefined>;
29 +
  store?: Class<SessionStore>;
30 +
  cookie?: boolean;
31 +
  redirectTo?: string;
32 +
  openapi?: boolean;
33 +
  required?: boolean;
34 +
  create?: boolean;
35 +
}
36 +
37 +
export function UseSessions(options: UseSessionOptions = {}): HookDecorator {
38 +
39 +
  function badRequestOrRedirect(description: string): HttpResponse {
40 +
    if (options.redirectTo) {
41 +
      return new HttpResponseRedirect(options.redirectTo);
42 +
    }
43 +
    return new HttpResponseBadRequest({ code: 'invalid_request', description });
44 +
  }
45 +
46 +
  function unauthorizedOrRedirect(description: string): HttpResponse {
47 +
    if (options.redirectTo) {
48 +
      return new HttpResponseRedirect(options.redirectTo);
49 +
    }
50 +
    return new HttpResponseUnauthorized({ code: 'invalid_token', description })
51 +
      .setHeader(
52 +
        'WWW-Authenticate',
53 +
        `error="invalid_token", error_description="${description}"`
54 +
      );
55 +
  }
56 +
57 +
  async function hook(ctx: Context, services: ServiceManager) {
58 +
    const ConcreteSessionStore: ClassOrAbstractClass<SessionStore> = options.store || SessionStore;
59 +
    const store = services.get(ConcreteSessionStore);
60 +
61 +
    async function postFunction(response: HttpResponse) {
62 +
      if (!(ctx.session) || isHttpResponseInternalServerError(response)) {
63 +
        return;
64 +
      }
65 +
66 +
      if (ctx.session.isDestroyed) {
67 +
        if (options.cookie) {
68 +
          removeSessionCookie(response);
69 +
        }
70 +
        return;
71 +
      }
72 +
73 +
      await ctx.session.commit();
74 +
75 +
      if (options.cookie) {
76 +
        setSessionCookie(response, ctx.session);
77 +
      }
78 +
    }
79 +
80 +
    /* Validate the request */
81 +
82 +
    let sessionID: string;
83 +
84 +
    if (options.cookie) {
85 +
      const cookieName = Config.get('settings.session.cookie.name', 'string', SESSION_DEFAULT_COOKIE_NAME);
86 +
      const content = ctx.request.cookies[cookieName] as string|undefined;
87 +
88 +
      if (!content) {
89 +
        if (!options.required) {
90 +
          if (options.create ?? true) {
91 +
            ctx.session = await createSession(store);
92 +
          }
93 +
          return postFunction;
94 +
        }
95 +
        return badRequestOrRedirect('Session cookie not found.');
96 +
      }
97 +
98 +
      sessionID = content;
99 +
    } else {
100 +
      const authorizationHeader = ctx.request.get('Authorization') || '';
101 +
102 +
      if (!authorizationHeader) {
103 +
        if (!options.required) {
104 +
          if (options.create) {
105 +
            ctx.session = await createSession(store);
106 +
          }
107 +
          return postFunction;
108 +
        }
109 +
        return badRequestOrRedirect('Authorization header not found.');
110 +
      }
111 +
112 +
      const content = authorizationHeader.split('Bearer ')[1] as string|undefined;
113 +
      if (!content) {
114 +
        return badRequestOrRedirect('Expected a bearer token. Scheme is Authorization: Bearer <token>.');
115 +
      }
116 +
117 +
      sessionID = content;
118 +
    }
119 +
120 +
    /* Verify the session ID */
121 +
122 +
    const session = await readSession(store, sessionID);
123 +
124 +
    if (!session) {
125 +
      const response = unauthorizedOrRedirect('token invalid or expired');
126 +
      if (options.cookie) {
127 +
        removeSessionCookie(response);
128 +
      }
129 +
      return response;
130 +
    }
131 +
132 +
    /* Verify CSRF token */
133 +
134 +
    if (
135 +
      options.cookie &&
136 +
      Config.get('settings.session.csrf.enabled', 'boolean', false) &&
137 +
      ![ 'GET', 'HEAD', 'OPTIONS' ].includes(ctx.request.method)
138 +
    ) {
139 +
      const expectedCsrftoken = session.get<string|undefined>('csrfToken');
140 +
      if (!expectedCsrftoken) {
141 +
        throw new Error(
142 +
          'Unexpected error: the session content does not have a "csrfToken" field. '
143 +
          + 'Are you sure you created the session with "createSession"?'
144 +
        );
145 +
      }
146 +
      const actualCsrfToken = ctx.request.body._csrf ||
147 +
        ctx.request.get('X-CSRF-Token') ||
148 +
        ctx.request.get('X-XSRF-Token');
149 +
      if (actualCsrfToken !== expectedCsrftoken) {
150 +
        return new HttpResponseForbidden('CSRF token missing or incorrect.');
151 +
      }
152 +
    }
153 +
154 +
    /* Set ctx.session */
155 +
156 +
    ctx.session = session;
157 +
158 +
    /* Set ctx.user */
159 +
160 +
    if (session.userId !== null && options.user) {
161 +
      ctx.user = await options.user(session.userId);
162 +
      if (!ctx.user) {
163 +
        await session.destroy();
164 +
        const response = unauthorizedOrRedirect('The token does not match any user.');
165 +
        if (options.cookie) {
166 +
          removeSessionCookie(response);
167 +
        }
168 +
        return response;
169 +
      }
170 +
    }
171 +
172 +
    return postFunction;
173 +
  }
174 +
175 +
  const openapi = [
176 +
    options.required ?
177 +
      ApiResponse(401, { description: 'Auth token is missing or invalid.' }) :
178 +
      ApiResponse(401, { description: 'Auth token is invalid.' })
179 +
  ];
180 +
181 +
  if (options.cookie) {
182 +
    const securityScheme: IApiSecurityScheme = {
183 +
      in: 'cookie',
184 +
      name: Config.get('settings.session.cookie.name', 'string', SESSION_DEFAULT_COOKIE_NAME),
185 +
      type: 'apiKey',
186 +
    };
187 +
    openapi.push(ApiDefineSecurityScheme('cookieAuth', securityScheme));
188 +
    if (options.required) {
189 +
      openapi.push(ApiSecurityRequirement({ cookieAuth: [] }));
190 +
    }
191 +
    if (Config.get('settings.session.csrf.enabled', 'boolean', false)) {
192 +
      openapi.push(ApiResponse(403, { description: 'CSRF token is missing or incorrect.' }));
193 +
    }
194 +
  } else {
195 +
    const securityScheme: IApiSecurityScheme = {
196 +
      scheme: 'bearer',
197 +
      type: 'http',
198 +
    };
199 +
    openapi.push(ApiDefineSecurityScheme('bearerAuth', securityScheme));
200 +
    if (options.required) {
201 +
      openapi.push(ApiSecurityRequirement({ bearerAuth: [] }));
202 +
    }
203 +
  }
204 +
205 +
  return Hook(hook, openapi, { openapi: options.openapi });
206 +
}

@@ -0,0 +1,31 @@
Loading
1 +
import { Class } from '../class.interface';
2 +
import { IApiComponents, IOpenAPI } from './interfaces';
3 +
4 +
export class OpenApi {
5 +
6 +
  private documentMap: Map<Class, IOpenAPI> = new Map();
7 +
  private componentMap: Map<object, IApiComponents|undefined> = new Map();
8 +
9 +
  addDocument(controllerClass: Class, document: IOpenAPI, controllers: object[] = []): void {
10 +
    this.documentMap.set(controllerClass, document);
11 +
    for (const controller of controllers) {
12 +
      this.componentMap.set(controller, document.components);
13 +
    }
14 +
  }
15 +
16 +
  getDocument(controllerClass: Class): IOpenAPI {
17 +
    const document = this.documentMap.get(controllerClass);
18 +
    if (!document) {
19 +
      throw new Error(
20 +
        `No OpenAPI document found associated with the controller ${controllerClass.name}. `
21 +
        + 'Are you sure you added the @ApiInfo decorator on the controller?'
22 +
      );
23 +
    }
24 +
    return document;
25 +
  }
26 +
27 +
  getComponents(controller: object): IApiComponents {
28 +
    return this.componentMap.get(controller) || {};
29 +
  }
30 +
31 +
}
0 32
imilarity index 100%
1 33
ename from packages/core/src/openapi/utils/dynamic.type.ts
2 34
ename to packages/core/src/core/openapi/utils/dynamic.type.ts
3 35
imilarity index 100%
4 36
ename from packages/core/src/openapi/utils/index.ts
5 37
ename to packages/core/src/core/openapi/utils/index.ts
6 38
imilarity index 100%
7 39
ename from packages/core/src/openapi/utils/merge-components.spec.ts
8 40
ename to packages/core/src/core/openapi/utils/merge-components.spec.ts
9 41
imilarity index 100%
10 42
ename from packages/core/src/openapi/utils/merge-components.ts
11 43
ename to packages/core/src/core/openapi/utils/merge-components.ts
12 44
imilarity index 99%
13 45
ename from packages/core/src/openapi/utils/merge-operations.spec.ts
14 46
ename to packages/core/src/core/openapi/utils/merge-operations.spec.ts

@@ -8,12 +8,9 @@
Loading
8 8
 * @export
9 9
 * @param {string} plainTextPassword - The password in clear text.
10 10
 * @param {string} passwordHash - The password hash generated by the `hashPassword` function.
11 -
 * @param {{ legacy?: boolean }} [options={}]
12 11
 * @returns {Promise<boolean>} True if the hash and the password match. False otherwise.
13 12
 */
14 -
export async function verifyPassword(plainTextPassword: string, passwordHash: string,
15 -
                                     options: { legacy?: boolean } = {}): Promise<boolean> {
16 -
  const legacy = options.legacy || false;
13 +
export async function verifyPassword(plainTextPassword: string, passwordHash: string): Promise<boolean> {
17 14
  const [ algorithm, iterations, salt, derivedKey ] = passwordHash.split('$');
18 15
19 16
  strictEqual(algorithm, 'pbkdf2_sha256', 'Invalid algorithm.');
@@ -23,12 +20,12 @@
Loading
23 20
  strictEqual(typeof derivedKey, 'string', 'Invalid password format.');
24 21
  strictEqual(isNaN(parseInt(iterations, 10)), false, 'Invalid password format.');
25 22
26 -
  const saltBuffer = Buffer.from(salt, legacy ? 'hex' : 'base64');
27 -
  const derivedKeyBuffer = Buffer.from(derivedKey, legacy ? 'hex' : 'base64');
23 +
  const saltBuffer = Buffer.from(salt, 'base64');
24 +
  const derivedKeyBuffer = Buffer.from(derivedKey, 'base64');
28 25
  const digest = 'sha256'; // TODO: depends on the algorthim var
29 26
  const password = await promisify(pbkdf2)(
30 27
    plainTextPassword,
31 -
    legacy ? saltBuffer.toString('hex') : saltBuffer,
28 +
    saltBuffer,
32 29
    parseInt(iterations, 10),
33 30
    derivedKeyBuffer.length,
34 31
    digest

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiSecurityRequirement } from '../interfaces';
4 4
5 5
export function getApiSecurity(
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-servers.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-servers.spec.ts
9 9
imilarity index 74%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-servers.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-servers.ts

@@ -23,11 +23,11 @@
Loading
23 23
export function getAjvInstance(): Ajv.Ajv {
24 24
  if (!_instanceWrapper.instance) {
25 25
    _instanceWrapper.instance = new Ajv({
26 -
      allErrors: Config.get2('settings.ajv.allErrors', 'boolean'),
27 -
      coerceTypes: Config.get2('settings.ajv.coerceTypes', 'boolean', true),
28 -
      nullable: Config.get2('settings.ajv.nullable', 'boolean'),
29 -
      removeAdditional: Config.get2('settings.ajv.removeAdditional', 'boolean|string', true) as boolean|'all'|'failing',
30 -
      useDefaults: Config.get2('settings.ajv.useDefaults', 'boolean|string', true) as boolean|'empty'|'shared',
26 +
      allErrors: Config.get('settings.ajv.allErrors', 'boolean'),
27 +
      coerceTypes: Config.get('settings.ajv.coerceTypes', 'boolean', true),
28 +
      nullable: Config.get('settings.ajv.nullable', 'boolean'),
29 +
      removeAdditional: Config.get('settings.ajv.removeAdditional', 'boolean|string', true) as boolean|'all'|'failing',
30 +
      useDefaults: Config.get('settings.ajv.useDefaults', 'boolean|string', true) as boolean|'empty'|'shared',
31 31
    });
32 32
  }
33 33
  return _instanceWrapper.instance;

@@ -0,0 +1,17 @@
Loading
1 +
import { Session } from './session';
2 +
import { SessionStore } from './session-store';
3 +
4 +
export async function readSession(store: SessionStore, id: string): Promise<Session | null> {
5 +
  const state = await store.read(id);
6 +
  if (state === null) {
7 +
    return null;
8 +
  }
9 +
10 +
  const session = new Session(store, state, { exists: true });
11 +
  if (session.isExpired) {
12 +
    await session.destroy();
13 +
    return null;
14 +
  }
15 +
16 +
  return session;
17 +
}

@@ -1,106 +1,278 @@
Loading
1 1
// FoalTS
2 -
import { signToken, verifySignedToken } from '../common';
2 +
import { generateToken } from '../common';
3 3
import { Config } from '../core';
4 +
import {
5 +
  SESSION_DEFAULT_ABSOLUTE_TIMEOUT,
6 +
  SESSION_DEFAULT_GARBAGE_COLLECTOR_PERIODICITY,
7 +
  SESSION_DEFAULT_INACTIVITY_TIMEOUT,
8 +
} from './constants';
9 +
import { SessionState } from './session-state.interface';
10 +
import { SessionStore } from './session-store';
4 11
5 12
/**
6 -
 * Representation of a server/database session.
13 +
 * Representation of a server/database/file session.
7 14
 *
8 15
 * @export
9 16
 * @class Session
10 17
 */
11 18
export class Session {
12 19
20 +
  private readonly oldFlash: { [key: string]: any };
21 +
  private oldId = '';
22 +
  private status: 'new'|'exists'|'regenerated'|'destroyed';
23 +
13 24
  /**
14 -
   * Verify a session token and return the sessionID if the token is valid.
25 +
   * Retuns the user ID. If the session is anonymous, the value is null.
15 26
   *
16 -
   * @static
17 -
   * @param {string} token - The session token to verify.
18 -
   * @returns {(string|false)} False if the token is invalid. Otherwise, the returned value is the session ID.
27 +
   * @readonly
28 +
   * @type {(string|number|null)}
19 29
   * @memberof Session
20 30
   */
21 -
  static verifyTokenAndGetId(token: string): string|false {
22 -
    const secret = Config.getOrThrow(
23 -
      'settings.session.secret',
24 -
      'string',
25 -
      'You must provide a secret when using sessions.'
26 -
    );
27 -
28 -
    return verifySignedToken(token, secret);
31 +
  get userId(): string|number|null {
32 +
    return this.state.userId;
29 33
  }
30 34
31 -
  private modified = false;
35 +
  /**
36 +
   * Returns true if the session has expired due to inactivity
37 +
   * or because it reached the maximum lifetime allowed.
38 +
   *
39 +
   * @readonly
40 +
   * @type {boolean}
41 +
   * @memberof Session
42 +
   */
43 +
  get isExpired(): boolean {
44 +
    const { absoluteTimeout, inactivityTimeout } = this.getTimeouts();
45 +
    const now = this.getTime();
32 46
33 -
  constructor(readonly sessionID: string, private sessionContent: any, readonly createdAt: number) {
34 -
    if (sessionID.includes('.')) {
35 -
      throw new Error('A session ID cannot include dots.');
47 +
    if (now - this.state.updatedAt >= inactivityTimeout) {
48 +
      return true;
36 49
    }
50 +
    if (now - this.state.createdAt >= absoluteTimeout) {
51 +
      return true;
52 +
    }
53 +
54 +
    return false;
37 55
  }
38 56
39 57
  /**
40 -
   * Return true if an element was added/replaced in the session
58 +
   * Returns the session expiration time in seconds.
41 59
   *
42 60
   * @readonly
43 -
   * @type {boolean}
61 +
   * @type {number}
44 62
   * @memberof Session
45 63
   */
46 -
  get isModified(): boolean {
47 -
    return this.modified;
64 +
  get expirationTime(): number {
65 +
    const { absoluteTimeout, inactivityTimeout } = this.getTimeouts();
66 +
    return Math.min(
67 +
      this.state.updatedAt + inactivityTimeout,
68 +
      this.state.createdAt + absoluteTimeout,
69 +
    );
70 +
  }
71 +
72 +
  constructor(
73 +
    private readonly store: SessionStore,
74 +
    private readonly state: SessionState,
75 +
    options: { exists: boolean },
76 +
  ) {
77 +
    this.status = options.exists ? 'exists' : 'new';
78 +
    this.oldFlash = state.flash;
79 +
    state.flash = {};
48 80
  }
49 81
50 82
  /**
51 -
   * Add/replace an element in the session. This operation is not saved
52 -
   * in the saved unless you call SessionStore.update(session).
83 +
   * Sets or replaces the userId associated with the session.
53 84
   *
54 -
   * @param {string} key
55 -
   * @param {*} value
85 +
   * @param {({ id: number|string })} user - The user containing the ID.
56 86
   * @memberof Session
57 87
   */
58 -
  set(key: string, value: any): void {
59 -
    this.sessionContent[key] = value;
60 -
    this.modified = true;
88 +
  setUser(user: { id: number|string|object } | { _id: number|string|object }): void {
89 +
    // tslint:disable-next-line
90 +
    const id: number|string|object = (user as any).id ?? (user as any)._id;
91 +
    if (typeof id === 'object') {
92 +
      this.state.userId = id.toString();
93 +
      return;
94 +
    }
95 +
    this.state.userId = id;
61 96
  }
62 97
63 98
  /**
64 -
   * The value of an element in the session content.
99 +
   * Adds or replaces an element in the session. This operation is not saved
100 +
   * in the saved unless you call the "commit" function.
101 +
   *
102 +
   * @param {string} key - The property key.
103 +
   * @param {*} value - The property value.
104 +
   * @param {{ flash?: boolean }} [options={}] If flash is true, the key/value
105 +
   * will be erased at the end of the next request.
106 +
   * @memberof Session
107 +
   */
108 +
  set(key: string, value: any, options: { flash?: boolean } = {}): void {
109 +
    if (options.flash) {
110 +
      this.state.flash[key] = value;
111 +
    } else {
112 +
      this.state.content[key] = value;
113 +
    }
114 +
  }
115 +
116 +
  /**
117 +
   * Gets the value of a key in the session content.
65 118
   *
66 119
   * @template T
67 -
   * @param {string} key - The property key
68 -
   * @returns {(T | undefined)} The property valye
120 +
   * @param {string} key - The property key.
121 +
   * @returns {(T | undefined)} The property value.
69 122
   * @memberof Session
70 123
   */
71 124
  get<T>(key: string): T | undefined;
72 125
  get<T>(key: string, defaultValue: any): T;
73 126
  get(key: string, defaultValue?: any): any {
74 -
    if (!this.sessionContent.hasOwnProperty(key)) {
75 -
      return defaultValue;
127 +
    if (this.oldFlash.hasOwnProperty(key)) {
128 +
      return this.oldFlash[key];
76 129
    }
77 -
    return this.sessionContent[key];
130 +
    if (this.state.content.hasOwnProperty(key)) {
131 +
      return this.state.content[key];
132 +
    }
133 +
    return defaultValue;
78 134
  }
79 135
80 136
  /**
81 -
   * Get the session token. This token is used by `@TokenRequired` and `@TokenOptional` to retreive
82 -
   * the session and the authenticated user if she/he exists.
137 +
   * Gets the session ID.
83 138
   *
84 -
   * @returns {string} - The session token.
139 +
   * @returns {string} - The session ID.
85 140
   * @memberof Session
86 141
   */
87 142
  getToken(): string {
88 -
    const secret = Config.getOrThrow(
89 -
      'settings.session.secret',
90 -
      'string',
91 -
      'You must provide a secret when using sessions.'
92 -
    );
93 -
    return signToken(this.sessionID, secret);
143 +
    return this.state.id;
144 +
  }
145 +
146 +
  /**
147 +
   * Regenerates the session with a new ID. It is recommended
148 +
   * to regenerate the session ID after any privilege level change
149 +
   * within the associated user session.
150 +
   *
151 +
   * Common scenario: an anonymous user is authenticated.
152 +
   *
153 +
   * @returns {Promise<void>}
154 +
   * @memberof Session
155 +
   */
156 +
  async regenerateID(): Promise<void> {
157 +
    this.oldId = this.state.id;
158 +
    this.state.id = await generateToken();
159 +
    this.status = 'regenerated';
94 160
  }
95 161
96 162
  /**
97 -
   * Get a copy of the session content.
163 +
   * Destroys the session.
98 164
   *
99 -
   * @returns {object} - The session content copy.
165 +
   * @returns {Promise<void>}
100 166
   * @memberof Session
101 167
   */
102 -
  getContent(): object {
103 -
    return { ...this.sessionContent };
168 +
  async destroy(): Promise<void> {
169 +
    await this.store.destroy(this.state.id);
170 +
    this.status = 'destroyed';
171 +
  }
172 +
173 +
  /**
174 +
   * Returns true if the method `destroy` has previously been called.
175 +
   *
176 +
   * @readonly
177 +
   * @type {boolean}
178 +
   * @memberof Session
179 +
   */
180 +
  get isDestroyed(): boolean {
181 +
    return this.status === 'destroyed';
182 +
  }
183 +
184 +
  /**
185 +
   * Saves or updates the session and extends its lifetime.
186 +
   *
187 +
   * If the session has already been destroyed, an error is thrown.
188 +
   *
189 +
   * This function calls periodically the store method "cleanUpExpiredSessions".
190 +
   *
191 +
   * @returns {Promise<void>}
192 +
   * @memberof Session
193 +
   */
194 +
  async commit(): Promise<void> {
195 +
    const { absoluteTimeout, inactivityTimeout } = this.getTimeouts();
196 +
197 +
    if (this.shouldCleanUpExpiredSessions()) {
198 +
      await this.store.cleanUpExpiredSessions(
199 +
        inactivityTimeout,
200 +
        absoluteTimeout,
201 +
      );
202 +
    }
203 +
204 +
    this.state.updatedAt = this.getTime();
205 +
206 +
    switch (this.status) {
207 +
      case 'regenerated':
208 +
        await this.store.destroy(this.oldId);
209 +
        await this.store.save(this.state, inactivityTimeout);
210 +
        this.status = 'exists';
211 +
        break;
212 +
      case 'new':
213 +
        await this.store.save(this.state, inactivityTimeout);
214 +
        this.status = 'exists';
215 +
        break;
216 +
      case 'exists':
217 +
        await this.store.update(this.state, inactivityTimeout);
218 +
        break;
219 +
      case 'destroyed':
220 +
        throw new Error('Impossible to commit the session. Session already destroyed.');
221 +
      default:
222 +
        break;
223 +
    }
224 +
  }
225 +
226 +
  /**
227 +
   * Returns the current time in seconds.
228 +
   *
229 +
   * @private
230 +
   * @returns {number} The current time.
231 +
   * @memberof Session
232 +
   */
233 +
  private getTime(): number {
234 +
    return Math.trunc(Date.now() / 1000);
235 +
  }
236 +
237 +
  private shouldCleanUpExpiredSessions(): boolean {
238 +
    const periodicity = Config.get(
239 +
      'settings.session.garbageCollector.periodicity',
240 +
      'number',
241 +
      SESSION_DEFAULT_GARBAGE_COLLECTOR_PERIODICITY,
242 +
    );
243 +
    return Math.trunc(Math.random() * periodicity) === 0;
244 +
  }
245 +
246 +
  private getTimeouts(): { absoluteTimeout: number, inactivityTimeout: number} {
247 +
    const inactivityTimeout = Config.get(
248 +
      'settings.session.expirationTimeouts.inactivity',
249 +
      'number',
250 +
      SESSION_DEFAULT_INACTIVITY_TIMEOUT
251 +
    );
252 +
    if (inactivityTimeout < 0) {
253 +
      throw new Error(
254 +
        '[CONFIG] The value of settings.session.expirationTimeouts.inactivity must be a positive number.'
255 +
      );
256 +
    }
257 +
258 +
    const absoluteTimeout = Config.get(
259 +
      'settings.session.expirationTimeouts.absolute',
260 +
      'number',
261 +
      SESSION_DEFAULT_ABSOLUTE_TIMEOUT,
262 +
    );
263 +
    if (absoluteTimeout < 0) {
264 +
      throw new Error(
265 +
        '[CONFIG] The value of settings.session.expirationTimeouts.absolute must be a positive number.'
266 +
      );
267 +
    }
268 +
269 +
    if (absoluteTimeout < inactivityTimeout) {
270 +
      throw new Error(
271 +
        '[CONFIG] The value of settings.session.expirationTimeouts.absolute must be greater than *.inactivity.'
272 +
      );
273 +
    }
274 +
275 +
    return { absoluteTimeout, inactivityTimeout };
104 276
  }
105 277
106 278
}

@@ -1,6 +1,17 @@
Loading
1 +
// std
2 +
import { ValidateFunction } from 'ajv';
3 +
1 4
// FoalTS
2 -
import { Config, Context, Hook, HookDecorator, HttpResponseBadRequest } from '../../core';
3 -
import { ApiParameter, ApiResponse, IApiPathParameter, IApiSchema } from '../../openapi';
5 +
import {
6 +
  ApiParameter,
7 +
  ApiResponse,
8 +
  Context,
9 +
  Hook,
10 +
  HookDecorator,
11 +
  HttpResponseBadRequest,
12 +
  OpenApi,
13 +
  ServiceManager
14 +
} from '../../core';
4 15
import { getAjvInstance } from '../utils';
5 16
import { isFunction } from './is-function.util';
6 17
@@ -20,37 +31,37 @@
Loading
20 31
  schema: object | ((controller: any) => object),
21 32
  options: { openapi?: boolean } = {}
22 33
): HookDecorator {
23 -
  const ajv = getAjvInstance();
34 +
  let validateSchema: ValidateFunction|undefined;
24 35
25 -
  function validate(this: any, ctx: Context) {
26 -
    const paramsSchema = {
27 -
      properties: {
28 -
        [name]: isFunction(schema) ? schema(this) : schema
29 -
      },
30 -
      required: [ name ],
31 -
      type: 'object',
32 -
    };
33 -
    if (!ajv.validate(paramsSchema, ctx.request.params)) {
34 -
      return new HttpResponseBadRequest({ pathParams: ajv.errors });
35 -
    }
36 -
  }
36 +
  function validate(this: any, ctx: Context, services: ServiceManager) {
37 +
    if (!validateSchema) {
38 +
      const ajvSchema = isFunction(schema) ? schema(this) : schema;
39 +
      const components = services.get(OpenApi).getComponents(this);
37 40
38 -
  return (target: any, propertyKey?: string) =>  {
39 -
    Hook(validate)(target, propertyKey);
40 -
41 -
    if (options.openapi === false ||
42 -
      (options.openapi === undefined && !Config.get2('settings.openapi.useHooks', 'boolean'))
43 -
    ) {
44 -
      return;
41 +
      validateSchema = getAjvInstance().compile({
42 +
        components,
43 +
        properties: {
44 +
          [name]: ajvSchema
45 +
        },
46 +
        required: [ name ],
47 +
        type: 'object',
48 +
      });
45 49
    }
46 50
47 -
    function makeParameter(schema: IApiSchema): IApiPathParameter {
48 -
      return { in: 'path', name, required: true, schema };
51 +
    if (!validateSchema(ctx.request.params)) {
52 +
      return new HttpResponseBadRequest({ pathParams: validateSchema.errors });
49 53
    }
54 +
  }
50 55
51 -
    const apiPathParameter = isFunction(schema) ? (c: any) => makeParameter(schema(c)) : makeParameter(schema);
56 +
  const openapi = [
57 +
    ApiParameter((c: any) => ({
58 +
      in: 'path',
59 +
      name,
60 +
      required: true,
61 +
      schema: isFunction(schema) ? schema(c) : schema,
62 +
    })),
63 +
    ApiResponse(400, { description: 'Bad request.' })
64 +
  ];
52 65
53 -
    ApiParameter(apiPathParameter)(target, propertyKey);
54 -
    ApiResponse(400, { description: 'Bad request.' })(target, propertyKey);
55 -
  };
66 +
  return Hook(validate, openapi, options);
56 67
}

@@ -9,6 +9,8 @@
Loading
9 9
  IApiSecurityScheme, IApiServer, IApiTag
10 10
} from './interfaces';
11 11
12 +
export type OpenApiDecorator = (target: any, propertyKey?: string) => any;
13 +
12 14
function AddMetadataItem<T>(metadataKey: string, item: T) {
13 15
  return (target: any, propertyKey?: string) => {
14 16
    // Note that propertyKey can be undefined as it's an optional parameter in getMetadata.
@@ -28,130 +30,130 @@
Loading
28 30
  };
29 31
}
30 32
31 -
export function ApiInfo(info: IApiInfo | ((controller: any) => IApiInfo)) {
33 +
export function ApiInfo(info: IApiInfo | ((controller: any) => IApiInfo)): OpenApiDecorator {
32 34
  return Reflect.metadata('api:document:info', info);
33 35
}
34 36
35 -
export function ApiOperationDescription(description: string | ((controller: any) => string)) {
37 +
export function ApiOperationDescription(description: string | ((controller: any) => string)): OpenApiDecorator {
36 38
  return Reflect.metadata('api:operation:description', description);
37 39
}
38 40
39 -
export function ApiOperationId(operationId: string | ((controller: any) => string)) {
41 +
export function ApiOperationId(operationId: string | ((controller: any) => string)): OpenApiDecorator {
40 42
  return Reflect.metadata('api:operation:operationId', operationId);
41 43
}
42 44
43 -
export function ApiOperationSummary(summary: string | ((controller: any) => string)) {
45 +
export function ApiOperationSummary(summary: string | ((controller: any) => string)): OpenApiDecorator {
44 46
  return Reflect.metadata('api:operation:summary', summary);
45 47
}
46 48
47 -
export function ApiServer(server: IApiServer | ((controller: any) => IApiServer)) {
49 +
export function ApiServer(server: IApiServer | ((controller: any) => IApiServer)): OpenApiDecorator {
48 50
  return AddMetadataItem('api:documentOrOperation:servers', server);
49 51
}
50 52
51 53
export function ApiSecurityRequirement(
52 54
  securityRequirement: IApiSecurityRequirement | ((controller: any) => IApiSecurityRequirement)
53 -
) {
55 +
): OpenApiDecorator {
54 56
  return AddMetadataItem('api:documentOrOperation:security', securityRequirement);
55 57
}
56 58
57 -
export function ApiDefineTag(tag: IApiTag | ((controller: any) => IApiTag)) {
59 +
export function ApiDefineTag(tag: IApiTag | ((controller: any) => IApiTag)): OpenApiDecorator {
58 60
  return AddMetadataItem('api:document:tags', tag);
59 61
}
60 62
61 63
export function ApiExternalDoc(
62 64
  externalDoc: IApiExternalDocumentation | ((controller: any) => IApiExternalDocumentation)
63 -
) {
65 +
): OpenApiDecorator {
64 66
  return Reflect.metadata('api:documentOrOperation:externalDocs', externalDoc);
65 67
}
66 68
67 -
export function ApiOperation(operation: IApiOperation | ((controller: any) => IApiOperation)) {
69 +
export function ApiOperation(operation: IApiOperation | ((controller: any) => IApiOperation)): OpenApiDecorator {
68 70
  return Reflect.metadata('api:operation', operation);
69 71
}
70 72
71 -
export function ApiUseTag(tag: string | ((controller: any) => string)) {
73 +
export function ApiUseTag(tag: string | ((controller: any) => string)): OpenApiDecorator {
72 74
  return AddMetadataItem('api:operation:tags', tag);
73 75
}
74 76
75 77
export function ApiParameter(
76 78
  parameter: IApiParameter | IApiReference | ((controller: any) => IApiParameter | IApiReference)
77 -
) {
79 +
): OpenApiDecorator {
78 80
  return AddMetadataItem('api:operation:parameters', parameter);
79 81
}
80 82
81 83
export function ApiRequestBody(
82 84
  requestBody: IApiRequestBody | IApiReference | ((controller: any) => IApiRequestBody | IApiReference)
83 -
) {
85 +
): OpenApiDecorator {
84 86
  return Reflect.metadata('api:operation:requestBody', requestBody);
85 87
}
86 88
87 89
export function ApiResponse(
88 90
  key: 'default'|'1XX'|'2XX'|'3XX'|'4XX'|'5XX'|number,
89 91
  response: IApiResponse | IApiReference | ((controller: any) => IApiResponse | IApiReference)
90 -
) {
92 +
): OpenApiDecorator {
91 93
  return AddMetadataProperty('api:operation:responses', key.toString(), response);
92 94
}
93 95
94 96
export function ApiCallback(
95 97
  key: string, callback: IApiCallback | IApiReference | ((controller: any) => IApiCallback | IApiReference)
96 -
) {
98 +
): OpenApiDecorator {
97 99
  return AddMetadataProperty('api:operation:callbacks', key, callback);
98 100
}
99 101
100 -
export function ApiDeprecated(deprecated: boolean | ((controller: any) => boolean) = true) {
102 +
export function ApiDeprecated(deprecated: boolean | ((controller: any) => boolean) = true): OpenApiDecorator {
101 103
  return Reflect.metadata('api:operation:deprecated', deprecated);
102 104
}
103 105
104 106
export function ApiDefineSchema(
105 107
  key: string, schema: IApiSchema | IApiReference | ((controller: any) => IApiSchema | IApiReference)
106 -
) {
108 +
): OpenApiDecorator {
107 109
  return AddMetadataProperty('api:components:schemas', key, schema);
108 110
}
109 111
110 112
export function ApiDefineResponse(
111 113
  key: string, response: IApiResponse | IApiReference | ((controller: any) => IApiResponse | IApiReference)
112 -
) {
114 +
): OpenApiDecorator {
113 115
  return AddMetadataProperty('api:components:responses', key, response);
114 116
}
115 117
116 118
export function ApiDefineParameter(
117 119
  key: string, parameter: IApiParameter | IApiReference | ((controller: any) => IApiParameter | IApiReference)
118 -
) {
120 +
): OpenApiDecorator {
119 121
  return AddMetadataProperty('api:components:parameters', key, parameter);
120 122
}
121 123
122 124
export function ApiDefineExample(
123 125
  key: string, example: IApiExample | IApiReference | ((controller: any) => IApiExample | IApiReference)
124 -
) {
126 +
): OpenApiDecorator {
125 127
  return AddMetadataProperty('api:components:examples', key, example);
126 128
}
127 129
128 130
export function ApiDefineRequestBody(
129 131
  key: string, requestBody: IApiRequestBody | IApiReference | ((controller: any) => IApiRequestBody | IApiReference)
130 -
) {
132 +
): OpenApiDecorator {
131 133
  return AddMetadataProperty('api:components:requestBodies', key, requestBody);
132 134
}
133 135
134 136
export function ApiDefineHeader(
135 137
  key: string, header: IApiHeader | IApiReference | ((controller: any) => IApiHeader | IApiReference)
136 -
) {
138 +
): OpenApiDecorator {
137 139
  return AddMetadataProperty('api:components:headers', key, header);
138 140
}
139 141
140 142
export function ApiDefineSecurityScheme(
141 143
  key: string,
142 144
  securityScheme: IApiSecurityScheme | IApiReference | ((controller: any) => IApiSecurityScheme | IApiReference)
143 -
) {
145 +
): OpenApiDecorator {
144 146
  return AddMetadataProperty('api:components:securitySchemes', key, securityScheme);
145 147
}
146 148
147 149
export function ApiDefineLink(
148 150
  key: string, link: IApiLink | IApiReference | ((controller: any) => IApiLink | IApiReference)
149 -
) {
151 +
): OpenApiDecorator {
150 152
  return AddMetadataProperty('api:components:links', key, link);
151 153
}
152 154
153 155
export function ApiDefineCallback(
154 156
  key: string, callback: IApiCallback | IApiReference | ((controller: any) => IApiCallback | IApiReference)
155 -
) {
157 +
): OpenApiDecorator {
156 158
  return AddMetadataProperty('api:components:callbacks', key, callback);
157 159
}
158 160
imilarity index 70%
159 161
ename from packages/core/src/openapi/index.ts
160 162
ename to packages/core/src/core/openapi/index.ts

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiExternalDocumentation } from '../interfaces';
4 4
5 5
export function getApiExternalDocs(
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-info.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-info.spec.ts
9 9
imilarity index 69%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-info.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-info.ts

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiServer } from '../interfaces';
4 4
5 5
export function getApiServers(
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-tags.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-tags.spec.ts
9 9
imilarity index 72%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-tags.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-tags.ts

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
4 4
export function getApiUsedTags(
5 5
  controllerClass: Class, propertyKey?: string
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/index.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/index.ts

@@ -1,10 +1,4 @@
Loading
1 -
// std
2 -
import { createReadStream, exists, stat } from 'fs';
3 -
import { basename, join } from 'path';
4 -
import { promisify } from 'util';
5 -
6 -
// 3p
7 -
import { getType } from 'mime';
1 +
import { Context } from './contexts';
8 2
9 3
/**
10 4
 * Cookie options of the HttpResponse.setCookie method.
@@ -269,53 +263,6 @@
Loading
269 263
    (typeof obj === 'object' && obj !== null && obj.isHttpResponseOK === true);
270 264
}
271 265
272 -
/**
273 -
 * Create an HttpResponseOK whose content is the specified file. If returned in a controller,
274 -
 * the server sends the file in streaming.
275 -
 *
276 -
 * @param {Object} options - The options used to create the HttpResponseOK.
277 -
 * @param {string} options.directory - Directory where the file is located.
278 -
 * @param {string} options.file - Name of the file with its extension. If a path is given,
279 -
 * only the basename is kept.
280 -
 * @param {boolean} [options.forceDownload=false] - Indicate if the browser should download
281 -
 * the file directly without trying to display it in the window.
282 -
 * @param {filename} [options.string=options.file] - Default name used by the browser when
283 -
 * saving the file to the disk.
284 -
 * @deprecated
285 -
 * @returns {Promise<HttpResponseOK>}
286 -
 */
287 -
export async function createHttpResponseFile(options:
288 -
  { directory: string, file: string, forceDownload?: boolean, filename?: string }
289 -
): Promise<HttpResponseOK> {
290 -
  const file = basename(options.file);
291 -
  const filePath = join(options.directory, file);
292 -
  if (!await new Promise(resolve => exists(filePath, resolve))) {
293 -
    throw new Error(`The file "${filePath}" does not exist.`);
294 -
  }
295 -
296 -
  const stats = await promisify(stat)(filePath);
297 -
  if (stats.isDirectory()) {
298 -
    throw new Error(`The directory "${filePath}" is not a file.`);
299 -
  }
300 -
301 -
  const stream = createReadStream(filePath);
302 -
  const response = new HttpResponseOK(stream, { stream: true });
303 -
304 -
  const mimeType = getType(options.file);
305 -
  if (mimeType) {
306 -
    response.setHeader('Content-Type', mimeType);
307 -
  }
308 -
  response
309 -
    .setHeader('Content-Length', stats.size.toString())
310 -
    .setHeader(
311 -
      'Content-Disposition',
312 -
      (options.forceDownload ? 'attachment' : 'inline')
313 -
      + `; filename="${options.filename || file}"`
314 -
    );
315 -
316 -
  return response;
317 -
}
318 -
319 266
/**
320 267
 * Represent an HTTP response with the status 201 - CREATED.
321 268
 *
@@ -966,6 +913,8 @@
Loading
966 913
   * @memberof HttpResponseInternalServerError
967 914
   */
968 915
  readonly isHttpResponseInternalServerError = true;
916 +
  readonly error?: Error;
917 +
  readonly ctx?: Context;
969 918
  statusCode = 500;
970 919
  statusMessage = 'INTERNAL SERVER ERROR';
971 920
@@ -974,8 +923,10 @@
Loading
974 923
   * @param {*} [body] - Optional body of the response.
975 924
   * @memberof HttpResponseInternalServerError
976 925
   */
977 -
  constructor(body?: any, options: { stream?: boolean } = {}) {
926 +
  constructor(body?: any, options: { stream?: boolean, error?: Error, ctx?: Context } = {}) {
978 927
    super(body, options);
928 +
    this.error = options.error;
929 +
    this.ctx = options.ctx;
979 930
  }
980 931
}
981 932

@@ -1,6 +1,8 @@
Loading
1 +
export * from './app.controller.interface';
1 2
export * from './class.interface';
2 3
export { createController } from './controllers';
3 4
export * from './http';
5 +
export * from './openapi';
4 6
export * from './hooks';
5 7
export * from './routes';
6 8
export * from './config';

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
4 4
export function getApiDeprecated(
5 5
  controllerClass: Class, propertyKey?: string
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-external-docs.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-external-docs.spec.ts
9 9
imilarity index 77%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-external-docs.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-external-docs.ts

@@ -1,20 +1,18 @@
Loading
1 1
// FoalTS
2 -
import { generateToken } from '../common';
3 -
import { Config } from '../core';
4 -
import { SESSION_DEFAULT_ABSOLUTE_TIMEOUT, SESSION_DEFAULT_INACTIVITY_TIMEOUT } from './constants';
5 -
import { Session } from './session';
2 +
import { SessionState } from './session-state.interface';
6 3
7 -
export interface SessionOptions {
8 -
  csrfToken?: boolean;
4 +
export class SessionAlreadyExists extends Error {
5 +
  readonly name = 'SessionAlreadyExists';
9 6
}
10 7
11 8
/**
12 -
 * Abstract class to be override when creating a session storage service.
9 +
 * Store used to create, read, update and delete sessions.
13 10
 *
14 -
 * A session store peforms CRUD operations on sessions and can store them in
15 -
 * a database, file system, memory, etc.
11 +
 * All session stores must inherit this abstract class.
16 12
 *
17 -
 * Examples of Store: TypeORMStore, RedisStore, MongoDBStore.
13 +
 * When this class is used with the `@dependency` decorator,
14 +
 * it returns the `ConcreteSessionStore` class from the file or the package specified
15 +
 * with the configuration key "settings.session.store".
18 16
 *
19 17
 * @export
20 18
 * @abstract
@@ -26,120 +24,53 @@
Loading
26 24
  static concreteClassName = 'ConcreteSessionStore';
27 25
28 26
  /**
29 -
   * Read session expiration timeouts from the configuration.
27 +
   * Saves the session for the first time.
30 28
   *
31 -
   * The values are in seconds.
32 -
   *
33 -
   * Default values are:
34 -
   * - 15 min for inactivity timeout
35 -
   * - 1 week for absolute timeout
36 -
   *
37 -
   * This method throws an error if one of the following is true:
38 -
   * - The given inactivity timeout is negative.
39 -
   * - The given absolute timeout is negative.
40 -
   * - The given inactivity timeout is greater than the absolute timeout.
41 -
   *
42 -
   * @static
43 -
   * @returns {{ inactivity: number , absolute: number }} The expiration timeouts
44 -
   * @memberof Store
45 -
   */
46 -
  static getExpirationTimeouts(): { inactivity: number , absolute: number } {
47 -
    const result = {
48 -
      absolute: Config.get2('settings.session.expirationTimeouts.absolute', 'number', SESSION_DEFAULT_ABSOLUTE_TIMEOUT),
49 -
      inactivity: Config.get2(
50 -
        'settings.session.expirationTimeouts.inactivity',
51 -
        'number',
52 -
        SESSION_DEFAULT_INACTIVITY_TIMEOUT
53 -
      ),
54 -
    };
55 -
    if (result.absolute < 0) {
56 -
      throw new Error('[CONFIG] The value of settings.session.expirationTimeouts.absolute must be a positive number.');
57 -
    }
58 -
    if (result.inactivity < 0) {
59 -
      throw new Error(
60 -
        '[CONFIG] The value of settings.session.expirationTimeouts.inactivity must be a positive number.'
61 -
      );
62 -
    }
63 -
    if (result.absolute < result.inactivity) {
64 -
      throw new Error(
65 -
        '[CONFIG] The value of settings.session.expirationTimeouts.absolute must be greater than *.inactivity.'
66 -
      );
67 -
    }
68 -
    return result;
69 -
  }
70 -
71 -
  /**
72 -
   * Create and save an new session from a user.
73 -
   *
74 -
   * @param {({ id: string|number })} user - User id.
75 -
   * @param {SessionOptions} options - Session options.
76 -
   * @param {boolean} [options.csrfToken] - Generate and add a `csrfToken` to the sessionContent.
77 -
   * @returns {Promise<Session>} The created session.
78 -
   * @memberof Store
79 -
   */
80 -
  createAndSaveSessionFromUser(user: { id: string|number }, options?: SessionOptions): Promise<Session> {
81 -
    return this.createAndSaveSession({ userId: user.id }, options);
82 -
  }
83 -
84 -
  /**
85 -
   * Create and save a new session.
86 -
   *
87 -
   * This method *MUST* call the `generateSessionID` method to generate the session ID.
88 -
   * This method *MUST* call the `applySessionOptions` method to extend the sessionContent.
29 +
   * If a session already exists with the given ID, a SessionAlreadyExists error MUST be thrown.
89 30
   *
90 31
   * @abstract
91 -
   * @param {object} sessionContent - The content of the session (often includes the user ID).
92 -
   * @param {SessionOptions} options - Session options.
93 -
   * @param {boolean} [options.csrfToken] - Generate and add a `csrfToken` to the sessionContent.
94 -
   * @returns {Promise<Session>} The created session.
95 -
   * @memberof Store
96 -
   */
97 -
  abstract createAndSaveSession(sessionContent: object, options?: SessionOptions): Promise<Session>;
98 -
  /**
99 -
   * Update and extend the lifetime of a session.
100 -
   *
101 -
   * Depending on the implementation, the internal behavior can be similar to "update" or "upsert".
102 -
   *
103 -
   * @abstract
104 -
   * @param {Session} session - The session containaing the updated content.
32 +
   * @param {SessionState} state - The state of the session.
33 +
   * @param {number} maxInactivity - The maximum idle activity of the session (useful for cache stores).
105 34
   * @returns {Promise<void>}
106 35
   * @memberof Store
107 36
   */
108 -
  abstract update(session: Session): Promise<void>;
37 +
  abstract save(state: SessionState, maxInactivity: number): Promise<void>;
109 38
  /**
110 -
   * Delete a session, whether it exists or not.
39 +
   * Reads a session.
40 +
   *
41 +
   * If the session does not exist, the value `null` MUST be returned.
111 42
   *
112 43
   * @abstract
113 -
   * @param {string} sessionID - The ID of the session.
114 -
   * @returns {Promise<void>}
44 +
   * @param {string} id - The ID of the session.
45 +
   * @returns {(Promise<SessionState|null>)} The state of the session.
115 46
   * @memberof Store
116 47
   */
117 -
  abstract destroy(sessionID: string): Promise<void>;
48 +
  abstract read(id: string): Promise<SessionState | null>;
118 49
  /**
119 -
   * Read a session from its ID.
50 +
   * Updates and extends the lifetime of a session.
120 51
   *
121 -
   * Returns `undefined` if the session does not exist or has expired.
52 +
   * If the session no longer exists (i.e. has expired or been destroyed), the session MUST still be saved.
122 53
   *
123 54
   * @abstract
124 -
   * @param {string} sessionID - The ID of the session.
125 -
   * @returns {(Promise<Session|undefined>)} The Session object.
55 +
   * @param {SessionState} state - The state of the session.
56 +
   * @param {number} maxInactivity - The maximum idle activity of the session (useful for cache stores).
57 +
   * @returns {Promise<void>}
126 58
   * @memberof Store
127 59
   */
128 -
  abstract read(sessionID: string): Promise<Session|undefined>;
60 +
  abstract update(state: SessionState, maxInactivity: number): Promise<void>;
129 61
  /**
130 -
   * Extend the lifetime of a session from its ID. The duration is
131 -
   * the inactivity timeout.
62 +
   * Deletes a session.
132 63
   *
133 -
   * If the session does not exist, the method does not throw an error.
64 +
   * If the session does not exist, NO error MUST be thrown.
134 65
   *
135 66
   * @abstract
136 -
   * @param {string} sessionID - The ID of the session.
67 +
   * @param {string} id - The ID of the session.
137 68
   * @returns {Promise<void>}
138 69
   * @memberof Store
139 70
   */
140 -
  abstract extendLifeTime(sessionID: string): Promise<void>;
71 +
  abstract destroy(id: string): Promise<void>;
141 72
  /**
142 -
   * Clear all sessions.
73 +
   * Clears all sessions.
143 74
   *
144 75
   * @abstract
145 76
   * @returns {Promise<void>}
@@ -151,38 +82,17 @@
Loading
151 82
   *
152 83
   * This method deletes all expired sessions.
153 84
   *
85 +
   * If the store manages a cache database, then this method can remain empty but it must NOT throw an error.
86 +
   *
154 87
   * @abstract
88 +
   * @param {number} maxInactivity - The maximum idle activity of a session.
89 +
   * @param {number} maxLifeTime - The maximum absolute life time of a session.
155 90
   * @returns {Promise<void>}
156 91
   * @memberof Store
157 92
   */
158 -
  abstract cleanUpExpiredSessions(): Promise<void>;
159 -
160 -
  /**
161 -
   * Generate a 128-bit base64url-encoded session ID.
162 -
   *
163 -
   * @protected
164 -
   * @returns {Promise<string>} - The session ID.
165 -
   * @memberof Store
166 -
   */
167 -
  protected async generateSessionID(): Promise<string> {
168 -
    return generateToken();
169 -
  }
93 +
  abstract cleanUpExpiredSessions(maxInactivity: number, maxLifeTime: number): Promise<void>;
170 94
171 -
  /**
172 -
   * Apply session options to the given session content.
173 -
   *
174 -
   * @protected
175 -
   * @param {object} content - Session content.
176 -
   * @param {SessionOptions} options - Session options.
177 -
   * @param {boolean} [options.csrfToken] - Generate and add a `csrfToken` to the sessionContent.
178 -
   * @returns {Promise<void>}
179 -
   * @memberof Store
180 -
   */
181 -
  protected async applySessionOptions(content: object, options: SessionOptions): Promise<void> {
182 -
    if (options.csrfToken) {
183 -
      (content as any).csrfToken = await generateToken();
184 -
    }
185 -
  }
95 +
  boot(): void|Promise<void> {}
186 96
}
187 97
188 98
export { Store as SessionStore };

@@ -1,8 +1,8 @@
Loading
1 -
import { Class } from '../../core';
1 +
import { Class } from '../../class.interface';
2 2
3 3
import { IApiComponents } from '../interfaces';
4 4
5 -
import { getMetadata } from '../../core/routes/utils';
5 +
import { getMetadata } from '../../routes/utils';
6 6
import { Dynamic } from '../utils';
7 7
8 8
export function getApiComponents<T>(controllerClass: Class<T>, controller: T, propertyKey?: string): IApiComponents {
9 9
imilarity index 100%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-deprecated.spec.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-deprecated.spec.ts
12 12
imilarity index 69%
13 13
ename from packages/core/src/openapi/metadata-getters/get-api-deprecated.ts
14 14
ename to packages/core/src/core/openapi/metadata-getters/get-api-deprecated.ts

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiTag } from '../interfaces';
4 4
5 5
export function getApiTags(
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-used-tags.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-used-tags.spec.ts
9 9
imilarity index 69%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-used-tags.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-used-tags.ts

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiOperation } from '../interfaces';
4 4
5 5
export function getApiOperation(
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-parameters.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-parameters.spec.ts
9 9
imilarity index 77%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-parameters.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-parameters.ts

@@ -1,3 +1,3 @@
Loading
1 -
export * from './route.interface';
1 +
export * from './get-response';
2 2
export { makeControllerRoutes } from './make-controller-routes';
3 3
export { getPath, getHttpMethod } from './utils';

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
4 4
export function getApiOperationId(
5 5
  controllerClass: Class, propertyKey?: string
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-operation-summary.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-operation-summary.spec.ts
9 9
imilarity index 69%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-operation-summary.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-operation-summary.ts

@@ -9,11 +9,12 @@
Loading
9 9
 * when adding the controller as a sub-controller.
10 10
 *
11 11
 * @export
12 +
 * @template T
12 13
 * @param {string} path - The HTTP path.
13 -
 * @param {Class} controllerClass - The controller class.
14 -
 * @returns {Class} The controller class.
14 +
 * @param {Class<T>} controllerClass - The controller class.
15 +
 * @returns {Class<T>} The controller class.
15 16
 */
16 -
export function controller(path: string, controllerClass: Class): Class {
17 +
export function controller<T>(path: string, controllerClass: Class<T>): Class<T> {
17 18
  Reflect.defineMetadata('path', path, controllerClass);
18 19
  return controllerClass;
19 20
}

@@ -1,5 +1,5 @@
Loading
1 -
import { Class } from '../../core';
2 -
import { getMetadata } from '../../core/routes/utils';
1 +
import { Class } from '../../class.interface';
2 +
import { getMetadata } from '../../routes/utils';
3 3
import { IApiReference, IApiRequestBody } from '../interfaces';
4 4
5 5
export function getApiRequestBody(controllerClass: Class, propertyKey?: string):
6 6
imilarity index 100%
7 7
ename from packages/core/src/openapi/metadata-getters/get-api-responses.spec.ts
8 8
ename to packages/core/src/core/openapi/metadata-getters/get-api-responses.spec.ts
9 9
imilarity index 74%
10 10
ename from packages/core/src/openapi/metadata-getters/get-api-responses.ts
11 11
ename to packages/core/src/core/openapi/metadata-getters/get-api-responses.ts

@@ -1,5 +1,6 @@
Loading
1 -
export * from './metadata-getters';
2 1
export { createOpenApiDocument } from './create-open-api-document';
3 2
export * from './decorators';
4 3
export * from './interfaces';
5 -
export { OpenAPI } from './openapi.service';
4 +
export * from './metadata-getters';
5 +
export * from './utils';
6 +
export { OpenApi } from './openapi.service';
6 7
imilarity index 100%
7 8
ename from packages/core/src/openapi/interfaces.ts
8 9
ename to packages/core/src/core/openapi/interfaces.ts
9 10
imilarity index 100%
10 11
ename from packages/core/src/openapi/metadata-getters/get-api-callbacks.spec.ts
11 12
ename to packages/core/src/core/openapi/metadata-getters/get-api-callbacks.spec.ts
12 13
imilarity index 77%
13 14
ename from packages/core/src/openapi/metadata-getters/get-api-callbacks.ts
14 15
ename to packages/core/src/core/openapi/metadata-getters/get-api-callbacks.ts

@@ -1,6 +1,20 @@
Loading
1 -
export function dotToUnderscore(str: string): string {
2 -
  return str
3 -
    .replace(/([A-Z])/g, letter => `_${letter}`)
4 -
    .replace(/\./g, '_')
5 -
    .toUpperCase();
1 +
function makeLine(str: string): string {
2 +
  const length = 58;
3 +
  const spacesAfter = length - str.length;
4 +
  if (spacesAfter <= 0) {
5 +
    return str;
6 +
  }
7 +
  return '|' + str + ' '.repeat(spacesAfter) + '|\n';
8 +
}
9 +
10 +
export function makeBox(title: string, content: string[]): string {
11 +
  return '  --------------------------------------------------------\n'
12 +
  + '|                                                          |\n'
13 +
  + makeLine('  ' + title)
14 +
  + '|                                                          |\n'
15 +
  + '| -------------------------------------------------------- |\n'