npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@avonjs/avonjs

v3.0.2

Published

A fluent Node.js API generator.

Downloads

249

Readme

Installation

Resources

Repositories

Filters

Orderings

Actions

Activity Log

Error Handling

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:

npm install @avonjs/avonjs

via yarn:

yarn install @avonjs/avonjs

To develop fast in Avonjs you have to install the Avonjs CLI:

via npm:

npm install @avonjs/cli -g

via yarn:

yarn install @avonjs/cli -g

Initialize

At first point you have to register the router:

// 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')
})

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:

// register Avonjs router
app.use('/api', Avon.express(true));

By default, Avonjs using the 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:

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:

Avon.credentials([new Fields.Email().default(() => '[email protected]')]);

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:

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 package.

Also you can change the secret key uses during sign options by the Avonjs appKey:

// 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:

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:

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:

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:

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 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:

{
    ...
    "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:

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:

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 to our resource soon and you'll learn more about 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:

// 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 that enables you to use the swagger-ui for documentation. here we using docker to install swagger-ui. let's do it:

first run the following command to install swagger-ui:

docker pull swaggerapi/swagger-ui

now we use the previously created URL for schema to run docker:

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 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:

/**
* 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:

// 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:

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:

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:

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:

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:

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:

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

The Array field pairs nicely with model attributes that are cast to array or equivalent:

import { Fields } from '@avonjs/avonjs';

new Fields.Array('tags')

You may restrict the length of an array with the min and max methods:

new Fields.Array('title').min(1).max(120)

Alternatively, you can set a specific length:

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:

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.

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 formatting.

Email Field

The Email field may be used to store a email value.

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:

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:

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:

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:

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:

new Fields.Integer('price').min(1).max(10000)

Decimal Field

The Decimal field store / retrieve value as float in the model:

import { Fields } from '@avonjs/avonjs';

new Fields.Decimal('price')

The precision method helps you to specify the maximum number of decimal places:

new Fields.Decimal('price').precision(2)

Text Field

The Text field store / retrieve value as string in the model:

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

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:

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:

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:

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:

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:

new Fields.Text('name').filterable()

Also its possible to passing a callback to customize the filtering behavior:

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:

new Fields.Text('name').orderable()

Also its possible to passing a callback to customize the ordering behavior:

new Fields.Text('name').orderable((request, repository, direction) => {
  repository.order({
    key: 'another key',
    direction: 'desc',
  });
})

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

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.

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:

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:

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:

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:

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:

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:

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:

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:

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.

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

/**
* 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 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:

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:

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:

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:

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:

/**
* 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:

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:

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:

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:

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:

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:

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:

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:

avonjs make:repository Posts

or with soft deletes:

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 provided later in the documentation.

Preset Repositories

Avon by default, ships with a variety of repositories:

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:

avonjs make:repository Posts --collection

or with soft deletes:

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.

avonjs make:repository Posts --file

or with soft deletes:

avonjs make:repository Posts --file --soft-deletes

Knex Repository

The Knex repository stores data on the database and uses knex.js package to maintain data. you could use it like so:

avonjs make:repository Posts --knex

or with soft deletes:

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:

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!

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:

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.

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:

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:

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:

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:

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:

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 to automatically set "created_at" and "updated_at" dates on the models when they're not set yet.

To customize the timestamps columns, you can override the getCreatedAtKey and getUpdatedAtKey methods. For example:

class MyRepository extends HasTimestamps(BaseRepository) {
  getCreatedAtKey() {
    return "my_created_at_column";
  }

  getUpdatedAtKey() {
    return "my_updated_at_column";
  }
}

To modify the timestamps' values, you can change the freshTimestamp method according to your requirements. This method is responsible for providing the current timestamp when creating or updating records.

Filters

Defining Filters

To create a filter you can use the Avonjs make:filter command like so:

avonjs make:filter ActivePosts

By default, Avonjs will place newly generated filters in the src/avonjs/filters directory. Each filter is a class that extended the base class filters and contains an apply method to modifying underlying repository query:

// filters/ActivePosts.js 
import { Filters, Constants } from '@avonjs/avonjs';

export class ActivePosts extends Filters.Filter {
  /**
  * Apply the filter into the given repository.
  */
  apply(request, repository, value) {
    return repository.where({
      key: 'state',
      value: 'active',
      operator: Constants.Operator.eq
    })
  }
}

Select Filter

The most common type of Avonjs filter is the "select" filter, which allows the user to select a filter option from a drop-down selection menu on the swagger-ui. You may generate a select filter using the make:filter Avonjs command.

avonjs make:filter ActivePosts --select

Each SelectFilter should have the options method that defines the "values" the filter may have.

// Filters/FilterByRoles.js 
import { Filters } from '@avonjs/avonjs';

export class FilterByRoles extends Filters.Select {
  /*
  * Apply the filter into the given repository.
  */
  apply(request, repository, value) {
    // modify query
  }

  /**
  * Get the possible filtering values.
  */
  public options(): AnyArray {
    return ['admin', 'user'];
  }
}

Boolean Filter

The Avonjs "boolean" filters, allow the user to determine a filter should apply on the resource or not. . You may generate a select filter using the make:filter Avonjs command:

avonjs make:filter ActivePosts --select

Range Filter

The "range" filters allow the user to chose records that has a value between a specific range. to create a range filter you may use the make:filter Avonjs command:

avonjs make:filter FilterByHits --range

Registering Filters

Once you have defined a filter, you are ready to attach it to a resource. Each resource created by Avonjs contains a filters method. To attach a filter to a resource, you should simply add it to the array of filters returned by this method:

/**
* Get the filters available on the entity.
*/
public filters(request: AvonRequest): Filter[] {
  return [
    new ActivePosts(),
  ];
}

After attaching a filter to the resource, the filter will appear in the swagger-ui index API.

Authorization Filters

If you need to limit the user to run filters, the canSee method gives a function that receive the current request that should return true or false to determine user can use the filter or not. if an restricted filter appear in the request, the Avonjs will ignore it:

new FilterByHits().canSee((request) => false)

Orderings

Defining Orderings

Avon orderings are simple classes that allow you to order your Avonjs index queries with custom conditions.

  • Before creating your own orderings, you may want to check out orderable fields. orderable fields can solve the ordering needs of most Avonjs installations without the need to write custom code.

To create a ordering you have to use make:ordering Avonjs command like so:

avonjs make:ordering OrderByFullName

By default, Avonjs will place newly generated orderings in the avonjs/orderings directory. Each "ordering" is a class that extended the base class orderings and contains an apply method to modifying underlying repository query:

// Orderings/OrderByFullName.js
import { Orderings } from '@avonjs/avonjs';

export class OrderByFullName extends Orderings.Ordering {
  /**
  * Apply the ordering into the given repository.
  */
  apply(request, repository, direction) {
    // modify query
  }
}

Registering Orderings

Once you have defined a ordering, you are ready to attach it to a resource. Each resource created by Avonjs contains a orderings method. To attach a ordering to a resource, you should simply add it to the array of orderings returned by this method:

/**
* Get the orderings available on the entity.
*/
public orderings(request: AvonRequest): Ordering[] {
  return [
    new OrderByFullName(),
  ];
}

After attaching a ordering to the resource, the ordering will appear in the swagger-ui index API.

Authorization Orderings

If you need to limit the user to run orderings, the canSee method gives a function that receive the current request that should return true or false to determine user can use the ordering or not. if an restricted ordering appear in the request, the Avonjs will ignore it:

new OrderingByHits().canSee((request) => false)

Actions

Defining Actions

Avon actions allow you to perform custom tasks on one or more resource records. For example, you might write an action that sends an email to a user containing account data they have requested. Or, you might write an action to transfer a group of records to another user.

Once an action has been attached to a resource definition, you can see extra API on the swagger-ui. to create an action you have to use the make:action Avonjs command:

avonjs make:action Publish

The most important method of an action is the handle method. The handle method receives the values for any fields attached to the action, as well as a collection of selected models. The handle method always receives a Collection of models, even if the action is only being performed against a single model.

Within the handle method, you may perform whatever tasks are necessary to complete the action. You are free to update database records, send emails, call other services, etc. The sky is the limit!

Action Fields

Sometimes you may wish to gather additional information from the user before dispatching an action. For this reason, Avonjs allows you to attach most of Avon's supported fields directly to an action. To add a field to an action, add the field to the array of fields returned by the action's fields method:

/**
* Get the fields available on the action.
*/
public fields(request: AvonRequest): Field[] {
    return [];
}

Action Responses

Typically, when an action is executed, a "success" response will create by Avon. However, you are free to return your custom response:

import { AvonResponse } from "avonjs";

/**
*Perform the action on the given models.
*/
protected async handle(fields: Fluent, models: Model[]): Promise<AvonResponse | undefined> {
    // perform action

    return new (class extends AvonResponse {
        constructor(meta = {}) {
            super(201, {}, { ...meta, type: 'PublishedResponse is my custom response'});
        }
    })();
}

Registering Actions

Once you have defined an action, you are ready to attach it to a resource. Each resource created by Avonjs contains an actions method. To attach an action to a resource, you should simply add it to the array of actions returned by this method:

/**
* Get the actions available on the entity.
*/
actions(request){
  return [
      new Publish()
  ];
}

Authorization Actions

If you would like to only expose a given action to certain users, you may invoke the canSee method when registering your action. The canSee method accepts a function which should return true or false. The function will receive the incoming HTTP request:

new Publish().canSee(request => false)

Sometimes a user may be able to "run" that an action exists but only against certain resources. you may use the canRun method in conjunction with the canSee method to have full control over authorization in this scenario. The callback passed to the canRun method receives the incoming HTTP request and the resource model:

new Publish().canRun((request, model) => model.getKey() % 2 === 0)

Standalone Actions

Typically, actions are executed against resources selected on a resource index or detail API. However, sometimes you may have an action that does not require any resources / models to run. In these situations, you may register the action as a "standalone" action by invoking the standalone method when registering the action. These actions always receives an empty collection of models in their handle method:

/**
* Get the actions available on the entity.
*/
actions(request){
  return [
    new Publish().canRun(request => false)
  ];
}

Activity Log

Action Events

It is often useful to view a log of the actions that have been run against a particular resource. Thankfully, Avonjs stores any actions that manipulate records. but by default, we store the logs on the memory, so data will be lost after restarts or any memory cleanups. to prevent losing data, you could define your custom repository for action events per each resource:

const { Resource } = require('@avonjs/avonjs');
const Activities = require('../repositories/Activities');

abstract class BaseResource extends Resource {
  actionRepository() {
    return new Activities();
  }
}

Sanitizing Event Records

Since Avon.js stores the request payload when recording action events, some payloads may contain sensitive data like user passwords. To prevent this data from being stored in event records, each resource includes a sanitizePayload method to prepare data for safe recording during create or update actions:

sanitizePayload(payload: Contracts.Payload) {
  return { ...payload, password: '*************' };
}

This method allows you to mask or remove sensitive information before it is stored, enhancing data security and privacy.

Custom Action Event

All action events repository should implements ActionEventRepository interface on the typescript. Fortunately, Avonjs has a mixin to help you make custom action event repositories without any trouble. To define your custom action event repository you could use FillsActionEvents mixin to extends your repository like so:

const { Repositories, FillsActionEvents } = require('@avonjs/avonjs');
const { dirname, join } = require('path');

class Activities extends FillsActionEvents(Repositories.File) {
  filepath() {
    return join(dirname(\_\_dirname), 'storage', 'activities.json');
  }
  searchableColumns() {
    return [];
  }
};

Action Event Table

The action events table structure should be defined as follows:

import type { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTableIfNotExists('action_events', (table) => {
    table.bigIncrements('id').unsigned().primary();
    table.string('name');
    table.string('model_type');
    table.bigInteger('model_id');
    table.string('resource_name');
    table.bigInteger('resource_id');
    table.bigInteger('user_id');
    table.string('batch_id');
    table.json('payload').nullable();
    table.json('changes').nullable();
    table.json('original').nullable();
    table.timestamps(true, true);
    table.timestamp('deleted_at').nullable();
    // Indexes
    table.index(['model_type', 'model_id'], 'action_event_morphs');
  });
}

export async function down(knex: Knex): Promise<void> {
  return knex.schema.dropTableIfExists('action_events');
}

Error Handling

Register Error Handler

The handleErrorUsing on the Avonjs allows you to register a custom callback to handle errors:

Avon.handleErrorUsing((error) => console.error(error))