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
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
function defineService<T>(config: {
name: string;
deps?: string[];
factory: (ctx: FrameworkContext) => T;
}): ServiceDefinition<T>;| Parameter | Type | Description |
|---|---|---|
config.name | string | Required. Unique service identifier used for injection. |
config.deps | string[] | Optional. Other services this service depends on. Used for circular dependency detection. |
config.factory | (ctx) => T | Required. 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.
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> };
}| Field | Type | Description |
|---|---|---|
db | DatabaseClient | Raw database escape hatch for direct queries via Kysely. |
schema | SchemaRegistry | Access resolved models, relationships, and field metadata. |
auth | object | Current user and their roles. |
scope | unknown | Active scope value (e.g. current company). |
config | object | Application configuration values. |
models | ModelAccessInterface | Query, create, update, and delete records on any model. |
service | function | Call other services by name (dependency injection). |
enqueue | function | Queue background jobs for async processing. |
events | object | Emit and subscribe to application events. |
notify | function | Send a notification to the current user or a channel. |
email | object | Send emails. |
Calling Services
From Hooks
defineHooks('sales.order', {
async afterSave(doc, ctx) {
const pricing = ctx.service('sales.pricing');
await pricing.recalculateTotals(doc);
},
});From Other Services
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
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 detectedError Handling
Services reject by throwing. The framework handles errors based on calling context:
| Called from | Error behavior |
|---|---|
| Action | Error message shown as toast. Operation aborted. |
| Hook | Error aborts the save. Returns 400 to client. |
| Job | Error triggers retry logic (if configured). |
| Service | Error 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.