VelaVela
CRUD

Configuration

Configure verbs, filtering, hooks, tenancy, and guards.

The @Crud() / forFeature configuration controls which verbs are generated and how the resource behaves.

Verb selection

Twenty-two verbs exist (core five, restore/clone/upsert, the batch family + bulkPatch, search/aggregate/export/import, and four version verbs gated on versioning: true). Select with only or except — table order and model gates always apply:

@Crud({
  model: Spirit,
  adapter,
  only: ['list', 'read'],        // or: except: ['delete', 'import']
})
@Controller('/spirits')
class SpiritsController {}

Querying

@Crud({
  model: Spirit,
  adapter,
  filterFields: ['element', 'name'],          // ?element[eq]=wind, 12 operators, fail-closed
  filterConfig: { element: ['eq', 'in'] },    // per-field operator allow-list
  sortFields: ['name'],                       // ?sort=name&order=desc
  searchFields: ['name'],                     // inline ?search= + GET /search
  pagination: {
    defaultPerPage: 20,
    maxPerPage: 100,
    cursor: { enabled: true, field: 'id' },   // keyset cursors (adapter capability)
  },
  upsert: { keys: ['name'] },                 // conflict target for the upsert family
})

Hooks

Hooks are ctx-first; update hooks receive two in-transaction snapshots:

hooks: {
  beforeCreate: (ctx, data) => ({ ...data, name: data.name.trim() }),
  afterUpdate: (ctx, prior, current) => audit(ctx, prior, current),
  afterDelete: (ctx, prior) => notify(prior),
}

Multi-tenant

Tenant-scoped models require an upstream resolver and an explicit affirmation — without it, decoration throws MissingTenantResolverError:

// model: defineModel({ ..., multiTenant: true })
@Crud({ model: Tenanted, adapter, tenantResolverMounted: true })

// mounted upstream of the Vela app:
outer.use('*', multiTenant());   // X-Tenant-ID by default; path/query/jwt/custom
outer.route('/', app.getHonoApp());

Guards and overrides

Generated routes run through the normal pipeline, so plain Vela decorators work — and @Override(verb) replaces one generated handler while keeping its route name and OpenAPI metadata:

@Controller('/spirits')
@UseGuards(AuthGuard)
@Crud({ model: Spirit, adapter })
class SpiritsController {
  @Override('list')
  list(@CrudCtx() ctx: CrudRequestContext) {
    // your handler; ctx.c is the Hono context, ctx.container the request scope
  }
}

Envelopes and errors

Success bodies are { success: true, result[, result_info] }; errors are { success: false, error: { code, message, details? } }. Both are pluggable:

responseEnvelope: {
  success: (result, info) => ({ data: result, meta: info ?? null }),
  error: (err) => ({ problem: err.code, detail: err.message }),
}
Edit on GitHub

Last updated on

On this page