axe-api 0.17.5 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Release Notes
2
2
 
3
+ ## [0.18.0 (2021-11-30)](https://github.com/axe-api/axe-api/compare/0.18.0...0.17.5)
4
+
5
+ ### Fixed
6
+
7
+ - [#115](https://github.com/axe-api/axe-api/issues/115)
8
+ - [#114](https://github.com/axe-api/axe-api/issues/114)
9
+
10
+ ### Enhancements
11
+
12
+ - [#113](https://github.com/axe-api/axe-api/issues/113)
13
+ - [#107](https://github.com/axe-api/axe-api/issues/107)
14
+ - [#108](https://github.com/axe-api/axe-api/issues/108)
15
+
3
16
  ## [0.17.5 (2021-11-27)](https://github.com/axe-api/axe-api/compare/0.17.5...0.17.4)
4
17
 
5
18
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axe-api",
3
- "version": "0.17.5",
3
+ "version": "0.18.0",
4
4
  "description": "AXE API is a simple tool which has been created based on Express and Knex.js to create Rest APIs quickly.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/constants.js CHANGED
@@ -72,31 +72,31 @@ const HTTP_METHODS = {
72
72
 
73
73
  const API_ROUTE_TEMPLATES = {
74
74
  [HANDLERS.INSERT]: {
75
- url: (parentUrl, resource) => `/api/${parentUrl}${resource}`,
75
+ url: (prefix, parentUrl, resource) => `/${prefix}/${parentUrl}${resource}`,
76
76
  method: HTTP_METHODS.POST,
77
77
  },
78
78
  [HANDLERS.PAGINATE]: {
79
- url: (parentUrl, resource) => `/api/${parentUrl}${resource}`,
79
+ url: (prefix, parentUrl, resource) => `/${prefix}/${parentUrl}${resource}`,
80
80
  method: HTTP_METHODS.GET,
81
81
  },
82
82
  [HANDLERS.SHOW]: {
83
- url: (parentUrl, resource, primaryKey) =>
84
- `/api/${parentUrl}${resource}/:${primaryKey}`,
83
+ url: (prefix, parentUrl, resource, primaryKey) =>
84
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
85
85
  method: HTTP_METHODS.GET,
86
86
  },
87
87
  [HANDLERS.UPDATE]: {
88
- url: (parentUrl, resource, primaryKey) =>
89
- `/api/${parentUrl}${resource}/:${primaryKey}`,
88
+ url: (prefix, parentUrl, resource, primaryKey) =>
89
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
90
90
  method: HTTP_METHODS.PUT,
91
91
  },
92
92
  [HANDLERS.AUTOSAVE]: {
93
- url: (parentUrl, resource, primaryKey) =>
94
- `/api/${parentUrl}${resource}/:${primaryKey}/autosave`,
93
+ url: (prefix, parentUrl, resource, primaryKey) =>
94
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}/autosave`,
95
95
  method: HTTP_METHODS.PUT,
96
96
  },
97
97
  [HANDLERS.DELETE]: {
98
- url: (parentUrl, resource, primaryKey) =>
99
- `/api/${parentUrl}${resource}/:${primaryKey}`,
98
+ url: (prefix, parentUrl, resource, primaryKey) =>
99
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
100
100
  method: HTTP_METHODS.DELETE,
101
101
  },
102
102
  };
@@ -1,31 +1,13 @@
1
1
  import HttpResponse from "./../core/HttpResponse.js";
2
2
  import { RELATIONSHIPS } from "../constants.js";
3
3
 
4
- const DEFAULT_OPTIONS = {
5
- min_per_page: 1,
6
- max_per_page: 1000,
7
- };
8
-
9
4
  class QueryParser {
10
- constructor({ model, models, options = {} }) {
5
+ constructor({ model, models }) {
11
6
  this.model = model;
12
7
  this.models = models;
13
8
  this.createdJoins = [];
14
9
  this.relationColumns = [];
15
10
  this.usedConditionColumns = new Set();
16
- this.options = { ...DEFAULT_OPTIONS, ...options };
17
-
18
- if (isNaN(this.options.min_per_page) || this.options.min_per_page < 1) {
19
- throw new Error(
20
- `You set unacceptable query parse option (min_per_page): ${this.options.min_per_page}`
21
- );
22
- }
23
-
24
- if (isNaN(this.options.max_per_page) || this.options.max_per_page > 10000) {
25
- throw new Error(
26
- `You set unacceptable query parse option (max_per_page): ${this.options.max_per_page}`
27
- );
28
- }
29
11
  }
30
12
 
31
13
  applyFields(query, fields) {
@@ -109,10 +91,9 @@ class QueryParser {
109
91
  });
110
92
 
111
93
  if (undefinedColumns.length > 0) {
112
- throw new HttpResponse(
113
- 400,
114
- `Undefined column names: ${undefinedColumns.join(",")}`
115
- );
94
+ throw new HttpResponse(400, {
95
+ message: `Undefined column names: ${undefinedColumns.join(",")}`,
96
+ });
116
97
  }
117
98
 
118
99
  return conditions;
@@ -215,7 +196,9 @@ class QueryParser {
215
196
  try {
216
197
  sections.q = JSON.parse(queryContent);
217
198
  } catch (err) {
218
- throw new Error(`Unacceptable query string: \n ${queryContent}`);
199
+ throw new HttpResponse(400, {
200
+ message: `Unacceptable query string: ${queryContent}`,
201
+ });
219
202
  }
220
203
  }
221
204
 
@@ -247,16 +230,8 @@ class QueryParser {
247
230
  _parsePerPage(content) {
248
231
  const value = parseInt(content);
249
232
 
250
- if (isNaN(value)) {
251
- return this.options.min_per_page;
252
- }
253
-
254
- if (value <= this.options.min_per_page) {
255
- return this.options.min_per_page;
256
- }
257
-
258
- if (value > this.options.max_per_page) {
259
- return this.options.max_per_page;
233
+ if (isNaN(value) || value <= 1 || value > 10000) {
234
+ return 10;
260
235
  }
261
236
 
262
237
  return value;
@@ -325,19 +300,24 @@ class QueryParser {
325
300
  return null;
326
301
  }
327
302
 
303
+ const wheres = [];
304
+ for (const key in content) {
305
+ wheres.push(this._parseConditionObject(content, key));
306
+ }
307
+
308
+ return wheres;
309
+ }
310
+
311
+ _parseConditionObject(content, key) {
328
312
  const where = {
329
313
  prefix: null,
330
314
  model: this.model,
331
315
  table: this.model.instance.table,
332
- field: null,
316
+ field: key,
333
317
  condition: "=",
334
- value: null,
318
+ value: content[key],
335
319
  };
336
320
 
337
- const key = Object.keys(content)[0];
338
- where.field = key;
339
- where.value = content[key];
340
-
341
321
  // Sometimes we can have basic OR operations for queries
342
322
  if (where.field.indexOf("$or.") === 0) {
343
323
  where.prefix = "or";
@@ -349,19 +329,30 @@ class QueryParser {
349
329
  where.field = where.field.replace("$and.", "");
350
330
  }
351
331
 
352
- this._applySpecialCondition(where, "$not", "<>");
353
- this._applySpecialCondition(where, "$gt", ">");
354
- this._applySpecialCondition(where, "$gte", ">=");
355
- this._applySpecialCondition(where, "$lt", "<");
356
- this._applySpecialCondition(where, "$lte", "<=");
357
- this._applySpecialCondition(where, "$like", "LIKE");
358
- this._applySpecialCondition(where, "$notLike", "NOT LIKE");
359
- this._applySpecialCondition(where, "$in", "In");
360
- this._applySpecialCondition(where, "$notIn", "NotIn");
361
- this._applySpecialCondition(where, "$between", "Between");
362
- this._applySpecialCondition(where, "$notBetween", "NotBetween");
363
- this._applySpecialCondition(where, "$null", "Null");
364
- this._applySpecialCondition(where, "$notNull", "NotNull");
332
+ // If there is not any value, it means that we should check nullable values
333
+ if (where.value === null) {
334
+ // If the client wants to see not nullable values
335
+ if (this._hasSpecialStructure(where.field, ".$not")) {
336
+ where.field = where.field.replace(".$not", "");
337
+ where.condition = "NotNull";
338
+ } else {
339
+ // So, it means that the clients wants to see null valus
340
+ where.condition = "Null";
341
+ }
342
+ } else {
343
+ // If there is value, we should check it
344
+ this._applySpecialCondition(where, "$not", "<>");
345
+ this._applySpecialCondition(where, "$gt", ">");
346
+ this._applySpecialCondition(where, "$gte", ">=");
347
+ this._applySpecialCondition(where, "$lt", "<");
348
+ this._applySpecialCondition(where, "$lte", "<=");
349
+ this._applySpecialCondition(where, "$like", "LIKE");
350
+ this._applySpecialCondition(where, "$notLike", "NOT LIKE");
351
+ this._applySpecialCondition(where, "$in", "In");
352
+ this._applySpecialCondition(where, "$notIn", "NotIn");
353
+ this._applySpecialCondition(where, "$between", "Between");
354
+ this._applySpecialCondition(where, "$notBetween", "NotBetween");
355
+ }
365
356
 
366
357
  if (where.condition === "In" || where.condition === "NotIn") {
367
358
  where.value = where.value.split(",");
@@ -371,10 +362,6 @@ class QueryParser {
371
362
  where.value = where.value.split(":");
372
363
  }
373
364
 
374
- if (where.condition === "Null" || where.condition === "NotNull") {
375
- where.value = null;
376
- }
377
-
378
365
  if (where.condition === "LIKE" || where.condition === "NOT LIKE") {
379
366
  where.value = where.value.replace(/\*/g, "%");
380
367
  }
@@ -389,7 +376,9 @@ class QueryParser {
389
376
  );
390
377
 
391
378
  if (!relation) {
392
- throw new Error(`Unacceptable query field: ${relationName}.${field}`);
379
+ throw new HttpResponse(400, {
380
+ message: `Unacceptable query field: ${relationName}.${field}`,
381
+ });
393
382
  }
394
383
 
395
384
  const relatedModel = this.models.find(
@@ -397,7 +386,9 @@ class QueryParser {
397
386
  );
398
387
 
399
388
  if (!relatedModel) {
400
- throw new Error(`Undefined model name: ${relation.model}`);
389
+ throw new HttpResponse(400, {
390
+ message: `Undefined model name: ${relation.model}`,
391
+ });
401
392
  }
402
393
 
403
394
  where.model = relatedModel;
@@ -504,7 +495,9 @@ class QueryParser {
504
495
  (i) => i.name === item.relationship
505
496
  );
506
497
  if (!relation) {
507
- throw new Error(`Undefined relation: ${item.relationship}`);
498
+ throw new HttpResponse(400, {
499
+ message: `Undefined relation: ${item.relationship}`,
500
+ });
508
501
  }
509
502
 
510
503
  this.relationColumns.push(
@@ -535,11 +528,15 @@ class QueryParser {
535
528
  _shouldBeAcceptableColumn(field) {
536
529
  const regex = /^[0-9,a-z,A-Z_.]+$/;
537
530
  if (!field.match(regex)) {
538
- throw new Error(`Unacceptable field name: ${field}`);
531
+ throw new HttpResponse(400, {
532
+ message: `Unacceptable field name: ${field}`,
533
+ });
539
534
  }
540
535
 
541
536
  if (field.indexOf(".") === 0 || field.indexOf(".") === field.length - 1) {
542
- throw new Error(`You have to define the column specefically: ${field}`);
537
+ throw new HttpResponse(400, {
538
+ message: `You have to define the column specefically: ${field}`,
539
+ });
543
540
  }
544
541
  }
545
542
  }
@@ -11,6 +11,7 @@ import {
11
11
  import Validator from "validatorjs";
12
12
  import { HOOK_FUNCTIONS, TIMESTAMP_COLUMNS } from "./../constants.js";
13
13
  import HttpResponse from "./../core/HttpResponse.js";
14
+ import { HANDLERS } from "./../constants.js";
14
15
 
15
16
  export default async (context) => {
16
17
  const { request, response, model, trx, relation, parentModel } = context;
@@ -29,7 +30,9 @@ export default async (context) => {
29
30
  .where(model.instance.primaryKey, request.params[model.instance.primaryKey])
30
31
  .first();
31
32
  if (!item) {
32
- throw new HttpResponse(404, `The item is not found on ${model.name}.`);
33
+ throw new HttpResponse(404, {
34
+ message: `The item is not found on ${model.name}.`,
35
+ });
33
36
  }
34
37
 
35
38
  await callHooks(model, HOOK_FUNCTIONS.onAfterUpdateQuery, {
@@ -78,7 +81,12 @@ export default async (context) => {
78
81
  });
79
82
 
80
83
  // Serializing the data by the model's serialize method
81
- item = await serializeData(item, model.instance.serialize);
84
+ item = await serializeData(
85
+ item,
86
+ model.instance.serialize,
87
+ HANDLERS.AUTOSAVE,
88
+ request
89
+ );
82
90
 
83
91
  // Filtering hidden fields from the response data.
84
92
  filterHiddenFields([item], model.instance.hiddens);
@@ -22,7 +22,9 @@ export default async (context) => {
22
22
 
23
23
  let item = await query.first();
24
24
  if (!item) {
25
- throw new HttpResponse(404, `The item is not found on ${model.name}.`);
25
+ throw new HttpResponse(404, {
26
+ message: `The item is not found on ${model.name}.`,
27
+ });
26
28
  }
27
29
 
28
30
  await callHooks(model, HOOK_FUNCTIONS.onAfterDeleteQuery, {
@@ -1,7 +1,7 @@
1
1
  import { RELATIONSHIPS } from "./../constants.js";
2
2
  import { camelCase } from "change-case";
3
3
  import HttpResponse from "./../core/HttpResponse.js";
4
- import IoC from '../core/IoC.js';
4
+ import IoC from "../core/IoC.js";
5
5
 
6
6
  const getInputFromBody = (body, field) => {
7
7
  if (!body) {
@@ -83,7 +83,8 @@ export const getRelatedData = async (
83
83
  model,
84
84
  models,
85
85
  database,
86
- handler
86
+ handler,
87
+ request
87
88
  ) => {
88
89
  if (withArray.length === 0) {
89
90
  return;
@@ -96,10 +97,9 @@ export const getRelatedData = async (
96
97
  (relation) => relation.name === clientQuery.relationship
97
98
  );
98
99
  if (!definedRelation) {
99
- throw new HttpResponse(
100
- 400,
101
- `Undefined relation: ${clientQuery.relationship}`
102
- );
100
+ throw new HttpResponse(400, {
101
+ message: `Undefined relation: ${clientQuery.relationship}`,
102
+ });
103
103
  }
104
104
 
105
105
  // Find the foreign model by the relationship
@@ -164,10 +164,9 @@ export const getRelatedData = async (
164
164
  (column) => !foreignModel.instance.columnNames.includes(column)
165
165
  );
166
166
  if (undefinedColumns.length > 0) {
167
- throw new HttpResponse(
168
- 400,
169
- `Undefined columns: ${undefinedColumns.join(", ")}`
170
- );
167
+ throw new HttpResponse(400, {
168
+ message: `Undefined columns: ${undefinedColumns.join(", ")}`,
169
+ });
171
170
  }
172
171
  }
173
172
 
@@ -189,7 +188,8 @@ export const getRelatedData = async (
189
188
  relatedRecords = await serializeData(
190
189
  relatedRecords,
191
190
  foreignModel.instance.serialize,
192
- handler
191
+ handler,
192
+ request
193
193
  );
194
194
 
195
195
  // We should hide hidden fields if there is any
@@ -203,7 +203,8 @@ export const getRelatedData = async (
203
203
  foreignModel,
204
204
  models,
205
205
  database,
206
- handler
206
+ handler,
207
+ request
207
208
  );
208
209
  }
209
210
 
@@ -243,19 +244,19 @@ export const bindTimestampValues = (formData, columnTypes = [], model) => {
243
244
  }
244
245
  };
245
246
 
246
- const serialize = async (data, callback) => {
247
+ const serialize = async (data, callback, request) => {
247
248
  if (!callback) {
248
249
  return data;
249
250
  }
250
251
 
251
252
  if (Array.isArray(data)) {
252
- return data.map(callback);
253
+ return data.map((item) => callback(item, request));
253
254
  }
254
255
 
255
- return [data].map(callback)[0];
256
- }
256
+ return callback(data, request);
257
+ };
257
258
 
258
- const globalSerializer = async (itemArray, handler) => {
259
+ const globalSerializer = async (itemArray, handler, request) => {
259
260
  const { Application } = await IoC.use("Config");
260
261
 
261
262
  if (!Application.serializers) {
@@ -263,35 +264,43 @@ const globalSerializer = async (itemArray, handler) => {
263
264
  }
264
265
 
265
266
  let callbacks = [];
266
- // Push all runable serializer into callbacks.
267
- Application.serializers.map(configSerializer => {
267
+ // Push all runable serializer into callbacks.
268
+ Application.serializers.map((configSerializer) => {
268
269
  // Serialize data for all requests types.
269
270
  if (typeof configSerializer === "function") {
270
271
  callbacks.push(configSerializer);
271
- return
272
+ return;
272
273
  }
273
274
 
274
275
  // Serialize data with specific handler like "PAGINATE" or "SHOW".
275
- if (typeof configSerializer === "object" && configSerializer.handler.includes(handler)) {
276
+ if (
277
+ typeof configSerializer === "object" &&
278
+ configSerializer.handler.includes(handler)
279
+ ) {
276
280
  // Handle multiple serializer.
277
281
  if (Array.isArray(configSerializer.serializer)) {
278
- configSerializer.serializer.forEach(fn => callbacks.push(fn));
279
- return
282
+ configSerializer.serializer.forEach((fn) => callbacks.push(fn));
283
+ return;
280
284
  }
281
- callbacks.push(configSerializer.serializer)
282
- return
285
+ callbacks.push(configSerializer.serializer);
286
+ return;
283
287
  }
284
- })
288
+ });
285
289
 
286
290
  while (callbacks.length !== 0) {
287
- itemArray = await serialize(itemArray, callbacks.shift());
291
+ itemArray = serialize(itemArray, callbacks.shift(), request);
288
292
  }
289
293
  return itemArray;
290
294
  };
291
295
 
292
- export const serializeData = async (itemArray, modelSerializer, handler) => {
293
- itemArray = await serialize(itemArray, modelSerializer);
294
- itemArray = await globalSerializer(itemArray, handler);
296
+ export const serializeData = async (
297
+ itemArray,
298
+ modelSerializer,
299
+ handler,
300
+ request
301
+ ) => {
302
+ itemArray = serialize(itemArray, modelSerializer, request);
303
+ itemArray = await globalSerializer(itemArray, handler, request);
295
304
  return itemArray;
296
305
  };
297
306
 
@@ -45,7 +45,15 @@ export default async (context) => {
45
45
  });
46
46
 
47
47
  // We should try to get related data if there is any
48
- await getRelatedData(result.data, conditions.with, model, models, trx, HANDLERS.PAGINATE);
48
+ await getRelatedData(
49
+ result.data,
50
+ conditions.with,
51
+ model,
52
+ models,
53
+ trx,
54
+ HANDLERS.PAGINATE,
55
+ request
56
+ );
49
57
 
50
58
  await callHooks(model, HOOK_FUNCTIONS.onAfterPaginate, {
51
59
  ...context,
@@ -55,7 +63,12 @@ export default async (context) => {
55
63
  });
56
64
 
57
65
  // Serializing the data by the model's serialize method
58
- result.data = await serializeData(result.data, model.instance.serialize, HANDLERS.PAGINATE);
66
+ result.data = await serializeData(
67
+ result.data,
68
+ model.instance.serialize,
69
+ HANDLERS.PAGINATE,
70
+ request
71
+ );
59
72
 
60
73
  // Filtering hidden fields from the response data.
61
74
  filterHiddenFields(result.data, model.instance.hiddens);
@@ -43,11 +43,21 @@ export default async (context) => {
43
43
 
44
44
  let item = await query.first();
45
45
  if (!item) {
46
- throw new HttpResponse(404, `The item is not found on ${model.name}.`);
46
+ throw new HttpResponse(404, {
47
+ message: `The item is not found on ${model.name}.`,
48
+ });
47
49
  }
48
50
 
49
51
  // We should try to get related data if there is any
50
- await getRelatedData([item], conditions.with, model, models, trx, HANDLERS.SHOW);
52
+ await getRelatedData(
53
+ [item],
54
+ conditions.with,
55
+ model,
56
+ models,
57
+ trx,
58
+ HANDLERS.SHOW,
59
+ request
60
+ );
51
61
 
52
62
  await callHooks(model, HOOK_FUNCTIONS.onAfterShow, {
53
63
  ...context,
@@ -57,7 +67,12 @@ export default async (context) => {
57
67
  });
58
68
 
59
69
  // Serializing the data by the model's serialize method
60
- item = await serializeData(item, model.instance.serialize, HANDLERS.SHOW);
70
+ item = await serializeData(
71
+ item,
72
+ model.instance.serialize,
73
+ HANDLERS.SHOW,
74
+ request
75
+ );
61
76
 
62
77
  // Filtering hidden fields from the response data.
63
78
  filterHiddenFields([item], model.instance.hiddens);
@@ -63,7 +63,12 @@ export default async (context) => {
63
63
  });
64
64
 
65
65
  // Serializing the data by the model's serialize method
66
- item = await serializeData(item, model.instance.serialize, HANDLERS.INSERT);
66
+ item = await serializeData(
67
+ item,
68
+ model.instance.serialize,
69
+ HANDLERS.INSERT,
70
+ request
71
+ );
67
72
 
68
73
  // Filtering hidden fields from the response data.
69
74
  filterHiddenFields([item], model.instance.hiddens);
@@ -28,7 +28,9 @@ export default async (context) => {
28
28
  .where(model.instance.primaryKey, request.params[model.instance.primaryKey])
29
29
  .first();
30
30
  if (!item) {
31
- throw new HttpResponse(404, `The item is not found on ${model.name}.`);
31
+ throw new HttpResponse(404, {
32
+ message: `The item is not found on ${model.name}.`,
33
+ });
32
34
  }
33
35
 
34
36
  await callHooks(model, HOOK_FUNCTIONS.onAfterUpdateQuery, {
@@ -76,7 +78,12 @@ export default async (context) => {
76
78
  });
77
79
 
78
80
  // Serializing the data by the model's serialize method
79
- item = await serializeData(item, model.instance.serialize, HANDLERS.UPDATE);
81
+ item = await serializeData(
82
+ item,
83
+ model.instance.serialize,
84
+ HANDLERS.UPDATE,
85
+ request
86
+ );
80
87
 
81
88
  // Filtering hidden fields from the response data.
82
89
  filterHiddenFields([item], model.instance.hiddens);
@@ -54,6 +54,10 @@ const requestHandler = async (handler, req, res, next, context) => {
54
54
  await context.trx.rollback();
55
55
  }
56
56
 
57
+ if (error.type === "HttpResponse") {
58
+ return res.status(error.status).json(error.content);
59
+ }
60
+
57
61
  next(error);
58
62
  }
59
63
  };
@@ -160,6 +164,13 @@ const getModelMiddlewares = (model, handler) => {
160
164
  return middlewares;
161
165
  };
162
166
 
167
+ const getRootPrefix = () => {
168
+ if (Config.Application.prefix) {
169
+ return Config.Application.prefix.replace(/^\/|\/$/g, "");
170
+ }
171
+ return "api";
172
+ };
173
+
163
174
  const createRouteByModel = async (
164
175
  model,
165
176
  models,
@@ -197,6 +208,7 @@ const createRouteByModel = async (
197
208
 
198
209
  const routeTemplate = API_ROUTE_TEMPLATES[handler];
199
210
  const url = routeTemplate.url(
211
+ getRootPrefix(),
200
212
  urlPrefix,
201
213
  resource,
202
214
  model.instance.primaryKey