axe-api 0.17.5 → 0.19.1

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.
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- node-version: [14.x, 16.x]
14
+ node-version: [14.x, 16.x, 17.x]
15
15
  database: [mysql8, mysql57, "postgres"]
16
16
  steps:
17
17
  - uses: actions/checkout@v2
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- node-version: [14.x, 16.x]
14
+ node-version: [14.x, 16.x, 17.x]
15
15
  steps:
16
16
  - uses: actions/checkout@v2
17
17
  - name: Use Node.js ${{ matrix.node-version }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Release Notes
2
2
 
3
+ ## [0.19.1 (2022-01-22)](https://github.com/axe-api/axe-api/compare/0.19.1...0.19.0)
4
+
5
+ ### Fixed
6
+
7
+ - knex.js version update.
8
+
9
+ ## [0.19.0 (2021-12-05)](https://github.com/axe-api/axe-api/compare/0.19.0...0.18.1)
10
+
11
+ ### Fixed
12
+
13
+ - [#110](https://github.com/axe-api/axe-api/issues/110)
14
+
15
+ ### Enhancements
16
+
17
+ - [#106](https://github.com/axe-api/axe-api/issues/106)
18
+
19
+ ## [0.18.1 (2021-12-02)](https://github.com/axe-api/axe-api/compare/0.18.1...0.18.0)
20
+
21
+ ### Fixed
22
+
23
+ - [#117](https://github.com/axe-api/axe-api/issues/117)
24
+
25
+ ## [0.18.0 (2021-11-30)](https://github.com/axe-api/axe-api/compare/0.18.0...0.17.5)
26
+
27
+ ### Fixed
28
+
29
+ - [#115](https://github.com/axe-api/axe-api/issues/115)
30
+ - [#114](https://github.com/axe-api/axe-api/issues/114)
31
+
32
+ ### Enhancements
33
+
34
+ - [#113](https://github.com/axe-api/axe-api/issues/113)
35
+ - [#107](https://github.com/axe-api/axe-api/issues/107)
36
+ - [#108](https://github.com/axe-api/axe-api/issues/108)
37
+
3
38
  ## [0.17.5 (2021-11-27)](https://github.com/axe-api/axe-api/compare/0.17.5...0.17.4)
4
39
 
5
40
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "axe-api",
3
- "version": "0.17.5",
3
+ "version": "0.19.1",
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",
@@ -20,37 +20,37 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "change-case": "^4.1.2",
23
- "dotenv": "^10.0.0",
24
- "express": "^4.17.1",
25
- "knex": "^0.95.12",
23
+ "dotenv": "^14.2.0",
24
+ "express": "^4.17.2",
25
+ "knex": "^1.0.1",
26
26
  "knex-paginate": "^3.0.0",
27
- "knex-schema-inspector": "^1.6.4",
27
+ "knex-schema-inspector": "^1.7.1",
28
28
  "pluralize": "^8.0.0",
29
29
  "validatorjs": "^3.22.1"
30
30
  },
31
31
  "devDependencies": {
32
- "@babel/cli": "^7.14.8",
33
- "@babel/core": "^7.14.8",
34
- "@babel/node": "^7.14.7",
35
- "@babel/preset-env": "^7.14.8",
36
- "@babel/runtime": "^7.14.8",
37
- "babel-jest": "^27.0.6",
38
- "babel-loader": "^8.2.2",
32
+ "@babel/cli": "^7.16.8",
33
+ "@babel/core": "^7.16.10",
34
+ "@babel/node": "^7.16.8",
35
+ "@babel/preset-env": "^7.16.11",
36
+ "@babel/runtime": "^7.16.7",
37
+ "babel-jest": "^27.4.6",
38
+ "babel-loader": "^8.2.3",
39
39
  "babel-plugin-module-resolver": "^4.1.0",
40
40
  "babel-preset-minify": "^0.5.1",
41
41
  "eslint": "^7.31.0",
42
42
  "eslint-config-standard": "^16.0.3",
43
- "eslint-plugin-import": "^2.23.4",
43
+ "eslint-plugin-import": "^2.25.4",
44
44
  "eslint-plugin-node": "^11.1.0",
45
45
  "eslint-plugin-promise": "^5.1.0",
46
46
  "eslint-plugin-standard": "^5.0.0",
47
47
  "eslint-plugin-unicorn": "^33.0.1",
48
48
  "eslint-watch": "^7.0.0",
49
- "jest": "^27.0.6",
49
+ "jest": "^27.4.7",
50
50
  "mysql": "^2.18.1",
51
- "nodemon": "^2.0.12",
52
- "pg": "^8.6.0",
53
- "set-value": ">=4.0.0",
51
+ "nodemon": "^2.0.15",
52
+ "pg": "^8.7.1",
53
+ "set-value": ">=4.1.0",
54
54
  "sqlite3": "^5.0.2"
55
55
  }
56
56
  }
package/readme.md CHANGED
@@ -23,21 +23,98 @@
23
23
  </a>
24
24
  </h1>
25
25
 
26
- > This project is under development and not ready for production.
26
+ The fastest way to create Rest API, by defining database models and relations.
27
27
 
28
- Fastest way to create simple Rest API by defining database models and relations.
28
+ > Axe API has great documentation. Please [check it out in here](https://axe-api.github.io/).
29
29
 
30
- ## Key Features
30
+ ## What Is Axe API?
31
31
 
32
- - Automatic route creating
33
- - Automatic route handling
34
- - Form validation support
35
- - Middlewares
36
- - Strong query features
37
- - Recursive resources
38
- - The extendable business logic structure
39
- - Multiple database support (Postgres, MSSQL, MySQL, MariaDB, SQLite3, Oracle, and Amazon Redshift)
40
- - Well documented
32
+ **Axe API** is the _fastest_ way to create **Rest API** by defining only database models and relationships between them. It is built on [Knex.js](http://knexjs.org), and its awesome active records pattern. On the other hand, you have another familiar thing, [Express](https://expressjs.com/).
33
+
34
+ You are going to be able to develop an API **10 times faster** with **Axe API**!
35
+
36
+ ## How It Works?
37
+
38
+ [Express](https://expressjs.com/) and [Knex.js](http://knexjs.org) are great tools to create [Node.js](https://nodejs.org) based applications. But usually, we code too much the same things to design an API. We aim to reduce code duplication and give you speed by using Axe API.
39
+
40
+ Axe API provides you the ability to separate your common tasks to build an API from your business logic. **Axe API** expects model definitions to analyze your routing structure. After you created your models and their relations between them, Axe API can handle all _well-known_ API requests. Creating an API with 5 tables takes almost 15 minutes.
41
+
42
+ Shortly, **Axe API** performs three basic functions;
43
+
44
+ - **Analyzes** your models and their relationships to create routes.
45
+ - **Handles** all HTTP requests.
46
+ - **Separate** your business logic from API best practices.
47
+
48
+ Let's assume that you have a model like this;
49
+
50
+ ```js
51
+ import { Model } from "axe-api";
52
+
53
+ class User extends Model {}
54
+ ```
55
+
56
+ With this model, you will have all of the basic API routes for **User** resources. **Axe API** will create **CRUD** routes for you in the _booting_ process and these routes would be completely ready to be handled and processed by Axe API. The following routes will be handled automatically;
57
+
58
+ - `POST api/users`
59
+ - `GET api/users`
60
+ - `GET api/users/:id`
61
+ - `PUT api/users/:id`
62
+ - `DELETE api/users/:id`
63
+
64
+ This is the magic of **Axe API**!
65
+
66
+ ## Installation
67
+
68
+ Using **Axe API** in an application is very easy. We've created a CLI tool for you; [axe-magic](https://github.com/axe-api/axe-magic).
69
+
70
+ You can create a new Axe API project by using [axe-magic](https://github.com/axe-api/axe-magic). But first, you can install it in your development environment. When you installed it, you can be able to access **axe-magic** command via CLI. You can use the following command to install **axe-magic** to your machine;
71
+
72
+ ```bash
73
+ $ npm i -g axe-magic
74
+ $ axe-magic --version
75
+ 1.0.0
76
+ ```
77
+
78
+ After that, creating a new project is very easy. Just you can execute the following command;
79
+
80
+ ```bash
81
+ $ axe-magic new my-api
82
+ ```
83
+
84
+ This command will pull [axe-api-template](https://github.com/axe-api/axe-api-template) project to your current directory with a new name, **my-api**.
85
+
86
+ To install your project's depencies, you can execute the following commands in the root directory;
87
+
88
+ ```bash
89
+ $ cd my-api
90
+ $ npm install
91
+ ```
92
+
93
+ To serve this application, you can execute the following command;
94
+
95
+ ```bash
96
+ $ npm run start:dev
97
+ ```
98
+
99
+ > `start:dev` command use [nodemon](https://www.npmjs.com/package/nodemon). If you haven't installed it yet, we suggest you install it first.
100
+
101
+ After that, your first **Axe API** application will be running in `localhost:3000`.
102
+
103
+ You will see the following API response if you visit [localhost:3000](http://localhost:3000).
104
+
105
+ ```json
106
+ {
107
+ "name": "AXE API",
108
+ "description": "The best API creation tool in the world.",
109
+ "aim": "To kill them all!"
110
+ }
111
+ ```
112
+
113
+ If you can see that response, it means that your project is running properly.
114
+
115
+ ## Documentation
116
+
117
+ Axe API has great documentation. Please [check it out in here](https://axe-api.github.io/).
41
118
 
42
119
  ## How To Run Integration Tests
43
120
 
package/src/constants.js CHANGED
@@ -13,6 +13,7 @@ const HOOK_FUNCTIONS = {
13
13
  onBeforeDeleteQuery: "onBeforeDeleteQuery",
14
14
  onBeforeDelete: "onBeforeDelete",
15
15
  onBeforePaginate: "onBeforePaginate",
16
+ onBeforeAll: "onBeforeAll",
16
17
  onBeforeShow: "onBeforeShow",
17
18
  onAfterInsert: "onAfterInsert",
18
19
  onAfterUpdateQuery: "onAfterUpdateQuery",
@@ -20,6 +21,7 @@ const HOOK_FUNCTIONS = {
20
21
  onAfterDeleteQuery: "onAfterDeleteQuery",
21
22
  onAfterDelete: "onAfterDelete",
22
23
  onAfterPaginate: "onAfterPaginate",
24
+ onAfterAll: "onAfterAll",
23
25
  onAfterShow: "onAfterShow",
24
26
  };
25
27
 
@@ -52,7 +54,8 @@ const HANDLERS = {
52
54
  SHOW: "show",
53
55
  UPDATE: "update",
54
56
  DELETE: "destroy",
55
- AUTOSAVE: "autosave",
57
+ PATCH: "patch",
58
+ ALL: "all",
56
59
  };
57
60
 
58
61
  const DEFAULT_HANDLERS = [
@@ -60,6 +63,7 @@ const DEFAULT_HANDLERS = [
60
63
  HANDLERS.PAGINATE,
61
64
  HANDLERS.SHOW,
62
65
  HANDLERS.UPDATE,
66
+ HANDLERS.PATCH,
63
67
  HANDLERS.DELETE,
64
68
  ];
65
69
 
@@ -68,35 +72,41 @@ const HTTP_METHODS = {
68
72
  GET: "GET",
69
73
  PUT: "PUT",
70
74
  DELETE: "DELETE",
75
+ PATCH: "PATCH",
71
76
  };
72
77
 
73
78
  const API_ROUTE_TEMPLATES = {
74
79
  [HANDLERS.INSERT]: {
75
- url: (parentUrl, resource) => `/api/${parentUrl}${resource}`,
80
+ url: (prefix, parentUrl, resource) => `/${prefix}/${parentUrl}${resource}`,
76
81
  method: HTTP_METHODS.POST,
77
82
  },
78
83
  [HANDLERS.PAGINATE]: {
79
- url: (parentUrl, resource) => `/api/${parentUrl}${resource}`,
84
+ url: (prefix, parentUrl, resource) => `/${prefix}/${parentUrl}${resource}`,
85
+ method: HTTP_METHODS.GET,
86
+ },
87
+ [HANDLERS.ALL]: {
88
+ url: (prefix, parentUrl, resource) =>
89
+ `/${prefix}/${parentUrl}${resource}/all`,
80
90
  method: HTTP_METHODS.GET,
81
91
  },
82
92
  [HANDLERS.SHOW]: {
83
- url: (parentUrl, resource, primaryKey) =>
84
- `/api/${parentUrl}${resource}/:${primaryKey}`,
93
+ url: (prefix, parentUrl, resource, primaryKey) =>
94
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
85
95
  method: HTTP_METHODS.GET,
86
96
  },
87
97
  [HANDLERS.UPDATE]: {
88
- url: (parentUrl, resource, primaryKey) =>
89
- `/api/${parentUrl}${resource}/:${primaryKey}`,
98
+ url: (prefix, parentUrl, resource, primaryKey) =>
99
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
90
100
  method: HTTP_METHODS.PUT,
91
101
  },
92
- [HANDLERS.AUTOSAVE]: {
93
- url: (parentUrl, resource, primaryKey) =>
94
- `/api/${parentUrl}${resource}/:${primaryKey}/autosave`,
95
- method: HTTP_METHODS.PUT,
102
+ [HANDLERS.PATCH]: {
103
+ url: (prefix, parentUrl, resource, primaryKey) =>
104
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
105
+ method: HTTP_METHODS.PATCH,
96
106
  },
97
107
  [HANDLERS.DELETE]: {
98
- url: (parentUrl, resource, primaryKey) =>
99
- `/api/${parentUrl}${resource}/:${primaryKey}`,
108
+ url: (prefix, parentUrl, resource, primaryKey) =>
109
+ `/${prefix}/${parentUrl}${resource}/:${primaryKey}`,
100
110
  method: HTTP_METHODS.DELETE,
101
111
  },
102
112
  };
package/src/core/Model.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import pluralize from "pluralize";
2
2
  import { snakeCase } from "snake-case";
3
- import { RELATIONSHIPS, HANDLERS } from "./../constants.js";
4
- const { INSERT, SHOW, UPDATE, PAGINATE, DELETE } = HANDLERS;
3
+ import { RELATIONSHIPS, DEFAULT_HANDLERS } from "./../constants.js";
5
4
 
6
5
  class Model {
7
6
  constructor() {
@@ -25,7 +24,7 @@ class Model {
25
24
  }
26
25
 
27
26
  get handlers() {
28
- return [INSERT, SHOW, PAGINATE, UPDATE, DELETE];
27
+ return [...DEFAULT_HANDLERS];
29
28
  }
30
29
 
31
30
  get middlewares() {
@@ -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
  }
@@ -0,0 +1,73 @@
1
+ import {
2
+ callHooks,
3
+ getRelatedData,
4
+ filterHiddenFields,
5
+ serializeData,
6
+ addForeignKeyQuery,
7
+ } from "./helpers.js";
8
+ import { HOOK_FUNCTIONS, HANDLERS } from "./../constants.js";
9
+ import QueryParser from "./../core/QueryParser.js";
10
+
11
+ export default async (context) => {
12
+ const { request, response, model, models, trx, relation, parentModel } =
13
+ context;
14
+
15
+ const queryParser = new QueryParser({ model, models });
16
+
17
+ // We should parse URL query string to use as condition in Lucid query
18
+ const conditions = queryParser.get(request.query);
19
+
20
+ // Creating a new database query
21
+ const query = trx.from(model.instance.table);
22
+
23
+ // Users should be able to select some fields to show.
24
+ queryParser.applyFields(query, conditions.fields);
25
+
26
+ // Binding parent id if there is.
27
+ addForeignKeyQuery(request, query, relation, parentModel);
28
+
29
+ // Users should be able to filter records
30
+ queryParser.applyWheres(query, conditions.q);
31
+
32
+ await callHooks(model, HOOK_FUNCTIONS.onBeforeAll, {
33
+ ...context,
34
+ conditions,
35
+ query,
36
+ });
37
+
38
+ // User should be able to select sorting fields and types
39
+ queryParser.applySorting(query, conditions.sort);
40
+
41
+ const result = await query;
42
+
43
+ // We should try to get related data if there is any
44
+ await getRelatedData(
45
+ result.data,
46
+ conditions.with,
47
+ model,
48
+ models,
49
+ trx,
50
+ HANDLERS.ALL,
51
+ request
52
+ );
53
+
54
+ await callHooks(model, HOOK_FUNCTIONS.onAfterAll, {
55
+ ...context,
56
+ result,
57
+ conditions,
58
+ query,
59
+ });
60
+
61
+ // Serializing the data by the model's serialize method
62
+ result.data = await serializeData(
63
+ result.data,
64
+ model.instance.serialize,
65
+ HANDLERS.ALL,
66
+ request
67
+ );
68
+
69
+ // Filtering hidden fields from the response data.
70
+ filterHiddenFields(result.data, model.instance.hiddens);
71
+
72
+ return response.json(result);
73
+ };
@@ -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
 
@@ -1,8 +1,9 @@
1
- import autosave from "./autosave.js";
1
+ import all from "./all.js";
2
+ import patch from "./patch.js";
2
3
  import store from "./store.js";
3
4
  import show from "./show.js";
4
5
  import paginate from "./paginate.js";
5
6
  import update from "./update.js";
6
7
  import destroy from "./destroy.js";
7
8
 
8
- export default { autosave, store, show, paginate, update, destroy };
9
+ export default { all, patch, store, show, paginate, update, destroy };
@@ -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);
@@ -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.PATCH,
88
+ request
89
+ );
82
90
 
83
91
  // Filtering hidden fields from the response data.
84
92
  filterHiddenFields([item], 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);
@@ -43,9 +43,14 @@ export default async (context) => {
43
43
  formData,
44
44
  });
45
45
 
46
- let [insertedPrimaryKeyValue] = await trx(model.instance.table)
46
+ const [returningResult] = await trx(model.instance.table)
47
47
  .insert(formData)
48
- .returning("id");
48
+ .returning(model.instance.primaryKey);
49
+
50
+ let insertedPrimaryKeyValue =
51
+ typeof returningResult === "number"
52
+ ? returningResult
53
+ : returningResult[model.instance.primaryKey];
49
54
 
50
55
  // If the user use a special primary key value, we should use that value
51
56
  if (insertedPrimaryKeyValue === 0) {
@@ -63,7 +68,12 @@ export default async (context) => {
63
68
  });
64
69
 
65
70
  // Serializing the data by the model's serialize method
66
- item = await serializeData(item, model.instance.serialize, HANDLERS.INSERT);
71
+ item = await serializeData(
72
+ item,
73
+ model.instance.serialize,
74
+ HANDLERS.INSERT,
75
+ request
76
+ );
67
77
 
68
78
  // Filtering hidden fields from the response data.
69
79
  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);
@@ -1,6 +1,6 @@
1
1
  import IoC from "./../core/IoC.js";
2
2
  import pluralize from "pluralize";
3
- import { paramCase } from "change-case";
3
+ import { paramCase, camelCase } from "change-case";
4
4
  import { RELATIONSHIPS, API_ROUTE_TEMPLATES } from "./../constants.js";
5
5
  import Handlers from "./../handlers/index.js";
6
6
 
@@ -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
  };
@@ -118,7 +122,7 @@ const createNestedRoutes = async (
118
122
  await createRouteByModel(
119
123
  model,
120
124
  models,
121
- `${urlPrefix}${resource}/:${getPrimaryKeyName(model)}/`,
125
+ `${urlPrefix}${resource}/:${camelCase(relation.foreignKey)}/`,
122
126
  model,
123
127
  relation,
124
128
  false
@@ -160,6 +164,21 @@ const getModelMiddlewares = (model, handler) => {
160
164
  return middlewares;
161
165
  };
162
166
 
167
+ export const getRootPrefix = async () => {
168
+ const config = await IoC.use("Config");
169
+ let prefix = config?.Application?.prefix || "api";
170
+
171
+ if (prefix.substr(0, 1) === "/") {
172
+ prefix = prefix.substr(1);
173
+ }
174
+
175
+ if (prefix.substr(prefix.length - 1) === "/") {
176
+ prefix = prefix.substr(0, prefix.length - 1);
177
+ }
178
+
179
+ return prefix;
180
+ };
181
+
163
182
  const createRouteByModel = async (
164
183
  model,
165
184
  models,
@@ -197,6 +216,7 @@ const createRouteByModel = async (
197
216
 
198
217
  const routeTemplate = API_ROUTE_TEMPLATES[handler];
199
218
  const url = routeTemplate.url(
219
+ await getRootPrefix(),
200
220
  urlPrefix,
201
221
  resource,
202
222
  model.instance.primaryKey