0.0.6 • Published 9 months ago

@oniryk/dreamer v0.0.6

Weekly downloads
-
License
MIT
Repository
-
Last release
9 months ago

@oniryk/dreamer

@oniryk/dreamer is a collection of tools, developed over oniryk vision, that helps you build APIs with AdonisJS v6 faster and easier. It includes a code generator and a set of helper functions and abstractions to speed up your development workflow.

What is included:

  • A code generator that creates a full CRUD starting from a migration definition;
  • Abstractions for CRUD actions;
  • Support for UUID as external ID;
  • Support for soft deletes;
  • Default formatting for API responses.

Installation

node ace add @oniryk/dreamer

After installing the package, you will be prompted with questions and a dreamer config file will be generated like this:

import { defineConfig } from '@oniryk/dreamer'

const dreamerConfig = defineConfig({
  useUUID: true,
  useSoftDelete: true,
  bruno: {
    enabled: true,
    documentsDir: '/docs',
    useAuth: true,
  },
})

export default dreamerConfig

Configuring

  • useUUID: when enabled, all models generated by the dreamer command will use uuid as field instead of id in the methods find() and findOrFail() from Lucid. learn more
  • useSoftDelete: when enabled, all models generated by the dreamer command will implement soft deletes. It will add the field deleted_at in the migration file. learn more
  • bruno.enabled: when enabled, will generate bruno files for all routes generated by the dreamer command
  • bruno.documentsDir: specifies where bruno's files will be placed
  • bruno.useAuth: when enabled, will automatically add an Authorization: Bearer ... into the request file.

Code generation

Dreamer has a powerful code generation tool that automates the creation of complete CRUD operations from a single migration file. This streamlines your development process by generating all necessary components with minimal configuration.

Basic Usage

To generate a new CRUD, use the following command:

node ace dreamer [entity]

Replace [entity] with your desired entity name. For example, to create a CRUD for blog posts:

node ace dreamer posts

Workflow

  1. The command creates a migration file
  2. You'll be prompted to edit the file and define your entity fields
  3. After saving, Dreamer automatically generates all CRUD components

Generated Components

Dreamer creates a complete set of files for your entity:

app/
├── models/
│   └── post.ts
├── controllers/
│   └── posts_controller.ts
├── validators/
│   └── post.ts
├── routes/
│   └── posts.ts
└── docs/  # If Bruno is enabled
    └── posts/
        └── index.bru
        └── show.bru
        └── store.bru
        └── update.bru
        └── destroy.bru
  • Model: Lucid model with all field definitions
  • Controller: RESTful actions implementation
  • Validators: Input validation rules for store and update actions
  • Routes: API endpoint definitions
  • API Documentation: Automatically generated if Bruno is enabled in your configuration

Advanced Features

Custom Actions

You can specify which CRUD actions to generate using the --actions flag:

node ace dreamer posts --actions=index,show

Available actions:

  • index (List all)
  • show (View single)
  • store (Create)
  • update (Edit)
  • destroy (Delete)

Nested Resources

Dreamer supports nested resource generation for related entities. This is particularly useful for parent-child relationships:

node ace dreamer posts/comments

Note: Currently, dreamer doesn't have the capacity to determine relationships.

Development Note

The code generated by Dreamer is 100% compatible with AdonisJS 6 development standards. While Dreamer's primary goal is to provide abstractions for common CRUD workflows to speed up development, it's designed to work seamlessly alongside traditional AdonisJS development patterns.

You can:

  • Use Dreamer's abstractions for standard CRUD operations
  • Create custom actions for complex scenarios using regular AdonisJS patterns
  • Mix both approaches in the same controller
  • Extend or override Dreamer's generated code using standard AdonisJS features

For example:

import Post from '#models/post'
import { index, show, destroy } from '@oniryk/dreamer/extensions/crud'

export default class PostsController {
  // Using Dreamer's abstractions for standard operations
  public index = index(Post)
  public show = show(Post)
  public destroy = destroy(Post)

  // Custom action for complex business logic
  public async publish({ params, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)

    await post.merge({
      status: 'published',
      publishedAt: new Date()
    }).save()

    await Event.emit('post:published', post)

    return response.status(200).send(post)
  }
}

This flexibility allows you to leverage Dreamer's convenience while maintaining the freedom to implement custom business logic when needed.

Extensions

The @oniryk/dreamer package provides several extensions to enhance your AdonisJS application with additional functionality. These extensions are designed to be composable and can be used individually or together to extend your models and controllers.

Lucid Extensions

UUID Support

The withUUID extension adds UUID support to your models. It's based on the concept of using UUID as a key to expose externally while keeping an autoincrementing integer as the primary key.

import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { withUUID } from '@oniryk/dreamer/extensions/lucid'

export default class Post extends compose(BaseModel, withUUID()) {
  //...
}

What's changed under the hood?

  • id column keeps existing as primary key to speed up relationship queries
  • uuid column is defined and autogenerates UUIDs for new records
  • changes the default behavior of find and findOrFail methods to use the uuid column instead of id when making queries

Soft-delete Support

The withSoftDelete extension implements soft delete functionality in your models:

  • Adds a deletedAt timestamp column to your model
  • Automatically filters out soft-deleted records from queries
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { withSoftDelete } from '@oniryk/dreamer/extensions/lucid'

export default class Post extends compose(BaseModel, withSoftDelete()) {
  // ...
}

Searchable Fields

The searchable fields feature allows you to define which fields can be searched in your models:

  • Define exact match fields (e.g., 'author_id')
  • Define partial match fields using the 'like:' prefix (e.g., 'like:title')
  • Automatically handles search queries in the CRUD index operation
  • Supports multiple search criteria in a single query
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { withSoftDelete } from '@oniryk/dreamer/extensions/lucid'

export default class Post extends BaseModel {
  public static searchable = ['author_id', 'like:title']
}

CRUD

The package provides pre-built CRUD operations that can be easily integrated into your controllers. All operations take a model as the first argument and offer some options depending on your functionality.

This is a basic example of a complete RESTful controller:

import Post from '#models/post'
import { index, show, store, update, destroy } from '@oniryk/dreamer/extensions/crud'
import { validatePostCreate, validatePostUpdate } from '#validators/post'
import csv from '@oniryk/dreamer-csv'

export default class PostsController {
  public index = index(Post)
  public show = show(Post)
  public store = store(Post, validatePostCreate)
  public update = update(Post, validatePostUpdate)
  public destroy = destroy(Post)
}

index

The index method provides a flexible way to list and filter records.

import { index } from '@oniryk/dreamer/extensions/crud'
import csv from '@oniryk/dreamer-csv'
import { validatePostIndex } from '#validators/post'

export default class PostsController {
  public index = index(Post, {
    perPage: 20,
    formats: [csv()],
    scope: 'highlights',
    validator: validatePostIndex
  })
}
OptionTypeDescription
perPagenumber(optional) Number of records per page
formatsOutputFormatFn[](optional) Array of formatters to enable alternative output formats. When a format is added, the user can request the content in a format by passing f or format in the query string:Ex: GET /posts?f=csv
scopestring | function(optional) Name of model scope to apply or function compatible with withScopes method of Lucid query builderEx: (scopes) => scopes.highlights()
validatorVineValidator(optional) Vine validation schema for query parameters

show

The show method provides a way to retrieve a single record. When using UUID extension, it automatically handles UUID-based lookups.

import Post from '#models/post'
import { show } from '@oniryk/dreamer/extensions/crud'

export default class PostsController {
  public show = show(Post)
}
OptionTypeDescription
modelBaseModelThe Lucid model class

store

The store method handles record creation with validation and optional data mutation.

import Post from '#models/post'
import { store } from '@oniryk/dreamer/extensions/crud'
import { validatePostUpdate } from '#validators/post'

export default class PostsController {
  public store = store(Post, validatePostUpdate, {
    mutate (row, payload) {
      row.title = payload.title.toLowerCase()
    }
  })
}
ParameterTypeDescription
modelBaseModelThe Lucid model class
validatorVineValidatorVine validator schema for input validation
options.mutate(row: Model, payload: any) => void | Promise(optional) Callback to modify data before saving

update

The update method handles record updates with validation and optional data mutation.

import Post from '#models/post'
import { update } from '@oniryk/dreamer/extensions/crud'
import { validatePostUpdate } from '#validators/post'

export default class PostsController {
  public update = update(Post, validatePostUpdate, {
    mutate (row, payload) {
      row.title = payload.title.toLowerCase()
    }
  })
}
ParameterTypeDescription
modelBaseModelThe Lucid model class
validatorVineValidatorVine validator schema for input validation
options.mutate(row: Model, payload: any) => void | Promise(optional) Callback to modify data before saving

destroy

The destroy method handles record deletion with proper error handling.

import Post from '#models/post'
import { destroy } from '@oniryk/dreamer/extensions/crud'

export default class PostsController {
  public destroy = destroy(Post)
}
ParameterTypeDescription
modelBaseModelThe Lucid model class

JSON Response Formatters

JSON response formatters provide a consistent way to structure your API responses. They help maintain a uniform pattern for success and error across all your routes.

success

The success method formats successful responses, supporting both simple and paginated data. It automatically structures the response with an "ok" status and includes the provided data, along with pagination metadata when applicable.

Example 1: Simple List Response

import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'
import { success } from '@oniryk/dreamer/extensions/http'

export default class PostsController {
  public async list({ response }: HttpContext) {
    const posts = await Post.all()
    success(response, posts)
  }
}

// Response:
{
  "status": "ok",
  "data": [ ... ]
}

Example 2: Paginated Response

export default class PostsController {
  public async paginated({ response, request }: HttpContext) {
    const page = request.input('page', 1);
    const limit = 20;
    const posts = await Post.paginate(page, limit)
    success(response, posts)
  }
}

// Response:
{
  "status": "ok",
  "data": [ ... ],
  "meta": {
    "currentPage": 1,
    "firstPage": 1,
    "firstPageUrl": "/?page=1",
    "lastPage": 1,
    "lastPageUrl": "/?page=1",
    "nextPageUrl": null,
    "perPage": 10,
    "previousPageUrl": null,
    "total": 6
  }
}

error

The error method standardizes error handling in the API, providing a consistent structure for different types of errors. It can handle validation errors, custom errors, and standard system exceptions.

Example 1: Validation Error Handling

import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'
import { error } from '@oniryk/dreamer/extensions/http'
import { validatePostCreate } from '#validators/post'

export default class PostsController {
  public async store({ response, request }: HttpContext) {
    try {
      await request.validate(validatePostCreate)
    } catch (e) {
      error(response, e);
    }
  }
}

// Response:
{
  "status": "error",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failure",
    "issues": [
       {
         "field": "title",
         "message": "The title field must be defined",
         "rule": "required"
       }
    ]
  }
}

Example 2: Standard Error Response

export default class PostsController {
  public async check ({ response, request }: HttpContext) {
    if (1 === 1) {
      error(response, new Error('invalid option'));
    }
  }
}

// Response:
{
  "status": "error",
  "error": {
    "code": "Error",
    "message": "invalid option"
  }
}

Example 3: Custom Error Response

export default class PostsController {
  public async check2 ({ response, request }: HttpContext) {
    if (1 === 1) {
      error(response, { code: 'ERROR_CODE', message: 'invalid option'});
    }
  }
}

// Response:
{
  "status": "error",
  "error": {
    "code": "ERROR_CODE",
    "message": "invalid option"
  }
}

Output formatters

You may want to deliver a response in a specific file format like csv, as you can see in the index action from the CRUD abstraction extension.

Dreamer has optional built-in formatters for csv and xlsx. You can install and use them as needed. They come in two separate packages: @oniryk/dreamer-csv and @oniryk/dreamer-xls.

You can also create your own formatter. It must implement the following type:

type OutputFormatFn<M extends typeof BaseModel> = {
  (ctx: HttpContext, rows: InstanceType<M>[]): Promise<void> | void
  formatName: string
}

Let's create a new example formatter:

export default function pdf({ name }: { name: string }) {
  const handler = async function ({ response }: HttpContext, rows: unknown[]) {
    const content = await convertToPdf(rows); // imaginary function

    response.header("Content-Type", "application/pdf");
    response.header("Content-Disposition", `attachment; filename="${name}"`);
    response.send(content);
  };

  handler.formatName = "pdf";
  return handler;
}

Using our new formatter:

export default class PostsController {
  public index = index(Post, {
    formats: [
      pdf({ name: 'posts.pdf' })
    ]
  })
}
0.0.5

9 months ago

0.0.6

9 months ago

0.0.4

9 months ago

0.0.3

9 months ago

0.0.2

9 months ago

0.0.1

9 months ago