Files
myeasycms-v2/.claude/skills/service-builder/examples.md
Giancarlo Buomprisco f3ac595d06 MCP Server 2.0 (#452)
* MCP Server 2.0

- Updated application version from 2.23.14 to 2.24.0 in package.json.
- MCP Server improved with new features
- Migrated functionality from Dev Tools to MCP Server
- Improved getMonitoringProvider not to crash application when misconfigured
2026-02-11 20:42:01 +01:00

274 lines
7.2 KiB
Markdown

# Service Builder Examples
## Service with Multiple Dependencies
When a service needs more than just a database client, inject all dependencies.
```typescript
// _lib/server/invoice.service.ts
import type { SupabaseClient } from '@supabase/supabase-js';
interface InvoiceServiceDeps {
client: SupabaseClient;
storage: SupabaseClient['storage'];
}
export function createInvoiceService(deps: InvoiceServiceDeps) {
return new InvoiceService(deps);
}
class InvoiceService {
constructor(private readonly deps: InvoiceServiceDeps) {}
async generatePdf(invoiceId: string): Promise<{ url: string }> {
const { data: invoice, error } = await this.deps.client
.from('invoices')
.select('*')
.eq('id', invoiceId)
.single();
if (error) throw error;
const pdf = this.buildPdf(invoice);
const { data: upload, error: uploadError } = await this.deps.storage
.from('invoices')
.upload(`${invoiceId}.pdf`, pdf);
if (uploadError) throw uploadError;
return { url: upload.path };
}
private buildPdf(invoice: Record<string, unknown>): Uint8Array {
// Pure logic — no I/O
// ...
return new Uint8Array();
}
}
```
**Server action adapter:**
```typescript
'use server';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { GenerateInvoiceSchema } from '../schemas/invoice.schema';
import { createInvoiceService } from './invoice.service';
export const generateInvoicePdfAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
const service = createInvoiceService({
client,
storage: client.storage,
});
return service.generatePdf(data.invoiceId);
},
{ auth: true, schema: GenerateInvoiceSchema },
);
```
## Service Composing Other Services
Services can depend on other services — compose at the adapter level.
```typescript
// _lib/server/onboarding.service.ts
import type { ProjectService } from './project.service';
import type { NotificationService } from './notification.service';
interface OnboardingServiceDeps {
projects: ProjectService;
notifications: NotificationService;
}
export function createOnboardingService(deps: OnboardingServiceDeps) {
return new OnboardingService(deps);
}
class OnboardingService {
constructor(private readonly deps: OnboardingServiceDeps) {}
async onboardAccount(params: { accountId: string; accountName: string }) {
// Create default project
const project = await this.deps.projects.create({
name: `${params.accountName}'s First Project`,
accountId: params.accountId,
});
// Send welcome notification
await this.deps.notifications.send({
accountId: params.accountId,
type: 'welcome',
data: { projectId: project.id },
});
return { project };
}
}
```
**Adapter composes the dependency tree:**
```typescript
'use server';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createOnboardingService } from './onboarding.service';
import { createProjectService } from './project.service';
import { createNotificationService } from './notification.service';
export const onboardAccountAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
const service = createOnboardingService({
projects: createProjectService(client),
notifications: createNotificationService(client),
});
return service.onboardAccount(data);
},
{ auth: true, schema: OnboardAccountSchema },
);
```
## Pure Validation Service (No I/O)
Some services are entirely pure — they don't even need a database client.
```typescript
// _lib/server/pricing.service.ts
interface PricingInput {
plan: 'starter' | 'pro' | 'enterprise';
seats: number;
billingPeriod: 'monthly' | 'yearly';
}
interface PricingResult {
unitPrice: number;
total: number;
discount: number;
currency: string;
}
export function calculatePricing(input: PricingInput): PricingResult {
const basePrices = { starter: 900, pro: 2900, enterprise: 9900 };
const unitPrice = basePrices[input.plan];
const yearlyDiscount = input.billingPeriod === 'yearly' ? 0.2 : 0;
const seatDiscount = input.seats >= 10 ? 0.1 : 0;
const discount = Math.min(yearlyDiscount + seatDiscount, 0.3);
const total = Math.round(unitPrice * input.seats * (1 - discount));
return { unitPrice, total, discount, currency: 'usd' };
}
```
This is the simplest case — a plain function, no class, no dependencies. Trivially testable:
```typescript
import { calculatePricing } from '../pricing.service';
it('applies yearly discount', () => {
const result = calculatePricing({
plan: 'pro',
seats: 1,
billingPeriod: 'yearly',
});
expect(result.discount).toBe(0.2);
expect(result.total).toBe(2320); // 2900 * 0.8
});
```
## Testing with Mock Client
Full mock pattern for Supabase client:
```typescript
import { vi } from 'vitest';
import type { SupabaseClient } from '@supabase/supabase-js';
/**
* Creates a chainable mock that mimics Supabase's query builder.
* Override any method in the chain via the `overrides` param.
*/
export function createMockSupabaseClient(
resolvedValue: { data: unknown; error: unknown } = { data: null, error: null },
overrides: Record<string, unknown> = {},
) {
const chain: Record<string, ReturnType<typeof vi.fn>> = {};
// Every method returns `this` (chainable) by default
const methods = [
'select', 'insert', 'update', 'upsert', 'delete',
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in',
'like', 'ilike', 'is', 'order', 'limit', 'range',
'single', 'maybeSingle',
];
for (const method of methods) {
chain[method] = vi.fn().mockReturnThis();
}
// Terminal methods resolve with data
chain.single = vi.fn().mockResolvedValue(resolvedValue);
chain.maybeSingle = vi.fn().mockResolvedValue(resolvedValue);
// Apply overrides
for (const [key, value] of Object.entries(overrides)) {
chain[key] = vi.fn().mockImplementation(
typeof value === 'function' ? value : () => value,
);
}
// Non-terminal chains that don't end with single/maybeSingle
// resolve when awaited via .then()
const proxyHandler: ProxyHandler<typeof chain> = {
get(target, prop) {
if (prop === 'then') {
return (resolve: (v: unknown) => void) => resolve(resolvedValue);
}
return target[prop as string] ?? vi.fn().mockReturnValue(target);
},
};
const chainProxy = new Proxy(chain, proxyHandler);
return {
from: vi.fn(() => chainProxy),
chain,
} as unknown as SupabaseClient & { chain: typeof chain };
}
```
Usage:
```typescript
import { createMockSupabaseClient } from '../test-utils';
import { createProjectService } from '../project.service';
it('lists projects for an account', async () => {
const projects = [
{ id: '1', name: 'Alpha', account_id: 'acc-1' },
{ id: '2', name: 'Beta', account_id: 'acc-1' },
];
const client = createMockSupabaseClient({ data: projects, error: null });
const service = createProjectService(client);
const result = await service.list('acc-1');
expect(result).toEqual(projects);
expect(client.from).toHaveBeenCalledWith('projects');
expect(client.chain.eq).toHaveBeenCalledWith('account_id', 'acc-1');
});
```