@avonjs/avonjs
Version:
A fluent Node.js API generator.
1,509 lines (1,056 loc) • 63.3 kB
Markdown
**Installation**
- [Requirements](#requirements)
- [Installation](#installation)
- [Initialize](#initialize)
- [Authentication](#authentication)
- [Login](#login)
- [Credentials](#credentials)
- [Options](#options)
- [Excluding](#excluding)
- [Commands](#commands)
**Resources**
- [The basics](#introduction)
- [Defining Resources](#defining-resources)
- [Registering Resources](#registering-resources)
- [Configuring Swagger UI](#configuring-swagger-ui)
- [Resource Hooks](#resource-hooks)
- [Resource Hooks](#resource-hooks)
- [Fields](#defining-fields)
- [Showing / Hiding Fields](#showing--hiding-fields)
- [Dynamic Field Methods](#dynamic-field-methods)
- [Default Values](#default-values)
- [Field Hydration](#field-hydration)
- [Field Types](#field-types)
- [Customization](#customization)
- [Nullable Fields](#nullable-fields)
- [Optional Fields](#optional-fields)
- [Filterable Fields](#filterable-fields)
- [Orderable Fields](#orderable-fields)
- [Lookup Fields](#lookup-fields)
- [Lazy Fields](#lazy-fields)
- [Relationships](#relationships)
- [BelongsTo](#belongsto)
- [HasMany](#hasmany)
- [HasOne](#hasone)
- [BelongsToMany](#belongstomany)
- [Customization](#customization)
- [Validation](#validation)
- [Rules](#attaching-rules)
- [Creation Rules](#creation-rules)
- [Update Rules](#update-rules)
- [Authorization](#authorization)
- [Performing Queries](#performing-queries)
**Repositories**
- [Defining Repositories](#defining-repositories)
- [Preset Repositories](#preset-repositories)
- [Defining Models](#defining-models)
- [Soft Deletes](#soft-deletes)
- [Timestamps](#timestamps)
**Filters**
- [Defining Filters](#defining-filters)
- [Registering Filters](#registering-filters)
- [Authorization Filters](#authorization-filters)
**Orderings**
- [Defining Orderings](#defining-orderings)
- [Registering Orderings](#registering-orderings)
- [Authorization Orderings](#authorization-orderings)
**Actions**
- [Defining Actions](#defining-actions)
- [Action Fields](#action-fields)
- [Action Responses](#action-responses)
- [Registering Actions](#registering-actions)
- [Authorization Actions](#authorization-actions)
- [Standalone Actions](#standalone-actions)
- [Inline Actions](#inline-actions)
**Activity Log**
- [Action Events](#action-events)
- [Custom Action Event](#custom-action-event)
- [Action Event Table](#action-event-table)
- [Action Event Actor](#action-event-actor)
**Error Handling**
- [Register Error Handler](#register-error-handler)
# Installation
## Requirements
Avon has a few requirements you should be aware of before installing:
- Node.js (Version 18)
- Expressjs Framework (Version 5.X)
## Installation
The following command install Avonjs application:
via npm:
```bash
npm install @avonjs/avonjs
```
via yarn:
```bash
yarn install @avonjs/avonjs
```
To develop fast in Avonjs you have to install the Avonjs [CLI](https://www.npmjs.com/package/@avonjs/cli):
via npm:
```bash
npm install @avonjs/cli -g
```
via yarn:
```bash
yarn install @avonjs/cli -g
```
## Initialize
At first point you have to register the router:
```js
// index.js
import { Avonjs } from '@avonjs/avonjs';
import express from 'express';
import cors from 'cors';
import express from 'express';
const app = express();
// required middlewares
app.use(express.json()).use(cors());
// register Avonjs router without authentication
app.use('/api', Avon.express());
// or register Avonjs router with authentication
app.use('/api', Avon.express(true));
app.listen(3000, () => {
console.log('running')
})
```
also, you can pass the array of middlewares as second parameter to customize the application behavior:
```js
app.use('/api', Avon.express(true, [
(req, res) => {
// custom middleware
}
]));
```
## Authentication
Avon ships by JWT authentication approach but it's disabled by default. to enable authentication you have to pass the `true` value as a second argument of the "express" method:
```js
// register Avonjs router
app.use('/api', Avon.express(true));
```
By default, Avonjs using the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) package to generate JWT tokens.
### Login
After enabling the JWT authentication you users need to login to get JWT token and access to API's. for this Avonjs has the `attemptUsing` method to handle users login:
```js
Avon.attemptUsing(async (payload) => {
const user = await new Users().first([
{
key: 'email',
operator: Constants.Operator.eq,
value: payload.email,
},
]);
if (user) {
return { id: user.getKey() };
}
});
```
you could return an arbitrary object containing the user identifier key as an `id` attribute.
### Credentials
Also, you are free to customize login credentials by the `credentials` method in the Avonjs class like so:
```js
Avon.credentials([new Fields.Email().default(() => 'zarehesmaiel@gmail.com')]);
```
The `credentials` method accepts an array of Avonjs fields as a parameter.
### JWT Options
Avonjs `signOptions` and `verifyOptions` help you to configure JWT durin signing or verify process.
#### Sign Options
Avon `signOptions` method allows you to customize JWT token config during token generation. for example; here we have an Asymmetric JWT configuration:
```js
Avon.signOptions({
algorithm: 'RS256',
allowInvalidAsymmetricKeyTypes: false,
expiresIn: config.get<number>('app.auth.cache'),
})
```
to read more about signing options you can refer to the [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) package.
Also you can change the secret key uses during sign options by the Avonjs `appKey`:
```js
// set private key here
Avon.appKey('private_key')
// then change signing options
Avon.signOptions({
algorithm: 'RS256',
allowInvalidAsymmetricKeyTypes: false,
expiresIn: config.get<number>('app.auth.cache'),
})
```
#### Verify Options
After configuring the sing options, you need to clarify the verify options for Avonjs. to perform this action there is a `verifyOptions` helper:
``` js
Avon.verifyOptions({
algorithms: ['RS256'],
secret: 'publicKey',
isRevoked: async (req, token) => {
// any operation to check
return false;
},
})
```
Here's an improved version of the documentation:
### Resolving User
Once a user logs into the API, you can access their instance in two ways:
1. Using the decoded JWT instance available via the `auth` method in the request.
2. Accessing the fully resolved user instance.
To set up user resolution after login, Avon.js allows you to define a callback using the `resolveUserUsing` method. This method resolves the authenticated user's instance and attaches it to the request, making it accessible throughout the application.
For example, to protect Avon.js for logged-in users, you can resolve the user in the request as follows:
```js
Avon.resolveUserUsing(async (request) => {
return new Users().find(request.auth()?.id);
});
```
After setting up user resolution, you can access the resolved user instance from the request using the `user` method. Here’s an example of how you might use it:
```js
new Fields.Text('role').canSee(request => request.user().isDeveloper());
```
This approach allows you to control access to fields, actions, and other resources based on the authenticated user's attributes.
### Excluding
If you need to exclude some routes from authentication the `except` method can help you:
```js
Avon.except('/api/resources/pages').except(/.*\/actions\/register-users/)
```
You are free to use `string` or `regex` to exclude paths. we are using the [express-unless](https://www.npmjs.com/package/express-unless) to handling this situation.
## Commands
By default, Avonjs put files generated by Avonjs cli under `src` directory. to change this path you could configure it by root `package.json` file:
```json
{
...
"sourceDir" : "myDir"
}
```
Also, Avonjs detects the type of package by checking the `tsconfig.json` existence or module type indicated on the package json but you could determine the output file per command:
```bash
avonjs make:resource Another --output typescript
```
# Resources
## Introduction
Avon is a beautiful API generator for Node.js applications written in typescript. Of course, the primary feature of Avonjs is the ability to administer your underlying repository records. Avonjs accomplishes this by allowing you to define an Avonjs `resource` corresponding to each `repository` in your application.
## Defining Resources
By default, Avonjs resources are stored in the `src/avonjs/resources` directory of your application. You may generate a new resource using the `resource:make` Avonjs command:
```bash
avonjs make:resource Post
```
Freshly created Avonjs resources only contain an `ID` field definition and simple repository instance. Don't worry, we'll add more [fields](#fields) to our resource soon and you'll learn more about [repositories](#defining-repositories) later.
## Registering Resources
Before resources are available within your API, they must first be registered with Avon. You may use the `resources` method to manually register individual resources:
```js
// resources method
Avon.resources([New Post()])
```
If you do not want a resource api to appear in the swagger-ui, you may override the following property of your resource class:
- `availableForSwagger`
Also if you want to hide some apis in the swagger-ui, you may override the following property of your resource class:
- `availableForIndex`
- `availableForDetail`
- `availableForCreation`
- `availableForUpdate`
- `availableForDelete`
- `availableForForceDelete`
- `availableForRestore`
- `availableForReview`
## Configuring Swagger UI
Avon creates a schema based on the [OpenApi](https://github.com/OAI/OpenAPI-Specification) that enables you to use the [swagger-ui](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/installation.md) for documentation. here we using docker to install `swagger-ui`. let's do it:
first run the following command to install swagger-ui:
```bash
docker pull swaggerapi/swagger-ui
```
now we use the previously [created](#initialize) URL for schema to run docker:
```bash
docker run -p 80:8080 -e SWAGGER_JSON_URL=http://localhost:3000/api/schema -e PERSIST_AUTHORIZATION=true swaggerapi/swagger-ui
```
now you can go to the `http://localhost` and see the result.
**_Attentions_**
- You have to run the server to see the the documentation in the swagger. maybe you need something like this in the root of your project `npm run start`
- If you see `CORS` error when swagger ui loaded the [this](https://expressjs.com/en/resources/middleware/cors.html#installation) tutorial can solve your problem.
## Resource Hooks
Avon provides a set of hooks that you can define on a resource. These hooks are invoked when the corresponding resource action is executed within Avon, allowing you to execute custom logic at specific points in the lifecycle of a resource.
- `afterCreate`
- `beforeCreate`
- `afterUpdate`
- `beforeUpdate`
- `afterDelete`
- `beforeDelete`
- `afterRestore`
- `beforeRestore`
- `afterForceDelete`
- `beforeForceDelete`
### Post-Commit Hooks
In addition to the above hooks, Avonjs provides the following hooks that are called after the resource changes are committed to the storage:
- `created`
- `updated`
- `deleted`
- `restored`
## Pagination
If you would like to customize the selectable maximum result amounts shown on each resource's "per page" filter menu, you can do so by overriding the `perPageOptions` method:
```js
/**
* Get the pagination per-page values
*/
public perPageOptions(): number[] {
return [15, 25, 50];
}
```
## Defining Fields
Each Avonjs resource contains a fields method. This method returns an array of fields, which generally extend the `Fields\Field` class. Avonjs ships with a variety of fields out of the box, including fields for text inputs, booleans, etc.
To add a field to a resource, you may simply add it to the resource's fields method. Typically, fields have to add as new class with accepts several arguments; however, you usually only need to pass the "attribute" name of the field that normally determine the underlying repository storage column:
```js
// Resources/Post.js
import { Fields } from '@avonjs/avonjs';
/**
* Get the fields available on the entity.
*/
public fields(request: AvonRequest): Field[] {
return [
new Fields.ID().filterable().orderable(),
new Fields.Text('name').filterable().orderable(),
];
}
```
## Showing / Hiding Fields
Often, you will only want to display a field in certain situations. For example, there is typically no need to show a `Password` field on a resource index listing. Likewise, you may wish to only display a `Created At` field on the creation / update forms. Avonjs makes it a breeze to hide / show fields on certain pages.
The following methods may be used to show / hide fields based on the display context:
- showing
- `showOnIndex`
- `showOnDetail`
- `showOnReview`
- `showOnAssociation`
- `showOnCreating`
- `showOnUpdating`
- hiding
- `hideFromIndex`
- `hideFromDetail`
- `hideFromReview`
- `hideFromAssociation`
- `hideWhenCreating`
- `hideWhenUpdating`
- other
- `onlyOnIndex`
- `onlyOnDetail`
- `onlyOnReview`
- `onlyOnAssociation`
- `onlyOnForms`
- `exceptOnForms`
You may chain any of these methods onto your field's definition in order to instruct Avonjs where the field should be displayed:
```js
new Fields.ID().exceptOnForms()
```
Alternatively, you may pass a callback to that methods as following;
For `show*` methods, the field will be displayed if the given callback returns `true`:
```js
new Fields.Text('name').exceptOnForms((request, resource) => {
return resource?.name === 'something';
}),
```
For `hide*` methods, the field will be hidden if the given callback returns `true`:
```js
new Fields.Text('role').hideFromIndex((request, resource) => {
return request.user()?.isNotDeveloper();
}),
```
## Dynamic Field Methods
If your application requires it, you may specify a separate list of fields for specific display contexts. The available methods that may be defined for individual display contexts are:
- `fieldsForIndex`
- `fieldsForDetail`
- `fieldsForReview`
- `fieldsForCreate`
- `fieldsForUpdate`
- `fieldsForAssociation`
**Dynamic Field Methods Precedence**
The `fieldsForIndex`, `fieldsForDetail`, `fieldsForReview`, `fieldsForCreate`, `fieldsForUpdate` and `fieldsForAssociation` methods always take precedence over the `fields` method.
## Default Values
There are times you may wish to provide a default value to your fields. Avonjs offers this functionality via the `default` method, which accepts callback. The result value of the callback will be used as the field's default input value on the resource's `creation` API:
```js
new Fields.Text('role').default((request) => 'default role')
```
## Field Hydration
For every create or update request Avon.js receives for a resource, each field’s corresponding model attribute is automatically populated before the model is saved to the database. If you need to customize how a field's value is set, you can use the `fillUsing` method:
```js
new Fields.Text('name').fillUsing((request, model, attribute, requestAttribute) => {
model.setAttribute(
attribute, request.string(attribute).toUpperCase()
);
});
```
Additionally, for each presentation request, Avon.js tries to resolve the field’s value based on the field's attribute name in the resource. If you need to adjust how a field’s value is retrieved, you can use the `resolveUsing` method:
```js
new Fields.Integer('userId').resolveUsing((_, resource) => Number(resource.getAttribute('user_id')));
```
These methods provide flexibility in customizing how fields are set and retrieved, allowing for tailored data processing before saving and presentation.
## Field Types
- [Array Field](#array-field)
- [Binary](#binary-field)
- [DateTime](#datetime-field)
- [Email Field](#email-field)
- [ID Field](#id-field)
- [Json Field](#json-field)
- [List Field](#list-field)
- [Integer Field](#integer-field)
- [Decimal Field](#decimal-field)
- [Text Field](#text-field)
- [Enum Field](#enum-field)
### Array Field
The `Array` field pairs nicely with model attributes that are cast to `array` or equivalent:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Array('tags')
```
You may restrict the length of an array with the `min` and `max` methods:
```js
new Fields.Array('title').min(1).max(120)
```
Alternatively, you can set a specific `length`:
```js
new Fields.Array('title').length(3)
```
### Binary Field
The `Binary` field may be used to represent a boolean / "tiny integer" column in your database. For example, assuming your database has a boolean column named `active`, you may attach a `Binary` field to your resource like so:
```js
import { Rules, Fields } from "@avonjs/avonjs";
new Fields.Binary("active").rules(Rules.required()).nullable(false);
```
### DateTime
The `DateTime` field may be used to store a `datetime` value.
```js
import { Fields } from '@avonjs/avonjs';
new Fields.DateTime('publish_at'),
```
The `format` method allows you to customize the date format that accepts any valid [luxon](https://moment.github.io/luxon/#/?id=luxon) formatting.
### Email Field
The `Email` field may be used to store a `email` value.
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Email('mail'),
```
### ID Field
The `ID` field represents the primary key of your resource's repository model. Typically, each Avonjs resource you define should contain an `ID` field. By default, the `ID` field assumes the underlying storage column is named `id`; however, you may pass the column name when creating an `ID` field:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.ID()
```
### Json Field
The `Json` field provides a convenient interface to edit, `key-value` data stored inside `JSON` column types. For example, you might store some information inside a `JSON` column type (opens new window) named `meta`:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Json('meta', [
new Fields.Text('title').creationRules(Rules.required()),
])
```
You are free to pass any non-relational field as second parameters of `Json` field to shape and validate its entire data.
### List Field
The `List` field offers a user-friendly interface for editing arrays of `key-value` data stored within `JSON` column types. the list field is a combination of `Json` field and `Array` field:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.List('comments', [
new Fields.Text('name').creationRules(Rules.required()),
new Fields.Text('comment').creationRules(Rules.required()),
])
```
You are free to pass any non-relational field as second parameters of `List` field to shape and validate its entire data.
### Integer Field
The `Integer` field store / retrieve value as `integer` in the model:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Integer('hits')
```
You can restrict the range of values accepted for a field in your application by using the `min` and `max` methods:
```js
new Fields.Integer('price').min(1).max(10000)
```
### Decimal Field
The `Decimal` field store / retrieve value as `float` in the model:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Decimal('price')
```
The `precision` method helps you to specify the maximum number of decimal places:
```js
new Fields.Decimal('price').precision(2)
```
## Text Field
The `Text` field store / retrieve value as `string` in the model:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Text('name')
```
You may define constraints on the length of text input for a field in your application using the `min` and `max` methods
```js
new Fields.Text('title').min(1).max(120)
```
## Enum Field
The `Enum` field store / retrieve certain values as `string` in the model. The enum field accepts a list of possible values as the second parameter:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.Enum('status', ['published', 'draft'])
```
## Customization
### Nullable Fields
By default, Avonjs attempts to store all fields with a value, however, there are times where you may prefer that Avonjs store a `null` value in the corresponding storage column when the field is empty. To accomplish this, you may invoke the `nullable` method on your field definition:
```js
new Fields.DateTime('publish_at').nullable()
```
You may also set which values should be interpreted as a `null` value using the `nullValues` method, which accepts an function as validator:
```js
new Fields.DateTime('publish_at').nullable().nullValues((value) => ['', undefined, null].includes(value));
```
### Optional Fields
By default, Avonjs attempts to validate all fields in a request. However, there are situations where certain fields may not be required in the request. To make a field optional, you can invoke the `optional` method on your field definition. for example; to define a `DateTime` field that is not required in the request, use the `optional` method as shown below:
```js
new Fields.DateTime("publish_at").optional();
```
This approach ensures that the `publish_at` field is not required during validation. If the field is omitted in the request, it will not trigger a validation error.
## Filterable Fields
The `filterable` method allows you to enable convenient, automatic filtering functionality for a given field on the resource's index:
```js
new Fields.Text('name').filterable()
```
Also its possible to passing a callback to customize the filtering behavior:
```js
import { Fields, Constants } from '@avonjs/avonjs';
new Fields.Text('name').filterable((request, repository, value) => {
repository.where({
key: this.filterableAttribute(request),
operator: Constants.Operator.like,
value,
});
})
```
## Orderable Fields
When attaching a field to a resource, you may use the `orderable` method to indicate that the resource index may be sorted by the given field:
```js
new Fields.Text('name').orderable()
```
Also its possible to passing a callback to customize the ordering behavior:
```js
new Fields.Text('name').orderable((request, repository, direction) => {
repository.order({
key: 'another key',
direction: 'desc',
});
})
```
## Lookup Fields
Sometimes you may need to find a resource using a field other than its default primary key. You can use the `lookupable()` method to mark a field as a valid lookup key:
```js
new Fields.Text('name').lookupable()
```
This allows the resource to be retrieved using that field in route definitions or API calls.
---
### 🔧 Customize Lookup Behavior
If you need more control over how the lookup is performed, you can pass a callback to define custom logic using the `lookupable()` method:
```js
new Fields.Text('name').lookupable((request, repository, value) => {
return repository.where('custom_column', value);
});
```
Alternatively, use the `lookupUsing()` method to simply change the database column used for the lookup:
```js
new Fields.Text('name').lookupUsing('custom_column')
```
## Lazy Fields
During development, you may need to resolve the value of certain fields based on the resolved resource. For this situation, you can create a new field that extends the `Lazy` class. This allows you to dynamically compute and set the field's value based on additional data fetched asynchronously.
Here's an example of how to create and use a `Lazy` field:
### Example: MessageCounter Field
```js
export default class MessageCounter extends Fields.Lazy {
/**
* Resolve necessary data for the given resources and set the resolved values.
*/
async resolveForResources(
request: AvonRequest,
resources: Array<Contracts.Model & HasMessages>
): Promise<void> {
// Fetch unread message counts for the given resources and user
const counts = await this.countUnreadMessages(
resources.map((resource) => resource.filterKey()),
[Number(request.user()?.userId)].filter((id) => id)
);
// Set the message count attribute on each resource
resources.forEach((resource) => {
const count = counts.find(
({ filter }) => resource.filterKey() === filter
);
resource.setAttribute(this.attribute, count?.count ?? 0);
});
}
/**
* Count unread messages for the given filters and user IDs.
*/
async countUnreadMessages(
filters: Array<string>,
userIds: Array<number>
): Promise<Array<{ filter: string, count: number }>> {
// Your implementation for counting unread messages
}
}
```
In the `resolveForResources` method, you can fetch the necessary data and set it on the resource. This data can then be used when resolving the field value.
### Using the Lazy Field
To use the `MessageCounter` field, simply include it in your resource definition and it will handle the lazy resolution of the message count for each resource.
```js
import MessageCounter from "./MessageCounter";
export default class UserResource extends Resource {
// Other field definitions...
fields() {
return [
// Other fields...
new MessageCounter("unreadMessages"),
];
}
}
```
This approach allows you to defer the resolution of field values until you have all the necessary data, making your application more flexible and efficient.
## Relationships
In addition to the variety of fields we've already discussed, Avonjs has support for some relationships. Avonjs relation fields allows you to handle relationships between resources.
### BelongsTo
The `BelongsTo` field corresponds to a `belongs-to` relationship. For example, let's assume a `Post` resource belongs to a `User` resource. We may add the relationship to our `Post` Avonjs resource like so:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.BelongsTo('users')
```
As you see, `BelongsTo` accepts the `uriKey` of the target resource as first argument. By default `BelongsTo` field guess the `relationship` name from the target resource, but you can pass the second argument when creating a field to change that.
In the example above, Avonjs will will give `user` value from request and store primary key of the `User` resource in the `user_id` attribute of the `Post` resource. to change that you can follow the below example:
```js
new Fields.BelongsTo('users', 'author')
```
now, Avonjs retrieve `author` from request and store it in the `author_id` of post attributes.
Avon determines the default foreign key name by examining the name of the `relationship` and suffixing the name with a `_` followed by the name of the parent resource model's primary key column. So, in this example, Avonjs will assume the `User` model's foreign key on the `posts` repository is `author_id`.
However, if the foreign key for your relationship does not follow these conventions, `withForeignKey` method allows you change the foreign key of the relation:
```js
new Fields.BelongsTo('users', 'author').withForeignKey('user_id')
```
Also, Avonjs use the `id` column of the parent model to store as foreign key. If your parent model does not use `id` as its primary key, or you wish to find the associated model using a different column you can use the `withOwnerKey` method to specifying your parent table's custom key:
```js
new Fields.BelongsTo('users', 'author').withOwnerKey('userId')
```
Now, Avonjs try to find the related `user` by `userId`.
#### Nullable Relationships
If you would like your `BelongsTo` relationship to be `nullable`, you may simply chain the nullable method onto the field's definition:
```js
new Fields.BelongsTo('users', 'author').nullable()
```
#### Load related Resource
The `BelongsTo` field only display the related resource foreign key on the `detail` and `index` API but some times you need to load the related resource instead of foreign key. for example you want to see the `User` record on the `Post` api. for this situation you can use the `load` method on the `BelongsTo` field:
```js
new Fields.BelongsTo('users').load()
```
#### Filter Trashed Items
By default, the `BelongsTo` field includes soft-deleted records when pre-loaded; however, this can be disabled using the `withoutTrashed` method:
```js
new Fields.BelongsTo('users', 'author').withoutTrashed()
```
### HasMany
The `HasMany` field corresponds to a `one-to-many` relationship. A one-to-many relationship is used to define relationships where a single model is the parent to one or more child models. For example, a use may have a many posts in the blog. We may add the relationship to our `User` Avonjs resource like so:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.HasMany('posts')
```
Like another relationships, `HasMany` accepts the `uriKey` of the target resource as first argument Also, guess the `relationship` name from the target resource, but you can pass the second argument when creating a field to change that.
```js
import { Fields } from '@avonjs/avonjs';
new Fields.HasMany('posts', 'latestPosts')
```
Avon determines the default foreign key name by examining the name of the resource and suffixing the name with a `_` followed by the name of the resource model's primary key column. So, in this example, Avonjs will assume the `User` model's foreign key on the `posts` repository is `user_id` but, the `withForeignKey` method allows you to change this behavior. so let assume the `User` id column stored as `author_id` on the posts record, so example will be change like following:
```js
new Fields.HasMany('posts', 'latestPosts').withForeignKey('author_id')
```
Also if you are using the another key instead of `id` of the resource, you can change the `HasMany` field like below:
```js
new Fields.HasMany('posts', 'latestPosts').withOwnerKey('userId')
```
### HasOne
The `HasOne` field corresponds to a `one-to-one` relationship. For example, let's assume a `User` Avonjs resource hasOne `Profile` Avonjs resource. this field is like the `HasMany` field, and the only thing that has changed is the result of loaded resources that limited only into one. so the following example will load only the one related resource detail:
```js
new Fields.HasOne('posts', 'latestPosts').withForeignKey('author_id')
```
### BelongsToMany
The `BelongsToMany` field corresponds to a `many-to-many` relationship. For example, let's assume a `Post` Avonjs resource has many `Tag` Avonjs resource and in reverse `Tag` Avonjs resource has many `Post` Avonjs resource. to show the related `Tag` records on the `Post` resource `index` / `detail` api, we need two another more resource to hold the `pivot` table. so we have to create `PostTag` Avonjs resource to store joining records. we may add the relationship on the `Post` resource like so:
```js
import { Fields } from '@avonjs/avonjs';
new Fields.BelongsToMany('tags', 'post-tags')
```
The `BelongsToMany` field stores the primary key of the resource and related resource into the pivot resource into attributes by examining the name of the them and suffixing the name with a `_` followed by the name of the model's primary key column. the `setResourceForeignKey` method allows you to change attribute name for the `resource` and `withForeignKey` change the `related-resource` attribute foreign key name:
```js
new Fields.BelongsToMany('tags', 'post-tags').setResourceForeignKey('postKey').withForeignKey('tagKey')
```
Also if you are using another key to reefer the resource or the related resource, `setResourceOwnerKey` and `withOwnerKey` allows to change this attributes like so:
```js
new Fields.BelongsToMany('tags', 'post-tags').setResourceForeignKey('postKey').withForeignKey('tagKey').setResourceOwnerKey('name').withOwnerKey('name')
```
#### Pivot Fields
If your `belongsToMany` relationship interacts with additional "pivot" fields that are stored on the intermediate table of the many-to-many relationship, you may also attach those to your `BelongsToMany` Avonjs relationship.
For example, let's assume our `Post` model `belongsToMany` `Tag` resource. On our `post-tag` intermediate storage, let's imagine we have a `order` attribute that contains ordering of relationship. We can attach this `pivot` attribute to the `BelongsToMany` field using the `pivots` method:
```js
new Fields.BelongsToMany('tags', 'post-tags').pivots((request) => {
return [
Integer('order'),
];
})
```
#### Load related Resource
The `BelongsToMany` field does not display the related resource on the `detail` and `index` API but the `load` method allows you to meet the attached resource like so:
```js
new Fields.BelongsToMany('tags').load()
```
### Customization
### Relatable Resource Formatting
By default, when you load the relationship fields on the resource API, Avonjs use the index fields to format the related resource. If you would like to customize the related resource attributes on the `parent` or `child` API, the `fields` method on the relationship fields allows you to pass some fields to change the display attributes like so:
```js
new Fields.BelongsTo('users').load().fields((request) => {
return [
new Fields.Text('name'),
new Fields.ID(),
]
})
```
Also on the `BelongsToMany` relationship you can access `pivot` values:
```js
new Fields.BelongsToMany('tags').load().fields((request) => {
return [
new Fields.Text('name'),
new Integer('order', (value, resource) => {
return resource.getAttribute('pivot').getAttribute('order')
})
]
})
```
### Relatable Resource Filtering & Ordering
The `filterable` and `orderable` methods can also be applied to association fields, enabling you to filter and order related resources. Here's an example of how to use these methods with a BelongsTo association:
```js
new Fields.BelongsTo('users').load().fields((request) => {
return [
new Fields.Text('name').filterable().orderable(),
new Fields.ID(),
]
})
```
By utilizing these methods, you can enhance the `filtering` and `ordering` capabilities of your resource associations. Adjust the fields and methods as needed to suit your application's requirements.
### Relatable Query Filtering
For now, the `BelongsToMany` and `BelongsTo` relationship field's, allows you to modify their results on the create / update API. for common use case when you want to display the select fields on the UI, you need an API to get related resource for this fields. Fortunately, Avonjs create an extra API for this types of relationships that enables you to have an specific customizable API for each field. For example, if you have a `BelongsTo` field on the `Post` resource to show the author of the post, you will see an API like `/api/resources/posts/associable/user` on the swagger-ui. If you would like to customize the association query, you may do so by invoking the `relatableQueryUsing` method:
```js
new Fields.BelongsTo('users').relatableQueryUsing((request, repository) => {
return repository.where({
key: 'role',
operator: Constants.Operator.like,
value : 'admin'
})
})
```
### Limiting Relation Results
You can limit the number of results that are returned when searching the field by defining a `relatableSearchResults` property on the class of the resource that you are searching for:
```js
/**
* The number of results to display when searching relatable resource.
*/
relatableSearchResults = 5;
```
## Validation
Unless you like to live dangerously, any Avonjs fields that are displayed on the Avonjs creation / update APIs will need some validation. Thankfully, it's a cinch to attach all of the [Joi](https://joi.dev/api) validation rules you're familiar with to your Avonjs resource fields. Let's get started.
## Rules
To avoid errors caused by merging different versions of Joi, **Avon.js** provides a `Rules` class, which acts as an alias for the Joi package. This ensures compatibility and simplifies validation without requiring you to install Joi as a separate dependency.
You can easily use the `Rules` class in your project as follows:
```js
import { Rules } from "@avonjs/avonjs";
// Example usage of Rules (Joi) for schema validation
new Fields.Text("name").rules(Rules.string());
```
By using `Rules`, you can avoid version conflicts and maintain consistent validation logic throughout your application.
## Attaching Rules
When defining a field on a resource, you may use the `rules` method to attach validation rules to the field:
```js
import { Rules } from "@avonjs/avonjs";
new Fields.Text("name").rules(Rules.string());
```
## Creation Rules
If you would like to define rules that only apply when a resource is being created, you may use the `creationRules` method:
```js
import { Rules } from "@avonjs/avonjs";
new Fields.Text("name").rules(Rules.string()).creationRules(Rules.required());
```
## Update Rules
Likewise, if you would like to define rules that only apply when a resource is being updated, you may use the `updateRules` method:
```js
import { Rules } from "@avonjs/avonjs";
new Fields.Text("name")
.rules(Rules.string())
.creationRules(Rules.required())
.updateRules(Rules.optional());
```
## Authorization
When Avonjs is accessed only by you or your development team, you may not need additional authorization before Avonjs handles incoming requests. However, if you provide access to Avonjs to your clients or a large team of developers, you may wish to authorize certain requests. For example, perhaps only administrators may delete records. Thankfully, Avonjs takes a simple approach to authorization.
### API
To limit which users may `view`, `create`, `update`, `delete`, `forceDelete`, `restore`, `attach`, `detach` and `add` resources, you can override the authorization methods:
- `authorizedToViewAny`
- `authorizedToView`
- `authorizedToCreate`
- `authorizedToUpdate`
- `authorizedToDelete`
- `authorizedToForcDelete`
- `authorizedToRestore`
- `authorizedToReview`
- `authorizedToAdd`
- `toggleAttachment`
### Disabling Authorization
If you want to disable authorization for specific resource (thus allowing all actions), change `authorizable` method to return `false`:
```js
/**
* Determine if need to perform authorization.
*/
public authorizable(): boolean {
return false;
}
```
## Performing Queries
Avon.js provides several helper methods to customize queries when retrieving resources from database storage. These methods allow you to modify queries before they are executed, providing flexibility to meet specific requirements:
- [indexQuery](#index-query)
- [detailQuery](#detail-query)
- [reviewQuery](#review-query)
- [relatableQuery](#relatable-query)
### Index Query
The `indexQuery` method lets you customize the query for the index API. For example, you can join tables or restrict results based on certain conditions:
```js
indexQuery(request: AvonRequest, queryBuilder: Repository) {
return queryBuilder.whereKeys([1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
```
In this example, the final query retrieves resources with IDs `[1, 2, 3, 4, 5, 6, 7, 8, 9]`.
### Detail Query
The `detailQuery` method is called when serving a resource view for specific IDs:
```js
detailQuery(request: AvonRequest, queryBuilder: Repository) {
return queryBuilder.withUsers();
}
```
In this example, the `withUsers` method is called on the repository to load related users.
### Review Query
The `reviewQuery` method is triggered when the resource is being restored:
```js
reviewQuery(request: AvonRequest, queryBuilder: Repository) {
return queryBuilder.whereIsNotExpired();
}
```
Here, the `whereIsNotExpired` method restricts the query to only non-expired resources.
### Relatable Query
The `relatableQuery` method customizes queries for associable APIs, for example, when assigning a related resource using a relational field:
```js
relatableQuery(request: AvonRequest, queryBuilder: Repository) {
return queryBuilder.isActive();
}
```
In this example, the `isActive` method restricts the query to only active resources when assigning a related resource.
**Relatable Query Example**
Suppose you have a `Post` resource with a `BelongsTo` field to assign users to posts. To restrict this field to only active users, add a `relatableQuery` method in the `User` resource to enforce this restriction:
```js
relatableQuery(request: AvonRequest, queryBuilder: Repository) {
return queryBuilder.isActive();
}
```
This approach ensures that only active users are available for assignment in related queries.
### Fields
Sometimes you may want to prevent updating certain fields by a group of users. You may easily accomplish this by chaining the `canSee` method onto your field definition. The `canSee` method accepts a function which should return `true` or `false`. The function will receive the incoming HTTP request:
```js
new Fields.Binary('active').canSee((request) => false)
```
# Repositories
## Defining Repositories
Repositories in AvonJS provide a structured way to interact with APIs and manage data storage. By default, all repositories are stored in the `repositories` directory. You can generate a repository using the `make:repository` Avonjs command as follows:
```bash
avonjs make:repository Posts
```
or with soft deletes:
```bash
avonjs make:repository Posts --soft-deletes
```
Each repository within AvonJS is designed to return data formatted according to a specific model. To define these models, you can refer to the [instructions on defining models](#defining-models) provided later in the documentation.
## Preset Repositories
Avon by default, ships with a variety of repositories:
- [Collection Repository](#collection-repository)
- [File Repository](#file-repository)
- [Knex Repository](#knex-repository)
### Collection Repository
The collection repository holds records under the memory RAM so it will lose data after operations or application restarts. this repository is good when you want to serve an array of data as an API. you could create a collection repository like so:
```bash
avonjs make:repository Posts --collection
```
or with soft deletes:
```bash
avonjs make:repository Posts --collection --soft-deletes
```
### File Repository
The file repository is a type of collection repository with a bit of change. as the collection repository stores data on the memory, the File repository, holds data in the JSON file. to create a file repository you have to create a class like the below and define the store file path.
```bash
avonjs make:repository Posts --file
```
or with soft deletes:
```bash
avonjs make:repository Posts --file --soft-deletes
```
### Knex Repository
The Knex repository stores data on the database and uses [knex.js](https://knexjs.org/) package to maintain data. you could use it like so:
```bash
avonjs make:repository Posts --knex
```
or with soft deletes:
```bash
avonjs make:repository Posts --knex --soft-deletes
```
## Defining Models
Each model is a class which implements `Model` interfaces. you may generate a model like so:
```bash
avonjs make:model Post
```
By default Avonjs ships by a simple `Fluent` model and you could use it as your model or base model if you want!
```js
import { Models } from '@avonjs/avonjs';
export default class MyModel extends Models.Fluent {
//
}
```
## Has Attributes
Avon.js includes the `HasAttributes` mixin, which provides tools for creating accessors, mutators, and attribute casting. These features allow you to transform model attribute values when retrieving or setting them on model instances. For example, you might want to convert a JSON string stored in the database to an array when accessed through your model. Additionally, you can control model serialization by hiding specific fields from visibility.
You can use the `HasAttributes` mixin in your model like this:
```js
import type { AnyRecord, Model, PrimaryKey, UnknownRecord } from '../Contracts';
import HasAttributes from '../Mixins/HasAttributes';
export default class Fluent extends HasAttributes(class {}) implements Model {
constructor(public attributes: UnknownRecord = {}) {
super();
// i tested some approach but it have problem with private member assignments
// biome-ignore lint/correctness/noConstructorReturn: i don't have any solution for it
return new Proxy(this, {
get: (parent, property, receiver) => {
// handle exists method
if (property in parent) {
return parent[property as keyof typeof parent];
}
return parent.getAttribute(property as string);
},
set: (model, key: string, value) => {
if (key in model) {
model[key as keyof Model] = value;
} else {
model.setAttribute(key, value ?? true);
}
return true;
},
});
}
/**
* Set the attributes.
*/
setAttributes(attributes: UnknownRecord) {
for (const key in attributes) {
this.setAttribute(key, attributes[key]);
}
return this;
}
/**
* Set value for the given key.
*/
setAttribute(key: string, value: unknown) {
super.setAttributeValue(key, value);
return this;
}
/**
* Get value for the given key.
*/
getAttribute<T = undefined>(key: string): T {
return super.getAttributeValue<T>(key);
}
/**
* Get the model key.
*/
getKey(): PrimaryKey {
return this.getAttribute<PrimaryKey>(this.getKeyName());
}
/**
* Get primary key name of the model.
*/
getKeyName(): string {
return 'id';
}
/**
* Convert attributes to JSON string.
*/
public toJson(): string {
return JSON.stringify(this.getAttributes());
}
}
```
This mixin enables flexible control over attribute handling, enhancing how data is managed and presented in your application.
### Accessors and Mutators
An accessor transforms an model attribute value when it is accessed. To define an accessor, create a method on your model to represent the accessible attribute. This method name should correspond to the "camel case" representation of the true underlying model attribute / database column when applicable.
In this example, we'll define an accessor for the `name` attribute. The accessor will automatically be called by model when attempting to retrieve the value of the `name` attribute.
```js
import Model from './Model';
export default class Entity extends Model {
declare id: number;
declare name: string;
getNameAttribute(value: number) {
return String(value).toLowerCase();
}
}
```
### Defining a Mutator
A mutator transforms an model attribute value when it is set. To define a mutator, you may provide the `set` argument when defining your attribute. Let's define a mutator for the `first_name` attribute. This mutator will be automatically called when we attempt to set the value of the `first_name` attribute on the model:
```js
import Model from './Model';
export default class Entity extends Model {
declare id: number;
declare name: string;
setFirstNameAttribute(value: number) {
this.attributes[key] = value.toLowerCase();
}
}
```
### Hiding Attributes
Sometimes you may wish to limit the attributes, such as passwords, that are included in your model's array and JSON serializations. To do so, add a `hidden` property to your model. Attributes that are listed in the `hidden` property's array will not be included in the serialized representation of your model:
```js
import config from 'config';
import Model from './Model';
import { Helpers } from '@avonjs/avonjs';
export default class User extends Model {
declare id: number;
declare username: string;
declare email: string;
declare password: string;
public hidden = ['password'];
}
```
Alternatively, you may use the `visible` property to define an "allow list" of attributes that should be included in your model's array and JSON representation. All attributes that are not present in the `visible` array will be hidden when the model is converted to an array or JSON:
```js
import config from 'config';
import Model from './Model';
import { Helpers } from '@avonjs/avonjs';
export default class User extends Model {
declare id: number;
declare username: string;
declare email: string;
declare password: string;
public visible = ['username', 'email'];
}
```
This is essential, for example, when Avon.js records data into action events. By excluding sensitive information like user passwords, you can prevent potential security issues.
## Soft Deletes
In addition to actually removing records from your repository, Avonjs can also "soft delete" records. When records are soft deleted, they are not actually removed from your database. Instead, a `deleted_at` attribute is set on the model indicating the date and time at which the model was "deleted". To enable soft deletes for a repository, extend the base repository by `SoftDeletes` mixins provided by Avon:
```js
const { Repositories, SoftDeletes } = require('@avonjs/avonjs');
const { dirname, join } = require('path');
module.exports = class Categories extends (
SoftDeletes(Repositories.File)
) {
filepath() {
return join(dirname(__dirname), 'storage', 'categories.json');
}
searchableColumns() {
return [];
}
};
```
Soft deletes could apply to all type of repositories and also extends your API by three additional APIs.
## Timestamps
Avon includes the `HasTimestamps` mixins for conveniently setting operation dates on the repository. Here's how you can use it:
```js
const { Repositories, HasTimestamps } = require("@avonjs/avonjs");
module.exports = class Categories extends HasTimestamps(Repositories.File) {
// Your code here
};
```
The `HasTimestamps` mixin can be applied to all types of repositories