Skip to content

defineService

Declares a service. A service is a named collection of methods available via dependency injection throughout the framework. Services are the single home for all business logic.

See Services concept for usage patterns.

Signature

typescript
import { defineService } from 'rangka';

export default defineService({
  name: 'sales.pricing',
  deps: ['inventory.stock'],
  factory: (ctx) => ({
    async calculateItemRate(item, priceList, customer) {
      const baseRate = await ctx.db
        .selectFrom('sales.item_price')
        .where('item', '=', item)
        .where('price_list', '=', priceList)
        .select('rate')
        .executeTakeFirst();

      if (!baseRate) return 0;
      const discount = await this.getCustomerDiscount(customer, item);
      return baseRate.rate * (1 - discount / 100);
    },

    async getCustomerDiscount(customer, item) {
      const rule = await ctx.db
        .selectFrom('sales.pricing_rule')
        .where('customer', '=', customer)
        .where('item', '=', item)
        .select('discount_percent')
        .executeTakeFirst();

      return rule?.discount_percent || 0;
    },
  }),
});

Function Signature

typescript
function defineService<T>(config: {
  name: string;
  deps?: string[];
  factory: (ctx: FrameworkContext) => T;
}): ServiceDefinition<T>;
ParameterTypeDescription
config.namestringRequired. Unique service identifier used for injection.
config.depsstring[]Optional. Other services this service depends on. Used for circular dependency detection.
config.factory(ctx) => TRequired. Factory function that receives the framework context and returns the service methods.

FrameworkContext

The factory function receives the universal FrameworkContext. This is the same context available in hooks, jobs, and other extension points.

typescript
interface FrameworkContext {
  db: DatabaseClient;
  schema: SchemaRegistry;
  auth: { user: ContextUser | null; roles: string[] };
  scope: unknown;
  config: Record<string, unknown>;
  models: ModelAccessInterface;
  service: (name: string) => ServiceInstance;
  enqueue: (job: string, data: unknown, opts?: JobOptions) => Promise<void>;
  events: {
    emit: (event: string, data?: unknown) => void;
    on: (event: string, handler: Function) => void;
  };
  notify: (message: string, opts?: NotifyOptions) => Promise<void>;
  email: { send: (opts: EmailOptions) => Promise<void> };
}
FieldTypeDescription
dbDatabaseClientRaw database escape hatch for direct queries via Kysely.
schemaSchemaRegistryAccess resolved models, relationships, and field metadata.
authobjectCurrent user and their roles.
scopeunknownActive scope value (e.g. current company).
configobjectApplication configuration values.
modelsModelAccessInterfaceQuery, create, update, and delete records on any model.
servicefunctionCall other services by name (dependency injection).
enqueuefunctionQueue background jobs for async processing.
eventsobjectEmit and subscribe to application events.
notifyfunctionSend a notification to the current user or a channel.
emailobjectSend emails.

Calling Services

From Hooks

typescript
defineHooks('sales.order', {
  async afterSave(doc, ctx) {
    const pricing = ctx.service('sales.pricing');
    await pricing.recalculateTotals(doc);
  },
});

From Other Services

typescript
defineService({
  name: 'sales.invoicing',
  deps: ['sales.pricing'],
  factory: (ctx) => ({
    async createInvoice(order) {
      const pricing = ctx.service('sales.pricing');
      const total = await pricing.calculateOrderTotal(order.items, order.price_list, order.customer);
      return await ctx.db.insertInto('sales.invoice').values({ total, ... }).execute();
    },
  }),
});

From Jobs

typescript
defineJob('sales.daily-summary', {
  schedule: '0 6 * * *',
  async handler(data, ctx) {
    const analytics = ctx.service('sales.analytics');
    const summary = await analytics.generateDailySummary();
    await ctx.email.send({ to: data.recipients, subject: 'Daily Summary', body: summary });
  },
});

Registration

Services are automatically discovered from the services/ directory in each module:

modules/sales/
  services/
    pricing.ts       # defineService({ name: 'sales.pricing', ... })
    invoicing.ts     # defineService({ name: 'sales.invoicing', ... })
    submitOrder.ts   # defineService({ name: 'sales.submitOrder', ... })

The service name parameter must be unique across the entire application.

Dependency Graph

Services can depend on other services. The framework detects circular dependencies at boot time and throws an error if found.

sales.pricing → inventory.stock    ✓ OK
sales.invoicing → sales.pricing    ✓ OK
inventory.stock → sales.pricing    ✗ Circular dependency detected

Error Handling

Services reject by throwing. The framework handles errors based on calling context:

Called fromError behavior
ActionError message shown as toast. Operation aborted.
HookError aborts the save. Returns 400 to client.
JobError triggers retry logic (if configured).
ServiceError propagates to the calling service.

Transaction Support

Services run within the caller's transaction context when invoked from hooks (which execute inside a transaction). Direct service calls from actions or jobs do not automatically wrap in a transaction. Use ctx.db to manage transactions explicitly if needed.