Skip to content

CRUD Operations

Lucid Resourceful provides a comprehensive set of CRUD (Create, Read, Update, Delete) operations through static methods on models that have been enhanced with the withResourceful mixin. These operations include built-in access control, validation, query scoping, and field-level security features.

Overview

Once a model is decorated with the resourceful mixin, it gains access to five primary CRUD methods:

All CRUD operations support:

  • Access Control Lists (ACL) - Both model-level and field-level permission checking
  • Query Scoping - Automatic filtering based on request context
  • Validation - Payload validation using Joi schemas
  • Error Handling - Comprehensive error responses with specific error types
  • Field Filtering - Response contains only fields the user has read access to

Core Parameters

All CRUD methods share common parameters:

  • ctx: HttpContext - AdonisJS HTTP context containing request information and authentication
  • app: ApplicationService - Application service instance for accessing app-level services
  • hooks?: Array - Optional callback functions for additional query scoping or validation

List Records ($onResourcefulIndex)

The index operation provides paginated listing and searching capabilities with comprehensive filtering support.

Method Signature

typescript
static async $onResourcefulIndex<SelectedFields, ReturnType>(
  filter: string | null | undefined,
  page: number,
  perPage: number,
  fields: SelectedFields | null | undefined,
  ctx: HttpContext,
  app: ApplicationService,
  hooks?: ResourcefulScopeHooks
): Promise<ResourcefulIndexResult<ReturnType>>

Parameters

ParameterTypeDescription
filterstring | null | undefinedLucene-style query string for filtering records (e.g., "name:john AND email:*.com"). If null or undefined, defaults to empty string (no filtering).
pagenumberThe page number for pagination (must be ≥ 1). Used with perPage to calculate offset.
perPagenumberNumber of records per page (must be ≥ 1 and ≤ 100 by default).
fieldsSelectedFields | null | undefinedArray of field names to include in the response. If null, undefined, or empty, defaults to just the primary key field. Field names should use serialized names (as they appear in API responses), not database column names.
ctxHttpContextHTTP context containing request information, authentication, and other request-scoped data.
appApplicationServiceApplication service instance providing access to application-level services and configuration.
hooks?ResourcefulScopeHooksOptional array of query scope callbacks to apply additional filtering constraints.

Return Value

Returns a Promise<ResourcefulIndexResult<ReturnType>> containing:

  • records - Array of partial record objects with only the requested fields
  • total - Total number of records matching the filter (before pagination)
  • page - The requested page number (echoed back)
  • perPage - The requested per-page limit (echoed back)
  • countQuery - SQL query string used for counting total records
  • recordsQuery - SQL query string used for fetching the actual records

Filter Syntax

The filter parameter supports Lucene-style query syntax. For complete syntax documentation, see the official Lucene query parser syntax documentation.

Supported Features:

  • Field queries: name:john, email:user@example.com
  • Wildcards: name:jo*, email:*@example.com
  • Boolean operators: name:john AND email:*.com, status:active OR status:pending
  • Date ranges: createdAt:[2021-01-01T00:00:00Z TO 2021-12-31T23:59:59Z]
  • Grouping: (name:john OR name:jane) AND status:active

Unsupported Features:

The following Lucene features are not supported by the underlying query parser:

  • Fuzzy Searches - Terms like name:john~ for approximate matching
  • Proximity Searches - Phrases like "john doe"~10 for word proximity
  • Boosting a Term - Terms like name:john^2 for relevance boosting

Usage Examples

typescript
// Basic pagination
const result = await User.$onResourcefulIndex(
  'name:john',
  1,
  10,
  ['id', 'name', 'email'],
  ctx,
  app
);

// Complex filtering with date ranges  
const result = await User.$onResourcefulIndex(
  'name:john AND createdAt:[2021-01-01T00:00:00Z TO 2021-12-31T23:59:59Z]',
  2,
  25,
  ['id', 'name', 'createdAt'],
  ctx,
  app
);

// With additional query scoping
const result = await User.$onResourcefulIndex(
  'status:active',
  1,
  20,
  ['id', 'name'],
  ctx,
  app,
  [(ctx, app, query) => query.where('tenant_id', ctx.auth.user.tenantId)]
);

Thrown Exceptions

  • E_MISSING_PRIMARY_KEY_EXCEPTION - When the model has no identifiable primary key
  • E_FORBIDDEN - When access is denied by model-level or field-level ACL filters
  • E_INVALID_COLUMN_ACCESS - When no fields are available for access after ACL filtering
  • E_INVALID_RESOUREFUL_INDEX_REQUEST_EXCEPTION - When input validation fails

API Reference

For complete type definitions and additional details, see the ResourcefulModel.$onResourcefulIndex API documentation.

Read Single Record ($onResourcefulRead)

Retrieves a single model record by its unique identifier with comprehensive access control.

Method Signature

typescript
static async $onResourcefulRead(
  uid: number,
  ctx: HttpContext,
  app: ApplicationService,
  hooks?: ResourcefulScopeHooks
): Promise<ResourcefulModelSerializableAttributes>

Parameters

ParameterTypeDescription
uidnumberThe unique identifier of the record to retrieve
ctxHttpContextHTTP context containing request information and authentication
appApplicationServiceApplication service instance for accessing app-level services
hooks?ResourcefulScopeHooksOptional array of query scope callbacks to apply additional filtering constraints

Return Value

Returns a Promise<ResourcefulModelSerializableAttributes> containing the record object with only accessible fields based on ACL permissions.

Access Control Flow

  1. Query Scoping - Applies configured query scope callbacks to verify record exists within user's access scope
  2. Record Retrieval - Fetches the full model instance for field-level ACL evaluation
  3. Field-Level ACL - Evaluates read permissions for each field
  4. Response Filtering - Returns only fields that pass ACL checks

Usage Examples

typescript
// Retrieve a user by ID
const user = await User.$onResourcefulRead(123, ctx, app);

// With additional query scoping
const user = await User.$onResourcefulRead(123, ctx, app, [
  (ctx, app, query) => query.where('tenant_id', ctx.auth.user.tenantId)
]);

// Result contains only fields the current user can read
console.log(user); // { id: 123, name: "John Doe" } - email field filtered out by ACL

Thrown Exceptions

  • E_MISSING_PRIMARY_KEY_EXCEPTION - When the model has no identifiable primary key
  • E_RECORD_NOT_FOUND_EXCEPTION - When no record exists with the given ID or user lacks access
  • E_FORBIDDEN - When access is denied by model-level or field-level ACL filters

API Reference

For complete type definitions and additional details, see the ResourcefulModel.$onResourcefulRead API documentation.

Create New Record ($onResourcefulCreate)

Creates a new model record with payload validation and comprehensive access control.

Method Signature

typescript
static async $onResourcefulCreate(
  payload: any,
  ctx: HttpContext,
  app: ApplicationService,
  hooks?: ResourcefulValidationHooks
): Promise<ResourcefulModelSerializableAttributes>

Parameters

ParameterTypeDescription
payloadanyThe data object containing field values for the new record
ctxHttpContextHTTP context containing request information and authentication
appApplicationServiceApplication service instance for accessing app-level services
hooks?ResourcefulValidationHooksOptional array of validation schema getters for additional payload validation

Return Value

Returns a Promise<ResourcefulModelSerializableAttributes> containing the created record with only accessible fields (uses $onResourcefulRead internally for consistent field filtering).

Validation & Security Flow

  1. Access Control - Checks model-level create permissions
  2. Payload Validation - Validates against both model-level and request-specific schemas
  3. Field-Level ACL - Verifies write permissions for each field in the payload
  4. Record Creation - Creates the new record with validated data
  5. Response Filtering - Returns created record via $onResourcefulRead for consistent ACL application

Usage Examples

typescript
// Create a new user
const user = await User.$onResourcefulCreate(
  {
    name: "John Doe",
    email: "john@example.com"
  },
  ctx,
  app
);

// With additional validation
const user = await User.$onResourcefulCreate(
  payload,
  ctx,
  app,
  [
    (ctx, app) => joi.object({ 
      email: joi.string().domain('company.com') 
    })
  ]
);

// Fields without write access are ignored, forbidden fields throw errors
const restrictedUser = await User.$onResourcefulCreate(
  {
    name: "Jane Doe",
    email: "jane@example.com",
    isAdmin: true  // This field may be filtered out or cause an error based on ACL
  },
  ctx,
  app
);

Validation Hooks

The hooks parameter accepts an array of validation schema getter functions:

typescript
type ResourcefulValidationHooks = Array<
  (ctx: HttpContext, app: ApplicationService) => Schema | null
>

These functions are called with the current context and can return additional Joi validation schemas that will be applied to the payload.

Thrown Exceptions

  • E_MISSING_PRIMARY_KEY_EXCEPTION - When the model has no identifiable primary key
  • E_INVALID_PAYLOAD_EXCEPTION - When core model validation fails
  • E_FORBIDDEN_PAYLOAD_EXCEPTION - When request-specific validation fails
  • E_FORBIDDEN - When access is denied by model-level or field-level ACL filters

API Reference

For complete type definitions and additional details, see the ResourcefulModel.$onResourcefulCreate API documentation.

Update Existing Record ($onResourcefulUpdate)

Updates an existing model record with payload validation and access control.

Method Signature

typescript
static async $onResourcefulUpdate(
  uid: number,
  payload: any,
  ctx: HttpContext,
  app: ApplicationService,
  hooks?: Partial<ResourcefulHooks>
): Promise<ResourcefulModelSerializableAttributes>

Parameters

ParameterTypeDescription
uidnumberThe unique identifier of the record to update
payloadanyThe data object containing field values to update
ctxHttpContextHTTP context containing request information and authentication
appApplicationServiceApplication service instance for accessing app-level services
hooks?Partial<ResourcefulHooks>Optional object containing query scope callbacks and validation schema getters

Hooks Parameter

The hooks parameter accepts an object with the following optional properties:

typescript
{
  queryScopeCallbacks?: ResourcefulScopeHooks,
  payloadValidationSchemas?: ResourcefulValidationHooks
}
  • queryScopeCallbacks - Additional query scoping for record access verification
  • payloadValidationSchemas - Additional validation schemas for the update payload

Return Value

Returns a Promise<ResourcefulModelSerializableAttributes> containing the updated record with only accessible fields.

Update Flow

  1. Record Verification - Uses $onResourcefulRead to verify record exists and user has access
  2. Payload Validation - Validates update payload against model and request-specific schemas
  3. Field-Level ACL - Checks write permissions for each field being updated
  4. Record Update - Applies validated changes to the existing record
  5. Response Filtering - Returns updated record via $onResourcefulRead for consistent field filtering

Usage Examples

typescript
// Update a user
const user = await User.$onResourcefulUpdate(
  123,
  {
    name: "Jane Doe"
  },
  ctx,
  app
);

// With additional scoping and validation
const user = await User.$onResourcefulUpdate(
  123,
  payload,
  ctx,
  app,
  {
    queryScopeCallbacks: [
      (ctx, app, query) => query.where('active', true)
    ],
    payloadValidationSchemas: [
      (ctx, app) => customValidationSchema
    ]
  }
);

// Partial updates - only provided fields are updated
const partialUpdate = await User.$onResourcefulUpdate(
  123,
  { name: "Updated Name" },  // Only name field will be updated
  ctx,
  app
);

Thrown Exceptions

  • E_MISSING_PRIMARY_KEY_EXCEPTION - When the model has no identifiable primary key
  • E_RECORD_NOT_FOUND_EXCEPTION - When no record exists with the given ID or user lacks access
  • E_INVALID_PAYLOAD_EXCEPTION - When core model validation fails
  • E_FORBIDDEN_PAYLOAD_EXCEPTION - When request-specific validation fails
  • E_FORBIDDEN - When access is denied by model-level or field-level ACL filters

API Reference

For complete type definitions and additional details, see the ResourcefulModel.$onResourcefulUpdate API documentation.

Delete Record ($onResourcefulDelete)

Deletes an existing model record with access control verification.

Method Signature

typescript
static async $onResourcefulDelete(
  uid: number,
  ctx: HttpContext,
  app: ApplicationService,
  hooks?: ResourcefulScopeHooks
): Promise<void>

Parameters

ParameterTypeDescription
uidnumberThe unique identifier of the record to delete
ctxHttpContextHTTP context containing request information and authentication
appApplicationServiceApplication service instance for accessing app-level services
hooks?ResourcefulScopeHooksOptional array of query scope callbacks to apply additional filtering constraints

Return Value

Returns a Promise<void> that resolves when the record has been successfully deleted.

Deletion Flow

  1. Query Scoping - Applies configured query scope callbacks to verify record exists within user's access scope
  2. Record Verification - Fetches the record to confirm it exists and is accessible
  3. Access Control - Checks delete permissions via model-level ACL filters
  4. Record Deletion - Removes the record from the database

Usage Examples

typescript
// Delete a user
await User.$onResourcefulDelete(123, ctx, app);

// With additional query scoping
await User.$onResourcefulDelete(123, ctx, app, [
  (ctx, app, query) => query.where('tenant_id', ctx.auth.user.tenantId)
]);

// Delete with confirmation
try {
  await User.$onResourcefulDelete(userId, ctx, app);
  console.log('User deleted successfully');
} catch (error) {
  if (error instanceof E_RECORD_NOT_FOUND_EXCEPTION) {
    console.log('User not found or access denied');
  } else if (error instanceof E_FORBIDDEN) {
    console.log('Insufficient permissions to delete user');
  } else {
    throw error;
  }
}

Security Considerations

  • No Field-Level ACL - Delete operations only check model-level permissions, not individual field permissions
  • Query Scoping - Records outside the user's query scope are treated as non-existent
  • Soft Deletes - If the model uses Lucid's soft delete features, this method respects that configuration

Thrown Exceptions

  • E_MISSING_PRIMARY_KEY_EXCEPTION - When the model has no identifiable primary key
  • E_RECORD_NOT_FOUND_EXCEPTION - When no record exists with the given ID or user lacks access
  • E_FORBIDDEN - When access is denied by model-level ACL filters

API Reference

For complete type definitions and additional details, see the ResourcefulModel.$onResourcefulDelete API documentation.

Access Control and Security

All CRUD operations implement comprehensive security through multiple layers:

Model-Level Access Control

Configured via the accessControlFilters option in the withResourceful mixin:

typescript
class User extends withResourceful({
  accessControlFilters: {
    list: [(ctx) => ctx.auth.user?.isActive],
    read: [(ctx, app, instance) => ctx.auth.user?.id === instance.id],
    create: [(ctx) => ctx.auth.user?.canCreateUsers],
    update: [(ctx, app, instance) => ctx.auth.user?.id === instance.id],
    delete: [(ctx, app, instance) => ctx.auth.user?.isAdmin]
  }
})(BaseModel) {
  // Model definition...
}

Field-Level Access Control

Defined per-field using the @resourceful decorator:

typescript
@column()
@resourceful({
  type: 'string',
  nullable: false,
  readAccessControlFilters: [(ctx) => ctx.auth.user?.isAdmin],
  writeAccessControlFilters: [(ctx) => ctx.auth.user?.isOwner]
})
public sensitiveField: string

Query Scoping

Automatically constrains database queries based on request context:

typescript
class User extends withResourceful({
  queryScopeCallbacks: {
    list: [(ctx, app, query) => query.where('tenant_id', ctx.auth.user.tenantId)],
    access: [(ctx, app, query) => query.where('active', true)]
  }
})(BaseModel) {
  // Model definition...
}

Validation System

CRUD operations support multiple validation layers. See the Validation documentation for comprehensive details on:

  • Model-level validation using resourceful decorators
  • Request-specific validation via hooks parameters
  • Custom validation schemas with Joi integration
  • Validation error handling and response formatting

Error Handling

All CRUD operations throw specific error types for different failure scenarios. See the Error Handling documentation for complete details on:

  • Error types and inheritance hierarchy
  • Type-safe error checking using isInstanceOf type guards
  • Error response formatting and HTTP status mapping
  • Best practices for error handling in applications

OpenAPI Schema Integration

Models with resourceful CRUD operations can generate OpenAPI schemas that reflect the actual accessible fields based on request context. See the OpenAPI documentation for details on:

  • Context-aware schema generation using $asOpenApiSchemaObject
  • Field filtering based on ACL permissions
  • Type mapping from resourceful decorators to OpenAPI types
  • API documentation generation for frontend and testing tools

Performance Considerations

Query Optimization

  • Field Selection - The fields parameter in $onResourcefulIndex reduces database load by selecting only required columns
  • Pagination - Built-in pagination prevents memory issues with large result sets
  • Query Scoping - Database-level filtering is more efficient than application-level filtering

ACL Evaluation

  • Concurrent Processing - ACL filters are evaluated with limited concurrency to balance performance and early termination
  • Caching Strategy - No built-in caching due to security implications; implement application-level caching as needed
  • Error Handling - ACL failures can be configured to halt immediately or allow graceful degradation

Validation Performance

  • Schema Compilation - Joi schemas are compiled once and reused for better performance
  • Validation Scope - Only fields being modified are validated during updates
  • Early Termination - Validation stops on first error to minimize processing time

Best Practices

Security

  1. Always use HTTPS when transmitting sensitive data through CRUD operations
  2. Implement rate limiting for create, update, and delete operations
  3. Log security events using the event emitter for ACL failures and validation errors
  4. Validate user input at multiple layers (client, server, database)
  5. Use query scoping to prevent data leakage across tenant boundaries

Performance

  1. Select minimal fields in index operations to reduce database load
  2. Implement appropriate pagination limits based on your data size and usage patterns
  3. Use database indexes on fields commonly used in filters and query scoping
  4. Monitor query performance and optimize complex filters

Maintainability

  1. Organize ACL logic in separate service classes for complex permissions
  2. Create reusable validation schemas for common patterns
  3. Document custom query scopes and their intended use cases
  4. Test error scenarios to ensure proper error handling and user experience

API Design

  1. Use consistent field naming between database columns and API responses
  2. Provide meaningful error messages that help developers understand issues
  3. Document filter syntax and available fields for frontend developers
  4. Version your APIs when changing field definitions or access control rules