axe-api 1.0.0-rc21 → 1.0.0-rc23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,7 +59,8 @@ class ModelTreeBuilder {
59
59
  this.version.modelList.get().forEach((model) => {
60
60
  const recursiveRelations = model.relations.filter((relation) => relation.model === model.name);
61
61
  if (recursiveRelations.length === 2) {
62
- tree.push(Object.assign(Object.assign({}, model), { isRecursive: true, children: [] }));
62
+ model.setAsRecursive();
63
+ tree.push(model);
63
64
  }
64
65
  });
65
66
  }
@@ -171,3 +171,7 @@ export declare enum StatusCodes {
171
171
  USE_PROXY = 305,
172
172
  MISDIRECTED_REQUEST = 421
173
173
  }
174
+ export declare enum CacheStrategies {
175
+ TimeBased = "time-based",
176
+ TagBased = "tag-based"
177
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StatusCodes = exports.QueryFeature = exports.QueryFeatureType = exports.AxeErrorCode = exports.TimestampColumns = exports.SortTypes = exports.Relationships = exports.HttpMethods = exports.Extensions = exports.HookFunctionTypes = exports.HandlerTypes = exports.DependencyTypes = exports.ConditionTypes = void 0;
3
+ exports.CacheStrategies = exports.StatusCodes = exports.QueryFeature = exports.QueryFeatureType = exports.AxeErrorCode = exports.TimestampColumns = exports.SortTypes = exports.Relationships = exports.HttpMethods = exports.Extensions = exports.HookFunctionTypes = exports.HandlerTypes = exports.DependencyTypes = exports.ConditionTypes = void 0;
4
4
  var ConditionTypes;
5
5
  (function (ConditionTypes) {
6
6
  ConditionTypes["NotNull"] = "NotNull";
@@ -187,3 +187,8 @@ var StatusCodes;
187
187
  StatusCodes[StatusCodes["USE_PROXY"] = 305] = "USE_PROXY";
188
188
  StatusCodes[StatusCodes["MISDIRECTED_REQUEST"] = 421] = "MISDIRECTED_REQUEST";
189
189
  })(StatusCodes || (exports.StatusCodes = StatusCodes = {}));
190
+ var CacheStrategies;
191
+ (function (CacheStrategies) {
192
+ CacheStrategies["TimeBased"] = "time-based";
193
+ CacheStrategies["TagBased"] = "tag-based";
194
+ })(CacheStrategies || (exports.CacheStrategies = CacheStrategies = {}));
@@ -1,9 +1,9 @@
1
- import { IModelService, IRelation, IQuery, IVersion, IWith, IContext } from "../Interfaces";
1
+ import { IModelService, IRelation, IQuery, IVersion, IWith, IContext, ICacheConfiguration } from "../Interfaces";
2
2
  import { Knex } from "knex";
3
3
  import { HandlerTypes, HookFunctionTypes, TimestampColumns } from "../Enums";
4
4
  import { ModelListService } from "../Services";
5
5
  import { SerializationFunction } from "../Types";
6
- import AxeRequest from "src/Services/AxeRequest";
6
+ import AxeRequest from "../Services/AxeRequest";
7
7
  export declare const bindTimestampValues: (formData: Record<string, any>, model: IModelService, columnTypes?: TimestampColumns[]) => void;
8
8
  export declare const getMergedFormData: (req: AxeRequest, fillables: string[]) => Record<string, any>;
9
9
  export declare const callHooks: (model: IModelService, type: HookFunctionTypes, params: IContext) => Promise<void>;
@@ -15,3 +15,9 @@ export declare const filterHiddenFields: (itemArray: any[], hiddens: string[] |
15
15
  export declare const addSoftDeleteQuery: (model: IModelService, conditions: IQuery | null, query: Knex.QueryBuilder) => void;
16
16
  export declare const getRelatedData: (version: IVersion, data: any[], withArray: IWith[], model: IModelService, modelList: ModelListService, database: Knex | Knex.Transaction, handler: HandlerTypes, request: AxeRequest) => Promise<void>;
17
17
  export declare const isBoolean: (value: any) => boolean;
18
+ export declare const getModelCacheConfiguration: (model: IModelService, apiConfig: ICacheConfiguration, versionConfig: ICacheConfiguration | null, handler: string) => ICacheConfiguration;
19
+ export declare const defaultCacheKeyFunction: (req: AxeRequest) => string;
20
+ export declare const toCacheKey: (context: IContext) => string;
21
+ export declare const putCache: (context: IContext, data: any) => Promise<void>;
22
+ export declare const clearCacheTags: (tag: string) => Promise<void>;
23
+ export declare const toCachePrefix: (value: string | undefined | null) => string;
@@ -12,7 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.isBoolean = exports.getRelatedData = exports.addSoftDeleteQuery = exports.filterHiddenFields = exports.serializeData = exports.addForeignKeyQuery = exports.checkPrimaryKeyValueType = exports.getParentColumn = exports.callHooks = exports.getMergedFormData = exports.bindTimestampValues = void 0;
15
+ exports.toCachePrefix = exports.clearCacheTags = exports.putCache = exports.toCacheKey = exports.defaultCacheKeyFunction = exports.getModelCacheConfiguration = exports.isBoolean = exports.getRelatedData = exports.addSoftDeleteQuery = exports.filterHiddenFields = exports.serializeData = exports.addForeignKeyQuery = exports.checkPrimaryKeyValueType = exports.getParentColumn = exports.callHooks = exports.getMergedFormData = exports.bindTimestampValues = void 0;
16
+ const crypto_1 = __importDefault(require("crypto"));
16
17
  const change_case_1 = require("change-case");
17
18
  const Enums_1 = require("../Enums");
18
19
  const ApiError_1 = __importDefault(require("../Exceptions/ApiError"));
@@ -221,6 +222,13 @@ const getRelatedData = (version, data, withArray, model, modelList, database, ha
221
222
  }
222
223
  // Fetching related records by foreignKey and primary key values.
223
224
  let relatedRecords = yield foreignModelQuery.whereIn(searchFieldKey, parentPrimaryKeyValues);
225
+ // Adding related data source to the request tags to set cache tag values
226
+ const { primaryKey } = foreignModel.instance;
227
+ const cacheConfig = foreignModel.getCacheConfiguration(handler);
228
+ const tagPrefix = (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.tagPrefix)
229
+ ? `${cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.tagPrefix}:`
230
+ : "";
231
+ request.original.tags.push(...relatedRecords.map((i) => `${tagPrefix}${foreignModel.name}:${i[primaryKey]}`));
224
232
  // We should serialize related data if there is any serialization function
225
233
  relatedRecords = yield (0, exports.serializeData)(version, relatedRecords, foreignModel.serialize, handler, request);
226
234
  // We should hide hidden fields if there is any
@@ -251,3 +259,85 @@ const isBoolean = (value) => {
251
259
  return false;
252
260
  };
253
261
  exports.isBoolean = isBoolean;
262
+ const getModelCacheConfiguration = (model, apiConfig, versionConfig, handler) => {
263
+ let base = Object.assign(Object.assign({}, constants_1.DEFAULT_CACHE_CONFIGURATION), apiConfig);
264
+ if (model.instance.cache) {
265
+ const data = model.instance.cache;
266
+ if (Array.isArray(data)) {
267
+ const handlerBasedConfigs = data;
268
+ for (const item of handlerBasedConfigs) {
269
+ const isFound = item.handlers.map((i) => i).includes(handler);
270
+ if (isFound) {
271
+ base = Object.assign(Object.assign({}, base), item.cache);
272
+ return base;
273
+ }
274
+ }
275
+ }
276
+ else {
277
+ base = Object.assign(Object.assign({}, base), data);
278
+ return base;
279
+ }
280
+ }
281
+ if (versionConfig) {
282
+ base = Object.assign(Object.assign({}, base), versionConfig);
283
+ }
284
+ return base;
285
+ };
286
+ exports.getModelCacheConfiguration = getModelCacheConfiguration;
287
+ const defaultCacheKeyFunction = (req) => {
288
+ return JSON.stringify({
289
+ url: req.url,
290
+ method: req.method,
291
+ headers: req.original.headers,
292
+ });
293
+ };
294
+ exports.defaultCacheKeyFunction = defaultCacheKeyFunction;
295
+ const toCacheKey = (context) => {
296
+ const { model, handlerType } = context;
297
+ const config = model.getCacheConfiguration(handlerType);
298
+ const keyData = (config === null || config === void 0 ? void 0 : config.cacheKey)
299
+ ? config.cacheKey(context.req)
300
+ : (0, exports.defaultCacheKeyFunction)(context.req);
301
+ const key = crypto_1.default.createHash("sha256").update(keyData).digest("hex");
302
+ return (0, exports.toCachePrefix)(config === null || config === void 0 ? void 0 : config.cachePrefix) + key;
303
+ };
304
+ exports.toCacheKey = toCacheKey;
305
+ const putCache = (context, data) => __awaiter(void 0, void 0, void 0, function* () {
306
+ // Getting the correct configuration
307
+ const { model, handlerType } = context;
308
+ const config = model.getCacheConfiguration(handlerType);
309
+ // Check if the cache enable for this handler
310
+ if (config === null || config === void 0 ? void 0 : config.enable) {
311
+ // Getting the redis service
312
+ const redis = yield Services_1.IoCService.use("Redis");
313
+ // Generating the cache key
314
+ const key = (0, exports.toCacheKey)(context);
315
+ console.log("HERE", key);
316
+ // Setting the tags if the cache configuration of the model has been set as
317
+ // tag-based cache invalidation strategy. Which means, the key cached value
318
+ // can be deleted if the tagged items updated/delete
319
+ if (config.invalidation === Enums_1.CacheStrategies.TagBased) {
320
+ redis.tags(context.req.original.tags, key);
321
+ }
322
+ // Putting the cache data
323
+ redis.set(key, JSON.stringify(data), config.ttl || 1000);
324
+ if (config.responseHeader) {
325
+ context.res.header(config.responseHeader, "Missed");
326
+ }
327
+ // Logging
328
+ Services_1.LogService.debug(`\t🔄 redis.cache(${key},${config.ttl})`);
329
+ }
330
+ });
331
+ exports.putCache = putCache;
332
+ const clearCacheTags = (tag) => __awaiter(void 0, void 0, void 0, function* () {
333
+ const redis = yield Services_1.IoCService.use("Redis");
334
+ const members = yield redis.getTagMemebers(tag);
335
+ if (members.length > 0) {
336
+ yield redis.delete(members);
337
+ }
338
+ });
339
+ exports.clearCacheTags = clearCacheTags;
340
+ const toCachePrefix = (value) => {
341
+ return value ? `${value}:` : "";
342
+ };
343
+ exports.toCachePrefix = toCachePrefix;
@@ -15,13 +15,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  const Services_1 = require("../Services");
16
16
  const URLService_1 = __importDefault(require("../Services/URLService"));
17
17
  const ConverterService_1 = require("../Services/ConverterService");
18
- const api = Services_1.APIService.getInstance();
19
18
  const return404 = (response) => {
20
19
  response.statusCode = 404;
21
20
  response.write(JSON.stringify({ error: "Resource not found" }));
22
21
  response.end();
23
22
  };
24
23
  exports.default = (request, response, next) => __awaiter(void 0, void 0, void 0, function* () {
24
+ const api = Services_1.APIService.getInstance();
25
25
  Services_1.LogService.debug(`📥 ${request.method} ${request.url}`);
26
26
  const { axeRequest, axeResponse } = (0, ConverterService_1.toAxeRequestResponsePair)(request, response);
27
27
  const match = URLService_1.default.match(axeRequest);
@@ -35,7 +35,7 @@ exports.default = (request, response, next) => __awaiter(void 0, void 0, void 0,
35
35
  // Prepare the database by the transaction option
36
36
  let trx = null;
37
37
  if (match.hasTransaction) {
38
- Services_1.LogService.warn("\tDB transaction created");
38
+ Services_1.LogService.warn("\t🛢 DBTransaction:created()");
39
39
  trx = yield database.transaction();
40
40
  }
41
41
  const context = Object.assign(Object.assign({}, match.data), { params: match.params, api, req: axeRequest, res: axeResponse, isTransactionOpen: match.hasTransaction, database: match.hasTransaction && trx ? trx : database });
@@ -57,7 +57,7 @@ exports.default = (request, response, next) => __awaiter(void 0, void 0, void 0,
57
57
  Services_1.LogService.error(error);
58
58
  // Rollback transaction
59
59
  if (match.hasTransaction && trx) {
60
- Services_1.LogService.warn("\tDB transaction rollback");
60
+ Services_1.LogService.warn("\t🛢 DBTransaction:rollback()");
61
61
  trx.rollback();
62
62
  }
63
63
  if (error.type === "ApiError") {
@@ -77,12 +77,17 @@ exports.default = (request, response, next) => __awaiter(void 0, void 0, void 0,
77
77
  // we should rollback it before the HTTP request end.
78
78
  if (context.res.statusCode() >= 400 && context.res.statusCode() < 599) {
79
79
  if (match.hasTransaction && trx) {
80
- Services_1.LogService.warn("\tDB transaction rollback");
80
+ Services_1.LogService.warn("\t🛢 DBTransaction:rollback()");
81
81
  trx.rollback();
82
82
  }
83
83
  Services_1.LogService.debug(`\tResponse ${context.res.statusCode()}`);
84
84
  break;
85
85
  }
86
+ // We should commit the transaction if there is any
87
+ if (match.hasTransaction && trx) {
88
+ Services_1.LogService.warn("\t🛢 DBTransaction:commit()");
89
+ trx.commit();
90
+ }
86
91
  Services_1.LogService.debug(`\t🟢 Response ${context.res.statusCode()}`);
87
92
  // We should brake the for-loop
88
93
  break;
@@ -3,7 +3,7 @@
3
3
  import { Knex } from "knex";
4
4
  import { Options as FormOptions } from "formidable";
5
5
  import { Column } from "knex-schema-inspector/lib/types/column";
6
- import { HandlerTypes, HttpMethods, HookFunctionTypes, Extensions, Relationships, SortTypes, ConditionTypes, DependencyTypes, QueryFeature, QueryFeatureType } from "./Enums";
6
+ import { HandlerTypes, HttpMethods, HookFunctionTypes, Extensions, Relationships, SortTypes, ConditionTypes, DependencyTypes, QueryFeature, QueryFeatureType, CacheStrategies } from "./Enums";
7
7
  import Model from "./Model";
8
8
  import { AdaptorType, AxeFunction, GeneralFunction, HandlerFunction, ModelHooks, PhaseFunction, SerializationFunction } from "./Types";
9
9
  import { ModelListService, QueryService } from "./Services";
@@ -13,6 +13,7 @@ import App from "./Services/App";
13
13
  import { LoggerOptions } from "pino";
14
14
  import { IncomingMessage } from "http";
15
15
  import { ErrorHandleFunction } from "connect";
16
+ import { RedisClientOptions } from "redis";
16
17
  export interface IColumn extends Column {
17
18
  table_name: string;
18
19
  }
@@ -22,6 +23,10 @@ export interface IHandlerBasedTransactionConfig {
22
23
  handlers: HandlerTypes[];
23
24
  transaction: boolean;
24
25
  }
26
+ export interface IHandlerBasedCacheConfig {
27
+ handlers: HandlerTypes[];
28
+ cache: ICacheConfiguration;
29
+ }
25
30
  interface IHandlerBasedSerializer {
26
31
  handler: HandlerTypes[];
27
32
  serializer: ((data: any, request: AxeRequest) => void)[];
@@ -40,23 +45,13 @@ export interface IQueryConfig {
40
45
  limits: Array<IQueryLimitConfig[]>;
41
46
  defaults?: IQueryDefaultConfig;
42
47
  }
43
- export interface IRedisOptions {
44
- host?: string;
45
- port?: number;
46
- password?: string;
47
- db?: number;
48
- }
49
- export interface IRateLimitAdaptorConfig {
50
- type: AdaptorType;
51
- redis?: IRedisOptions;
52
- }
53
48
  export interface IRateLimitOptions {
54
49
  maxRequests: number;
55
50
  windowInSeconds: number;
56
51
  }
57
52
  export interface IRateLimitConfig extends IRateLimitOptions {
58
53
  enabled: boolean;
59
- adaptor: IRateLimitAdaptorConfig;
54
+ adaptor: AdaptorType;
60
55
  trustProxyIP: boolean;
61
56
  keyGenerator?: (req: IncomingMessage) => string;
62
57
  }
@@ -67,8 +62,18 @@ export interface AxeVersionConfig {
67
62
  defaultLanguage: string;
68
63
  query: IQueryConfig;
69
64
  formidable: FormOptions;
65
+ cache: ICacheConfiguration | null;
70
66
  }
71
67
  export type IVersionConfig = Partial<AxeVersionConfig>;
68
+ export interface ICacheConfiguration {
69
+ enable?: boolean;
70
+ ttl?: number;
71
+ invalidation?: CacheStrategies;
72
+ cachePrefix?: string;
73
+ tagPrefix?: string;
74
+ responseHeader?: string | null;
75
+ cacheKey?: (req: AxeRequest) => string;
76
+ }
72
77
  export interface AxeConfig extends IConfig {
73
78
  env: string;
74
79
  port: number;
@@ -78,6 +83,8 @@ export interface AxeConfig extends IConfig {
78
83
  rateLimit: IRateLimitConfig;
79
84
  errorHandler: ErrorHandleFunction;
80
85
  docs: boolean;
86
+ redis: RedisClientOptions | undefined;
87
+ cache: ICacheConfiguration;
81
88
  }
82
89
  export type IApplicationConfig = Partial<AxeConfig>;
83
90
  export interface ILanguage {
@@ -140,6 +147,9 @@ export interface IModelService {
140
147
  setExtensions(type: Extensions, hookFunctionType: HookFunctionTypes, data: PhaseFunction): void;
141
148
  setQueryLimits(limits: IQueryLimitConfig[]): void;
142
149
  setSerialization(callback: SerializationFunction): void;
150
+ setCacheConfiguration(handler: string, cache: ICacheConfiguration): void;
151
+ getCacheConfiguration(handler: HandlerTypes): ICacheConfiguration | null;
152
+ setAsRecursive(): void;
143
153
  }
144
154
  export interface IRelation {
145
155
  type: Relationships;
@@ -1,6 +1,6 @@
1
1
  import { AdaptorType } from "../../Types";
2
2
  import RedisAdaptor from "./RedisAdaptor";
3
3
  import MemoryAdaptor from "./MemoryAdaptor";
4
- import { IRedisOptions } from "src/Interfaces";
5
- declare const _default: (adaptor: AdaptorType, redisOptions: IRedisOptions | undefined, prefix: string) => RedisAdaptor | MemoryAdaptor;
4
+ import { RedisClientOptions } from "redis";
5
+ declare const _default: (adaptor: AdaptorType, redisOptions: RedisClientOptions | undefined, prefix: string) => RedisAdaptor | MemoryAdaptor;
6
6
  export default _default;
@@ -1,10 +1,15 @@
1
- import { ICacheAdaptor, IRedisOptions } from "../../Interfaces";
1
+ import { createClient } from "redis";
2
+ import { ICacheAdaptor } from "../../Interfaces";
3
+ type RedisClientOptions = Parameters<typeof createClient>[0];
2
4
  declare class RedisAdaptor implements ICacheAdaptor {
3
5
  private client;
4
6
  private prefix;
5
- constructor(options: IRedisOptions | undefined, prefix: string);
7
+ constructor(options: RedisClientOptions | undefined, prefix: string);
6
8
  get(key: string): Promise<string | null>;
7
9
  set(key: string, value: string, ttl: number): Promise<void>;
10
+ tags(keys: string[], value: string): Promise<void>;
11
+ getTagMemebers(tag: string): Promise<string[]>;
12
+ delete(keys: string[]): Promise<number>;
8
13
  decr(key: string): Promise<void>;
9
14
  }
10
15
  export default RedisAdaptor;
@@ -26,6 +26,23 @@ class RedisAdaptor {
26
26
  yield this.client.setEx(`${this.prefix}${key}`, ttl, value);
27
27
  });
28
28
  }
29
+ tags(keys, value) {
30
+ return __awaiter(this, void 0, void 0, function* () {
31
+ keys.forEach((key) => {
32
+ this.client.sAdd(key, [value]);
33
+ });
34
+ });
35
+ }
36
+ getTagMemebers(tag) {
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ return yield this.client.sMembers(tag);
39
+ });
40
+ }
41
+ delete(keys) {
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ return yield this.client.del(keys);
44
+ });
45
+ }
29
46
  decr(key) {
30
47
  return __awaiter(this, void 0, void 0, function* () {
31
48
  yield this.client.decr(`${this.prefix}${key}`);
@@ -65,9 +65,9 @@ const getClientKeyByConfigurations = (req, config) => {
65
65
  return `axe-api-rate-limit:${req.socket.remoteAddress || ""}`;
66
66
  };
67
67
  const setupRateLimitAdaptors = (config) => {
68
- var _a, _b;
68
+ var _a;
69
69
  // Creating the correct adaptor by the configuration
70
- adaptor = (0, AdaptorFactory_1.default)(((_a = config.rateLimit) === null || _a === void 0 ? void 0 : _a.adaptor.type) || "memory", (_b = config.rateLimit) === null || _b === void 0 ? void 0 : _b.adaptor.redis, "");
70
+ adaptor = (0, AdaptorFactory_1.default)(((_a = config.rateLimit) === null || _a === void 0 ? void 0 : _a.adaptor) || "memory", config.redis, "");
71
71
  };
72
72
  exports.setupRateLimitAdaptors = setupRateLimitAdaptors;
73
73
  /**
@@ -1,4 +1,4 @@
1
- import { IRelation, IMethodBaseConfig, IQueryLimitConfig, IHandlerBasedTransactionConfig } from "./Interfaces";
1
+ import { IRelation, IMethodBaseConfig, IQueryLimitConfig, IHandlerBasedTransactionConfig, ICacheConfiguration, IHandlerBasedCacheConfig } from "./Interfaces";
2
2
  import { HandlerTypes, HttpMethods } from "./Enums";
3
3
  import { ModelMiddleware, AxeFunction, ModelValidation } from "./Types";
4
4
  declare class Model {
@@ -187,6 +187,7 @@ declare class Model {
187
187
  * @tutorial https://axe-api.com/reference/model-limits.html
188
188
  */
189
189
  get limits(): Array<IQueryLimitConfig[]>;
190
+ get cache(): ICacheConfiguration | IHandlerBasedCacheConfig[] | null;
190
191
  /**
191
192
  * Model relationship definition. Axe API creates `hasMany` routes automatically.
192
193
  *
@@ -219,6 +219,9 @@ class Model {
219
219
  get limits() {
220
220
  return [];
221
221
  }
222
+ get cache() {
223
+ return null;
224
+ }
222
225
  /**
223
226
  * Model relationship definition. Axe API creates `hasMany` routes automatically.
224
227
  *
@@ -0,0 +1,3 @@
1
+ import { IContext } from "../Interfaces";
2
+ declare const _default: (context: IContext) => Promise<void>;
3
+ export default _default;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const Helpers_1 = require("../Handlers/Helpers");
13
+ exports.default = (context) => __awaiter(void 0, void 0, void 0, function* () {
14
+ const { item, model, handlerType } = context;
15
+ const { primaryKey } = model.instance;
16
+ const config = model.getCacheConfiguration(handlerType);
17
+ const prefix = (0, Helpers_1.toCachePrefix)(config === null || config === void 0 ? void 0 : config.tagPrefix);
18
+ (0, Helpers_1.clearCacheTags)(`${prefix}${model.name}:${item[primaryKey]}`);
19
+ });
@@ -9,13 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- const Services_1 = require("../../Services");
13
12
  exports.default = (context) => __awaiter(void 0, void 0, void 0, function* () {
14
- const { database, isTransactionOpen, res } = context;
15
- // If there is a valid transaction, we should commit it
16
- if (isTransactionOpen) {
17
- Services_1.LogService.warn("\tDB transaction commit");
18
- database.commit();
19
- }
13
+ const { res } = context;
20
14
  res.noContent();
21
15
  });
@@ -0,0 +1,3 @@
1
+ import { IContext } from "../Interfaces";
2
+ declare const _default: (context: IContext) => Promise<void>;
3
+ export default _default;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const Services_1 = require("../Services");
13
+ const Helpers_1 = require("../Handlers/Helpers");
14
+ exports.default = (context) => __awaiter(void 0, void 0, void 0, function* () {
15
+ // Getting the correct configuration
16
+ const { model, handlerType } = context;
17
+ const config = model.getCacheConfiguration(handlerType);
18
+ // Check if the cache enable for this handler
19
+ if (config === null || config === void 0 ? void 0 : config.enable) {
20
+ // Getting the redis service
21
+ const redis = yield Services_1.IoCService.use("Redis");
22
+ // Generating the cache key
23
+ const key = (0, Helpers_1.toCacheKey)(context);
24
+ // Try to fetch the value via Redis
25
+ const value = yield redis.get(key);
26
+ // Check if there is a value
27
+ if (value) {
28
+ // Parse and respond the value
29
+ const result = JSON.parse(value);
30
+ const { res } = context;
31
+ if (config.responseHeader) {
32
+ res.header(config.responseHeader, "Hit");
33
+ }
34
+ res.json(result);
35
+ // Logging
36
+ Services_1.LogService.debug(`\t🔄 redis.get(${key})`);
37
+ }
38
+ }
39
+ });
@@ -9,13 +9,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- const Services_1 = require("../../Services");
12
+ const Helpers_1 = require("../../Handlers/Helpers");
13
13
  exports.default = (context) => __awaiter(void 0, void 0, void 0, function* () {
14
- const { database, isTransactionOpen, result, res } = context;
15
- // If there is a valid transaction, we should commit it
16
- if (isTransactionOpen) {
17
- Services_1.LogService.warn("\tDB transaction commit");
18
- database.commit();
19
- }
14
+ const { result, res, model, handlerType } = context;
15
+ // Adding cache tags
16
+ const { primaryKey } = model.instance;
17
+ const config = model.getCacheConfiguration(handlerType);
18
+ const tagPrefix = (0, Helpers_1.toCachePrefix)(config === null || config === void 0 ? void 0 : config.tagPrefix);
19
+ context.req.original.tags.push(...result.data.map((i) => `${tagPrefix}${model.name}:${i[primaryKey]}`));
20
+ // Caching the results
21
+ yield (0, Helpers_1.putCache)(context, result);
20
22
  res.json(result);
21
23
  });
@@ -9,13 +9,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- const Services_1 = require("../../Services");
12
+ const Helpers_1 = require("../../Handlers/Helpers");
13
13
  exports.default = (context) => __awaiter(void 0, void 0, void 0, function* () {
14
- const { database, isTransactionOpen, item, res } = context;
15
- // If there is a valid transaction, we should commit it
16
- if (isTransactionOpen) {
17
- Services_1.LogService.warn("\tDB transaction commit");
18
- database.commit();
19
- }
14
+ const { item, res, model, handlerType } = context;
15
+ // Adding cache tags
16
+ const { primaryKey } = model.instance;
17
+ const config = model.getCacheConfiguration(handlerType);
18
+ const tagPrefix = (0, Helpers_1.toCachePrefix)(config === null || config === void 0 ? void 0 : config.tagPrefix);
19
+ context.req.original.tags.push(`${tagPrefix}${model.name}:${item[primaryKey]}`);
20
+ // Caching the results
21
+ yield (0, Helpers_1.putCache)(context, item);
20
22
  res.json(item);
21
23
  });
@@ -3,6 +3,7 @@ declare class ModelResolver {
3
3
  private version;
4
4
  constructor(version: IVersion);
5
5
  resolve(): Promise<void>;
6
+ private setCacheOptions;
6
7
  private setModelRelations;
7
8
  private getModelList;
8
9
  private setDatabaseColumns;
@@ -20,6 +20,7 @@ const Enums_1 = require("../Enums");
20
20
  const Services_1 = require("../Services");
21
21
  const constants_1 = require("../constants");
22
22
  const AxeError_1 = __importDefault(require("../Exceptions/AxeError"));
23
+ const Helpers_1 = require("../Handlers/Helpers");
23
24
  class ModelResolver {
24
25
  constructor(version) {
25
26
  this.version = version;
@@ -41,9 +42,27 @@ class ModelResolver {
41
42
  yield this.setModelQueryLimits(modelList);
42
43
  Services_1.LogService.debug(`[${this.version.name}] Model query limits have been loaded`);
43
44
  this.version.modelList = modelList;
45
+ yield this.setCacheOptions(modelList);
44
46
  Services_1.LogService.debug(`[${this.version.name}] All models have been resolved.`);
45
47
  });
46
48
  }
49
+ setCacheOptions(modelList) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ const api = Services_1.APIService.getInstance();
52
+ // For each model should be analyzed
53
+ for (const model of modelList.get()) {
54
+ // For each cachable handler, developers are able to set a different
55
+ // configuration. That's why we should check for each of them.
56
+ for (const handler of constants_1.ALL_HANDLERS) {
57
+ // API configuration, version configuration and the handler type are in
58
+ // order. The following function gets the correct configuration
59
+ const configuration = (0, Helpers_1.getModelCacheConfiguration)(model, api.config.cache, this.version.config.cache, handler);
60
+ // We need to set this to use late in action
61
+ model.setCacheConfiguration(handler, configuration);
62
+ }
63
+ }
64
+ });
65
+ }
47
66
  setModelRelations(modelList) {
48
67
  return __awaiter(this, void 0, void 0, function* () {
49
68
  for (const model of modelList.get()) {
@@ -50,6 +50,7 @@ const http_1 = __importDefault(require("http"));
50
50
  const RequestHandler_1 = __importDefault(require("./Handlers/RequestHandler"));
51
51
  const App_1 = __importDefault(require("./Services/App"));
52
52
  const constants_1 = require("./constants");
53
+ const RedisAdaptor_1 = __importDefault(require("./Middlewares/RateLimit/RedisAdaptor"));
53
54
  class Server {
54
55
  /**
55
56
  * Start the application with the rootFolder.
@@ -83,11 +84,16 @@ class Server {
83
84
  Services_1.IoCService.singleton("App", () => new App_1.default());
84
85
  Services_1.IoCService.singleton("Database", () => __awaiter(this, void 0, void 0, function* () {
85
86
  const database = (0, knex_1.default)(api.config.database);
86
- Services_1.LogService.debug("Created a knex connection instance");
87
+ const { client } = api.config.database;
88
+ const { database: db, filename } = api.config.database.connection;
89
+ Services_1.LogService.debug(`Created a knex connection instance: [${client}:${db || filename}]`);
87
90
  (0, knex_paginate_1.attachPaginate)();
88
91
  Services_1.LogService.debug("Added pagination support to the knex");
89
92
  return database;
90
93
  }));
94
+ Services_1.IoCService.singleton("Redis", () => {
95
+ return new RedisAdaptor_1.default(api.config.redis, "");
96
+ });
91
97
  });
92
98
  }
93
99
  analyzeVersions() {
@@ -52,6 +52,7 @@ class APIService {
52
52
  limits: [],
53
53
  },
54
54
  formidable: {},
55
+ cache: null,
55
56
  },
56
57
  folders: {
57
58
  root,
@@ -20,6 +20,7 @@ class AxeRequest {
20
20
  constructor(request) {
21
21
  this.privateParams = {};
22
22
  this.request = request;
23
+ this.request.tags = [];
23
24
  this.urlObject = new URL(request.url || "", "http://127.0.0.1");
24
25
  // Application configuration is need for the default setting.
25
26
  this.version = (0, Helpers_1.getVersionByRequest)(this.urlObject);
@@ -1,5 +1,5 @@
1
- import { HookFunctionTypes, Extensions } from "../Enums";
2
- import { IColumn, IModelService, IQueryLimitConfig, IRelation } from "../Interfaces";
1
+ import { HookFunctionTypes, Extensions, HandlerTypes } from "../Enums";
2
+ import { ICacheConfiguration, IColumn, IModelService, IQueryLimitConfig, IRelation } from "../Interfaces";
3
3
  import Model from "./../Model";
4
4
  import { ModelHooks, PhaseFunction, SerializationFunction } from "../Types";
5
5
  declare class ModelService implements IModelService {
@@ -14,11 +14,15 @@ declare class ModelService implements IModelService {
14
14
  isRecursive: boolean;
15
15
  queryLimits: IQueryLimitConfig[];
16
16
  serialize: SerializationFunction | null;
17
+ cacheConfiguration: Record<string, ICacheConfiguration>;
17
18
  constructor(name: string, instance: Model);
18
19
  setColumns(columns: IColumn[]): void;
20
+ setCacheConfiguration(handler: string, cache: ICacheConfiguration): void;
21
+ getCacheConfiguration(handler: HandlerTypes): ICacheConfiguration;
19
22
  setExtensions(type: Extensions, hookFunctionType: HookFunctionTypes, data: PhaseFunction): void;
20
23
  setQueryLimits(limits: IQueryLimitConfig[]): void;
21
24
  setSerialization(callback: SerializationFunction): void;
25
+ setAsRecursive(): void;
22
26
  private setHooks;
23
27
  private setEvents;
24
28
  }
@@ -14,11 +14,18 @@ class ModelService {
14
14
  this.isRecursive = false;
15
15
  this.queryLimits = [];
16
16
  this.serialize = null;
17
+ this.cacheConfiguration = {};
17
18
  }
18
19
  setColumns(columns) {
19
20
  this.columns = columns;
20
21
  this.columnNames = this.columns.map((i) => i.name);
21
22
  }
23
+ setCacheConfiguration(handler, cache) {
24
+ this.cacheConfiguration[handler] = cache;
25
+ }
26
+ getCacheConfiguration(handler) {
27
+ return this.cacheConfiguration[handler];
28
+ }
22
29
  setExtensions(type, hookFunctionType, data) {
23
30
  if (type == Enums_1.Extensions.Hooks) {
24
31
  this.setHooks(hookFunctionType, data);
@@ -36,6 +43,10 @@ class ModelService {
36
43
  setSerialization(callback) {
37
44
  this.serialize = callback;
38
45
  }
46
+ setAsRecursive() {
47
+ this.isRecursive = true;
48
+ this.children = [];
49
+ }
39
50
  setHooks(hookFunctionType, data) {
40
51
  this.hooks[hookFunctionType] = data;
41
52
  }
@@ -1,5 +1,5 @@
1
1
  import { ConditionTypes, HandlerTypes, HttpMethods, QueryFeature, Relationships } from "./Enums";
2
- import { AxeConfig, AxeVersionConfig, IStepDefinition } from "./Interfaces";
2
+ import { AxeConfig, AxeVersionConfig, ICacheConfiguration, IStepDefinition } from "./Interfaces";
3
3
  export declare const RESERVED_KEYWORDS: string[];
4
4
  export declare const DEFAULT_HANDLERS: HandlerTypes[];
5
5
  export declare const DEFAULT_METHODS_OF_MODELS: string[];
@@ -19,5 +19,7 @@ export declare const NUMERIC_PRIMARY_KEY_TYPES: string[];
19
19
  export declare const STRING_COLUMN_TYPES: string[];
20
20
  export declare const HANDLER_METHOD_MAP: Record<HandlerTypes, HttpMethods>;
21
21
  export declare const HANDLER_CYLES: Record<HandlerTypes, IStepDefinition[]>;
22
+ export declare const DEFAULT_CACHE_CONFIGURATION: ICacheConfiguration;
22
23
  export declare const DEFAULT_APP_CONFIG: AxeConfig;
23
24
  export declare const DEFAULT_VERSION_CONFIG: AxeVersionConfig;
25
+ export declare const ALL_HANDLERS: HandlerTypes[];
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_VERSION_CONFIG = exports.DEFAULT_APP_CONFIG = exports.HANDLER_CYLES = exports.HANDLER_METHOD_MAP = exports.STRING_COLUMN_TYPES = exports.NUMERIC_PRIMARY_KEY_TYPES = exports.RelationQueryFeatureMap = exports.ConditionQueryFeatureMap = exports.API_ROUTE_TEMPLATES = exports.DEFAULT_METHODS_OF_MODELS = exports.DEFAULT_HANDLERS = exports.RESERVED_KEYWORDS = void 0;
6
+ exports.ALL_HANDLERS = exports.DEFAULT_VERSION_CONFIG = exports.DEFAULT_APP_CONFIG = exports.DEFAULT_CACHE_CONFIGURATION = exports.HANDLER_CYLES = exports.HANDLER_METHOD_MAP = exports.STRING_COLUMN_TYPES = exports.NUMERIC_PRIMARY_KEY_TYPES = exports.RelationQueryFeatureMap = exports.ConditionQueryFeatureMap = exports.API_ROUTE_TEMPLATES = exports.DEFAULT_METHODS_OF_MODELS = exports.DEFAULT_HANDLERS = exports.RESERVED_KEYWORDS = void 0;
7
7
  const Enums_1 = require("./Enums");
8
8
  const LimitService_1 = require("./Services/LimitService");
9
9
  const Single_1 = __importDefault(require("./Phases/Single"));
@@ -20,6 +20,9 @@ const Phase_1 = __importDefault(require("./Steps/Phase"));
20
20
  const Hook_1 = __importDefault(require("./Steps/Hook"));
21
21
  const Event_1 = __importDefault(require("./Steps/Event"));
22
22
  const ErrorHandler_1 = __importDefault(require("./Handlers/ErrorHandler"));
23
+ const GetCachePhase_1 = __importDefault(require("./Phases/GetCachePhase"));
24
+ const CacheTagCleanPhase_1 = __importDefault(require("./Phases/CacheTagCleanPhase"));
25
+ const Helpers_1 = require("./Handlers/Helpers");
23
26
  exports.RESERVED_KEYWORDS = [
24
27
  "force",
25
28
  "model",
@@ -71,6 +74,7 @@ exports.DEFAULT_METHODS_OF_MODELS = [
71
74
  "limits",
72
75
  "getFillableFields",
73
76
  "getValidationRules",
77
+ "cache",
74
78
  ];
75
79
  exports.API_ROUTE_TEMPLATES = {
76
80
  [Enums_1.HandlerTypes.INSERT]: (prefix, parentUrl, resource) => `/${prefix}/${parentUrl}${resource}`,
@@ -135,6 +139,7 @@ exports.HANDLER_CYLES = {
135
139
  new Phase_1.default("insert.response", Single_1.default.ResultPhase),
136
140
  ],
137
141
  [Enums_1.HandlerTypes.PAGINATE]: [
142
+ new Phase_1.default("paginate.cache", GetCachePhase_1.default),
138
143
  new Phase_1.default("paginate.prepareQuery", Paginate_1.default.PreparePhase),
139
144
  new Hook_1.default(Enums_1.HookFunctionTypes.onBeforePaginate),
140
145
  new Event_1.default(Enums_1.HookFunctionTypes.onBeforePaginate),
@@ -146,6 +151,7 @@ exports.HANDLER_CYLES = {
146
151
  new Phase_1.default("paginate.response", List_1.default.ResultPhase),
147
152
  ],
148
153
  [Enums_1.HandlerTypes.SHOW]: [
154
+ new Phase_1.default("paginate.cache", GetCachePhase_1.default),
149
155
  new Phase_1.default("show.prepareQuery", Show_1.default.PreparePhase),
150
156
  new Hook_1.default(Enums_1.HookFunctionTypes.onBeforeShow),
151
157
  new Event_1.default(Enums_1.HookFunctionTypes.onBeforeShow),
@@ -169,6 +175,7 @@ exports.HANDLER_CYLES = {
169
175
  new Phase_1.default("update.action", Update_1.default.ActionPhase),
170
176
  new Hook_1.default(Enums_1.HookFunctionTypes.onAfterUpdate),
171
177
  new Event_1.default(Enums_1.HookFunctionTypes.onAfterUpdate),
178
+ new Phase_1.default("cache.cleanTags", CacheTagCleanPhase_1.default),
172
179
  new Phase_1.default("update.serialize", Single_1.default.SerializePhase),
173
180
  new Phase_1.default("update.response", Single_1.default.ResultPhase),
174
181
  ],
@@ -184,6 +191,7 @@ exports.HANDLER_CYLES = {
184
191
  new Phase_1.default("delete.action", Delete_1.default.ActionPhase),
185
192
  new Hook_1.default(Enums_1.HookFunctionTypes.onAfterDelete),
186
193
  new Event_1.default(Enums_1.HookFunctionTypes.onAfterDelete),
194
+ new Phase_1.default("cache.cleanTags", CacheTagCleanPhase_1.default),
187
195
  new Phase_1.default("delete.response", Delete_1.default.ResponsePhase),
188
196
  ],
189
197
  [Enums_1.HandlerTypes.FORCE_DELETE]: [
@@ -198,6 +206,7 @@ exports.HANDLER_CYLES = {
198
206
  new Phase_1.default("force-delete.action", ForceDelete_1.default.ActionPhase),
199
207
  new Hook_1.default(Enums_1.HookFunctionTypes.onAfterForceDelete),
200
208
  new Event_1.default(Enums_1.HookFunctionTypes.onAfterForceDelete),
209
+ new Phase_1.default("cache.cleanTags", CacheTagCleanPhase_1.default),
201
210
  new Phase_1.default("force-delete.response", Delete_1.default.ResponsePhase),
202
211
  ],
203
212
  [Enums_1.HandlerTypes.PATCH]: [
@@ -213,10 +222,12 @@ exports.HANDLER_CYLES = {
213
222
  new Phase_1.default("patch.action", Update_1.default.ActionPhase),
214
223
  new Hook_1.default(Enums_1.HookFunctionTypes.onAfterUpdate),
215
224
  new Event_1.default(Enums_1.HookFunctionTypes.onAfterUpdate),
225
+ new Phase_1.default("cache.cleanTags", CacheTagCleanPhase_1.default),
216
226
  new Phase_1.default("patch.serialize", Single_1.default.SerializePhase),
217
227
  new Phase_1.default("patch.response", Single_1.default.ResultPhase),
218
228
  ],
219
229
  [Enums_1.HandlerTypes.ALL]: [
230
+ new Phase_1.default("paginate.cache", GetCachePhase_1.default),
220
231
  new Phase_1.default("all.prepareQuery", Paginate_1.default.PreparePhase),
221
232
  new Hook_1.default(Enums_1.HookFunctionTypes.onBeforePaginate),
222
233
  new Event_1.default(Enums_1.HookFunctionTypes.onBeforePaginate),
@@ -228,6 +239,15 @@ exports.HANDLER_CYLES = {
228
239
  new Phase_1.default("all.response", List_1.default.ResultPhase),
229
240
  ],
230
241
  };
242
+ exports.DEFAULT_CACHE_CONFIGURATION = {
243
+ enable: false,
244
+ ttl: 100,
245
+ invalidation: Enums_1.CacheStrategies.TimeBased,
246
+ tagPrefix: "tag",
247
+ cachePrefix: "axe-cache",
248
+ responseHeader: "X-Axe-API-Cache",
249
+ cacheKey: Helpers_1.defaultCacheKeyFunction,
250
+ };
231
251
  exports.DEFAULT_APP_CONFIG = {
232
252
  prefix: "api",
233
253
  env: "production",
@@ -241,9 +261,7 @@ exports.DEFAULT_APP_CONFIG = {
241
261
  },
242
262
  rateLimit: {
243
263
  enabled: false,
244
- adaptor: {
245
- type: "memory",
246
- },
264
+ adaptor: "memory",
247
265
  maxRequests: 200,
248
266
  windowInSeconds: 5,
249
267
  trustProxyIP: false,
@@ -255,6 +273,8 @@ exports.DEFAULT_APP_CONFIG = {
255
273
  },
256
274
  },
257
275
  errorHandler: ErrorHandler_1.default,
276
+ redis: {},
277
+ cache: Object.assign({}, exports.DEFAULT_CACHE_CONFIGURATION),
258
278
  };
259
279
  exports.DEFAULT_VERSION_CONFIG = {
260
280
  transaction: false,
@@ -276,4 +296,15 @@ exports.DEFAULT_VERSION_CONFIG = {
276
296
  },
277
297
  },
278
298
  formidable: {},
299
+ cache: null,
279
300
  };
301
+ exports.ALL_HANDLERS = [
302
+ Enums_1.HandlerTypes.ALL,
303
+ Enums_1.HandlerTypes.DELETE,
304
+ Enums_1.HandlerTypes.FORCE_DELETE,
305
+ Enums_1.HandlerTypes.INSERT,
306
+ Enums_1.HandlerTypes.PAGINATE,
307
+ Enums_1.HandlerTypes.PATCH,
308
+ Enums_1.HandlerTypes.SHOW,
309
+ Enums_1.HandlerTypes.UPDATE,
310
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axe-api",
3
- "version": "1.0.0-rc21",
3
+ "version": "1.0.0-rc23",
4
4
  "description": "AXE API is a simple tool to create Rest APIs quickly.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -64,8 +64,8 @@
64
64
  "knex-paginate": "^3.1.1",
65
65
  "knex-schema-inspector": "^3.0.1",
66
66
  "nanoid": "^3.3.6",
67
- "pino": "^8.15.0",
68
- "pino-pretty": "^10.2.0",
67
+ "pino": "^8.16.1",
68
+ "pino-pretty": "^10.2.3",
69
69
  "pluralize": "^8.0.0",
70
70
  "validatorjs": "^3.22.1"
71
71
  },
@@ -107,6 +107,7 @@
107
107
  "set-value": ">=4.1.0",
108
108
  "sqlite3": "^5.1.6",
109
109
  "ts-node": "^10.9.1",
110
+ "ts-node-dev": "^2.0.0",
110
111
  "typescript": "^5.2.2"
111
112
  },
112
113
  "lint-staged": {