axe-api / axe-api Goto Github PK
View Code? Open in Web Editor NEWThe next-generation Rest API Framework
Home Page: https://axe-api.com
License: MIT License
The next-generation Rest API Framework
Home Page: https://axe-api.com
License: MIT License
For now, we can use the following question;
/api/students?q=[ {"name": "John"} ]
In this example, we can add many query features like recursive, nested, logical operators, etc. But there is something that we can't do; query by the related table.
Let's assume that we have a model structure like this;
class Student extends Model {
school() {
return this.hasOne("School", "id", "school_id");
}
}
class School extends Model {
}
In this scenario, the client should be able to query the student by the student's names;
/api/students?q=[ {"school.name.$like": "*Royal Institution*"} ]
For now, in the property name section, we define the column name as default;
/api/students?q=[ {"name": "Royal"} ]
Also, clients can add prefix or suffix**.
/api/students?q=[ {"age.$gt": 18}, {"$or.surname": "Locke" } ]
But, we want to create a new feature for related data queries. To do that, we need to analyze the property name, by splitting the name with .
So there are four possible meanings of the property value;
title
, age
, etc.school
A general example should be like this;
/api/students?q=[ {"age.$gt": 18}, {"$or.school.name.$like": "*Royal*" } ]
This example represents the following SQL;
FROM students
LEFT JOIN schools ON schools.id = students.school_id
WHERE (students.age > 18 OR schools.name LIKE "%ROYAL%")
We should validate the relation name if it has been defined on the Student
model.
An error is occurred if the app/Models
folder contains .gitignore
file.
(node:51706) UnhandledPromiseRejectionWarning: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension "" for /my-api/app/Models/.gitignore
at Loader.defaultGetFormat [as _getFormat] (internal/modules/esm/get_format.js:71:15)
at Loader.getFormat (internal/modules/esm/loader.js:105:42)
at Loader.getModuleJob (internal/modules/esm/loader.js:243:31)
at async Loader.import (internal/modules/esm/loader.js:177:17)
at async default (file:///my-api/node_modules/axe-api/src/resolvers/getModelInstanceArray.js:15:32)
at async Server._analyzeModels (file:///my-api/node_modules/axe-api/src/Server.js:69:19)
at async Server.listen (file:///my-api/node_modules/axe-api/src/Server.js:31:5)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:51706) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:51706) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
We should create a standard between handlers and hook/event names. We use INSERT as the handler name but the following hook names are not compatible.
Axe API has a powerful query structure but we make it better. For now, clients can ask for the related data for a model. But, if the client can send a query with joins, it would be great.
GET /api/post?joins=user{id,create_user_id},user{id,update_user_id}
This feature is completely under discussion. I am not sure we need something like this. Also, to manage this, we should be able to use aliases for fields and joins.
In the DB Analyzing process, model names don't work.
For example; ContactPhone
=> contactphones
It should be contact_phones
We don't have enough unit test in the project and we should add more unit tests before the launch.
We need to add with
feature to the Queries.
api/users?with=posts{id|title|comments{content}}
We should update the documentations.
Pagination summary doesn't return the total
and the lastPage
values some pages.
"pagination": {
"perPage": 2,
"currentPage": 2,
"from": 2,
"to": 4
}
It should be like this;
"pagination": {
"total": 13,
"lastPage": 7,
"perPage": 2,
"currentPage": 2,
"from": 2,
"to": 4
}
In some cases, we may need to use a transaction. We should provide that feature by defining it in models.
Knex.js allows us to use transaction like the following code;
// Using trx as a transaction object:
const trx = await knex.transaction();
const books = [
{title: 'Canterbury Tales'},
{title: 'Moby Dick'},
{title: 'Hamlet'}
];
trx('catalogues')
.insert({name: 'Old Books'}, 'id')
.then(function(ids) {
books.forEach((book) => book.catalogue_id = ids[0]);
return trx('books').insert(books);
})
.then(trx.commit)
.catch(trx.rollback);
Axe API has many shared codes between handlers. We may let the developers set their transaction strategy in the application general. But also, developers should be able to set special transaction rules for some handlers.
app/Config/Application.js
import { LOG_LEVEL } from "axe-api";
export default async () => {
return {
env: process.env.NODE_ENV,
port: process.env.APP_PORT,
logLevel: LOG_LEVEL.INFO,
transaction: false
};
};
Default transaction value: false
. But developers should be able to open in general. On the other hand, if a developer doesn't want a general transaction rule, they should be able to define a transaction rule in model definition;
import { Model } from "axe-api";
class User extends Model {
get transaction() {
return true;
}
}
export default User;
A transaction definition like this means that Axe API will use a transaction in all routes for the model. But, developers should be able to define handler-specific transactions too;
import { Model, HANDLERS } from "axe-api";
class User extends Model {
get transaction() {
return [
{
handler: HANDLERS.INSERT,
transaction: false
},
{
handler: HANDLERS.DELETE,
transaction: true
},
];
}
}
export default User;
Configuration importance is like this;
Developers can't use transactions in middleware functions.
In events, developers can't use transactions because events can have a different timeline.
In hooks, developers should be able to use the transaction.
const onBeforeInsert = async ({ database, transaction }) => {
};
export { onBeforeInsert };
In general, we are passing the database instance to the hook methods. But, we should pass the transaction object to the hook methods if there is any transaction configuration for the related model. If the developers disabled the transaction, we should pass the transaction variable as NULL
.
We should be able to handle errors and we shouldn't let locked tables be.
Now, we have two different configuration files for the database system. First, the main configuration file app/Config/Database.js
. The second is ./knexfile.js
.
./knexfile.js
is used by knex cli
to execute migrations. We should merge them in a way.
Axe API has strong query features. But it doesn't mean every developer will want that feature in their APIs. Especially, some query features on some models. So, developers should be able to which query features will be open to use in which models.
There should be a two-level configuration;
You can see the all following examples;
*
fields
sorting
limits
where.*
where.equal
where.notEqual
where.gt
where.gte
where.lt
where.lte
where.like
where.notLike
where.in
where.notIn
where.between
where.notBetween
where.null
where.notNull
trashed
with.*
with.hasOne
with.hasMany
These values should be in Enum fields.
The *
character should contain all sub-features. For example, where.*
means that all where
features.
Developers should be able to use two functions;
allow(feature: QueryFeature: fields: string[] = [])
deny(feature: QueryFeature: fields: string[] = [])
Developers should define all features by manually by using allow
and deny
functions.
const config: IVersionConfig = {
transaction: [],
serializers: [],
supportedLanguages: ["en"],
defaultLanguage: "en",
options: {
defaults: {
perPage: 10,
minPerPage: 5,
maxPerPage: 20,
},
queryLimits: [
allow("fields.all"),
allow("with.hasOne"),
allow("sorting"),
allow("where.*"),
deny("where.like"),
deny("where.notLike"),
],
},
};
Developers should be able to define model-based configurations like the following example;
class User extends Model {
get limits() {
return [allow("limits"), allow("where.like", ["name", "surname"])];
}
}
In this definition, developers should be able to define a special rule for some columns.
All logic should be read from top to bottom. The bottom values override the above configuration. Column-based configuration is override all.
Initial process;
Checking the feature;
If the client send unacceptable request, we should return a Bad Request
response.
We can change the following name in everywhere;
capabilities -> handlers
This issue has been created by the discussion in #846385235.
{
"name": "id",
"tableName": "users",
"isNullable": false,
"dataType": "Number",
"defaultValue": null,
"maxLength": null,
"numericPrecision": null,
"numericScale": null,
"isPrimary": true,
"isAutoIncrement": true
}
When the child data updated, the parent's updated_at
column should be updated automatically.
Axe API should be able to update timestamps automatically. But also, developers should be able to change the column name in the model definition.
To define column name;
import { Model } from "axe-api";
class User extends Model {
get createdAtColumn () {
return 'created_at'
}
get updatedAtColumn () {
return 'updated_at'
}
}
export default User;
created_at
, updated_at
NULL
Developers should be able to hide some fields in the HTTP response automatically.
import { Model } from "axe-api";
class User extends Model {
get hiddens () {
return ["password_salt", "pasword_hash"]
}
}
export default User;
Developers should be able to add Autosave
feature for record updates.
import { Model, HANDLERS } from "axe-api";
const { INSERT, SHOW, UPDATE, PAGINATE, AUTOSAVE } = HANDLERS;
class Student extends Model {
get handlers() {
return [INSERT, PAGINATE, SHOW, UPDATE, AUTOSAVE];
}
get fillable() {
return ["name", "phone"];
}
}
export default Student;
As default, this feature should be disabled. If developers want to enable it, they have to set handlers
getter.
PUT api/students/:id/autosave
Client doesn't have to send all record properties in autosave. For example; if the client wants to update only
name
field, they should be able to send onlyname
field. Form validation should be executed after the item and new value has been merged.
We don't have a filename standard in the project. We should add the eslint plugin to manage that;
For now, we call the init() function for once. But, developers should be able to use the express application in the beginning, and in the end.
const onBeforeInit = async ({ app }) => {};
const onAfterInit = async ({ app }) => {};
export { onBeforeInit, onAfterInit };
Developers should be able to select defaultLanguage
in configurations.
The following example shows how the general configuration should be;
app/Config/Application.ts
const config: IApplicationConfig = {
prefix: "api",
env: process.env.NODE_ENV || "production",
port: process.env.APP_PORT ? parseInt(process.env.APP_PORT) : 3000,
logLevel: LogLevels.INFO,
transaction: [],
serializer: [],
supportedLanguages: ["en", "en-GB", "fr"],
defaultLanguage: "en-GB",
};
Axe API has several system messages such as warnings or bugs. We are not going to translate that messages for several reasons. We'll keep those messages in English.
Example;
throw new Error(`Dependency is not found ${name}`);
Developers should decide what kind of i18n support they will provide. There are many solutions out there that can be used. We will NOT provide an internal solution.
validatorjs has good localization support. We can use them. But we are going to use ISO Language Code Table that is not incompatible with validatorjs. We may need a language codes map.
Clients should send language selection for every HTTP request because of API's stateless structure. To do that, we are going to use Accept-Language header. Also, we can use the accept-language-parser library to detect the client's selections
We should add an internal resolver to resolve Accept-Language
header. There are many packages out there but they are outdated.
In queries, we should be able to use $and
prefixes like the following one;
/api/contacts/1/phones?q=[{"type": "MOBILE" }, {"$and.type":"HOME"}]
But, we got an error;
Error: Unacceptable field name: $and.type
Let's assume that we have a model like this;
class StudentLesson extends Model {
lesson() {
return this.belongsTo("Lesson", "lesson_id", "id");
}
teacher() {
return this.belongsTo("Teacher", "teacher_id", "id");
}
}
When we request the following example, we don't use parentheses to group query features;
/api/students/1/lessons?q=[{ "$or.lesson.name.$like":"*a*"},{ "$or.teacher.name.$like":"*a*"}]
When we execute this request, it executes the following query;
where `student_id` = 1 and `lessons`.`name` like "%a%" or `teachers`.`name` like "%a%"
But, it should be like the following one;
where `student_id` = 1 and (`lessons`.`name` like "%a%" or `teachers`.`name` like "%a%")
Developers should be able to add a custom serialization method to the model. With that, they can create computed properties.
import { Model } from "axe-api";
class User extends Model {
get serialize(item) {
return {
...item,
fullname: `${item.name} ${item.surname}`
}
}
}
export default User;
The serialize
method should be optional. If there is any definition, we should call it before returning the model response.
I am not %100 sure but we should be able to call serialize methods for the related data.
Knex.js allows us to use multiple database configurations at the same time. Developers should be able to select a different connection for models.
import { Model } from "axe-api"
class User extends Model {
get connection() {
return "database-1"
}
}
export default User
import { Model } from "axe-api"
class Post extends Model {
get connection() {
return "database-2"
}
}
export default Post
Database.js
export default {
"database-1": {
client: process.env.DB_CLIENT,
connection: {
filename: "./db-1.sqlite",
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
},
},
"database-2": {
client: process.env.DB_CLIENT,
connection: {
filename: "./db-2.sqlite",
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
},
},
};
Application.js
import { LOG_LEVEL } from "axe-api";
export default async () => {
return {
env: process.env.NODE_ENV,
port: process.env.APP_PORT,
logLevel: LOG_LEVEL.INFO,
defaultDatabase: "database-1"
};
};
I have no idea how we manage multiple databases for the migration files.
For now, the default API response is something like that;
{
"name": "AXE API",
"description": "The best API creation tool in the world.",
"aim": "To kill them all!"
}
But nobody has to use that schema. Developers should be able to select what kind of response should be given. To do that, I suggest that to add a new property to the app/Config/Application.js
file like the following code;
import { LOG_LEVEL } from "axe-api";
export default async () => {
return {
env: process.env.NODE_ENV,
port: process.env.APP_PORT,
logLevel: LOG_LEVEL.INFO,
defaultResponse: {
"name": "AXE API",
"description": "The best API creation tool in the world.",
"aim": "To kill them all!"
},
};
};
With that, developers can decide what they want to show.
In this issue, we should update the axe-api-template project, too.
We don't have the following hooks yet;
We should check for all databases. We already know that MySQL is open to use numeric column names.
For now, Axe API defines routes by looking at the hasMany
relationship. But, developers should be able to define routes by looking at the hasOne
relationship, too.
import { Model } from "axe-api";
class User extends Model {
option() {
return this.hasOne("UserOption", "id", "user_id");
}
}
export default User;
In this case, Axe API should create the following routes;
GET /api/users/:id/option
: The handler will not be the PAGINATION
handler. It should be the SHOW
handler.POST /api/users/:id/option
PUT /api/users/:id/option
DELETE /api/users/:id/option
We don't have to use the same handlers as the hasMany()
method. We can create completely different handlers for this action because the handler will have special cases. For example; we don't need an id
parameter update Option model because the relation is one-to-one between the User
and UserOption
model.
For now, developers have to define all handler definitions one by one.
get middlewares() {
return [
{
handler: SHOW,
middleware: isAdmin,
},
{
handler: UPDATE,
middleware: isAdmin,
},
];
}
But, we can use the following method;
get middlewares() {
return [
{
handler: [SHOW, UPDATE],
middleware: isAdmin,
},
];
}
Developers should be able to select a custom primary key column in the model definition. The default value should be id
.
import { Model } from "axe-api";
class User extends Model {
get primaryKey () {
return 'uid'
}
}
export default User;
As a developer, I should be able to hide a model in auto-created routes.
import { Model } from "axe-api"
class User extends Model {
get ignore() {
return true
}
}
export default User
Obviously, we can use the handlers method to manage this. But the ignore()
method would be easier.
Client should be able to use those models in related queries.
Developers should be able to create a global serializer for HTTP responses. It will be greater general camelCase conventions.
Config/Application.js
import { LOG_LEVEL } from "axe-api";
export default async () => {
return {
env: process.env.NODE_ENV,
port: process.env.APP_PORT,
logLevel: LOG_LEVEL.INFO,
serializer: (result) => {
// do something...
return result;
},
};
};
Also, if developers want, they should be able to create handler-based serializers;
Config/Application.js
import { LOG_LEVEL } from "axe-api";
export default async () => {
return {
env: process.env.NODE_ENV,
port: process.env.APP_PORT,
logLevel: LOG_LEVEL.INFO,
serializer: {
PAGINATE: (result) => {
// do something
return result;
},
},
};
};
Currently, we are using Express as the request handler. But we can use different frameworks such as Fastify.
express
packages are installed to the axe-api
. We should remove them from the axe-api
. In axe-api-template
we should install express
as the default framework. Developers must install fastify
dependencies if they want to use it. (I am not sure we can do it.)app: Express
keywords. We should create an IFramework
interface. Also, we should have FastifyFramework
and ExpressFramework
that implement IFramework
. By configuration, we should create an instance of any of them. But the developers always should use IFramework
.IRequest
, IResponse
interfaces. They should be framework agnostic.currentLanguage
at some point. We should be able to do it dynamically by the configuration.We should throw an error if the related model name is wrong.
http://localhost:3000/api/users?with=postsx
A soft delete marks a record as no longer active or valid without actually deleting it from the database. Soft deletes can improve performance, and can allow “deleted” data to be recovered. [*]
Developers should be able to add soft-delete features to the models.
To add a soft-delete feature to a model, developers must add the following code to the model file;
class User extends Model {
get deletedAtColumn(): string {
return "deleted_at";
}
}
deletedAtColumn
should return NULL
from the base Model
. If developers add a database field name, it means that we have a soft-deleting feature for this model. Developers must create a timestamps
data field on the database via migrations.
The DELETE
handler must work as the soft-deleting if there is soft-deleting support. Otherwise, it must work like a normal delete.
On the other hand, developers should be able to add FORCE_DELETE
handler to models like the following example to support the force delete feature;
const { INSERT, PAGINATE, DELETE, FORCE_DELETE } = HandlerTypes;
class User extends Model {
get handlers(): HandlerTypes[] {
return [INSERT, PAGINATE, DELETE, FORCE_DELETE];
}
}
Clients should not be able to see the soft-deleted records in queries (PAGINATE
, SHOW
). Also, clients should not be able to manipulate the soft-deleted records (UPDATE
, PATCH
).
In addition, the with
keyword that fetches the related data should work with the soft-deleting feature perfectly.
Also, related data fetch must work perfectly with soft deleting. For example; the /api/users/1/posts
request should check if the user 1
is soft-deleted or not. (#132)
This would be a new handler for force deleting.
DELETE /api/users/:id/force
Clients should be able to fetch soft-deleted records from a database. To do that, they must use the following command;
/api/users?trashed=true
This should be configurable. By default, it should be false.
The following hooks and events should be supported for the force-delete handler;
deletedAtColumn
column to the base Model
DELETE
handler works.FORCE_DELETE
handler.with
operator works.trashed
parameter to the queries.PUT
, PATCH
/api/users/1/posts
)/api/users/1/customers
=> Customer)/api/users?with=customers
=> Customer)/api/customers/1/phones
=> Customer)/api/phones?with=customer
=> Customer)trashed
record by the query parameters (PAGINATE, SHOW, ALL)In this example, auto-route creation doesn't work properly.
User.js
import { Model } from "axe-api";
class User extends Model {
}
export default User;
Contact.js
import { Model } from "axe-api";
class Contact extends Model {
phones() {
return this.hasMany("ContactPhone", "id", "contact_id");
}
user() {
return this.belongsTo("User", "user_id", "id");
}
}
export default Contact;
ContactPhone.js
import { Model } from "axe-api";
class ContactPhone extends Model {
contact() {
return this.belongsTo("Contact", "contact_id", "id");
}
}
export default ContactPhone;
In this definition, I should be able to use the following query;
GET /api/contacts/1/phones/1?with=contact{name|surname|user{email|name|surname}}
But the routes are not created;
[
"POST /api/users",
"GET /api/users",
"GET /api/users/:id",
"PUT /api/users/:id"
]
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.