@oniryk/dreamer v0.0.6
@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 useuuid
as field instead of id in the methodsfind()
andfindOrFail()
from Lucid. learn more - useSoftDelete: when enabled, all models generated by the
dreamer
command will implement soft deletes. It will add the fielddeleted_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
- The command creates a migration file
- You'll be prompted to edit the file and define your entity fields
- 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 queriesuuid
column is defined and autogenerates UUIDs for new records- changes the default behavior of
find
andfindOrFail
methods to use theuuid
column instead ofid
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
})
}
Option | Type | Description |
---|---|---|
perPage | number | (optional) Number of records per page |
formats | OutputFormatFn[] | (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 |
scope | string | function | (optional) Name of model scope to apply or function compatible with withScopes method of Lucid query builderEx: (scopes) => scopes.highlights() |
validator | VineValidator | (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)
}
Option | Type | Description |
---|---|---|
model | BaseModel | The 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()
}
})
}
Parameter | Type | Description |
---|---|---|
model | BaseModel | The Lucid model class |
validator | VineValidator | Vine 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()
}
})
}
Parameter | Type | Description |
---|---|---|
model | BaseModel | The Lucid model class |
validator | VineValidator | Vine 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)
}
Parameter | Type | Description |
---|---|---|
model | BaseModel | The 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' })
]
})
}