Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
// src/errors.ts
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
// Base error types
type DomainError =
| NotFoundError
| ValidationError
| ConflictError
| AuthError
| InfrastructureError
type NotFoundError = {
_tag: 'NotFoundError'
resource: string
id: string
}
type ValidationError = {
_tag: 'ValidationError'
field: string
message: string
value?: unknown
}
type ConflictError = {
_tag: 'ConflictError'
resource: string
field: string
value: string
}
type AuthError =
| { _tag: 'Unauthenticated' }
| { _tag: 'Unauthorized'; required: string }
| { _tag: 'TokenExpired' }
type InfrastructureError = {
_tag: 'InfrastructureError'
service: string
cause: unknown
}
// Smart constructors
export const notFound = (resource: string, id: string): NotFoundError => ({
_tag: 'NotFoundError',
resource,
id,
})
export const validation = (
field: string,
message: string,
value?: unknown
): ValidationError => ({
_tag: 'ValidationError',
field,
message,
value,
})
export const conflict = (
resource: string,
field: string,
value: string
): ConflictError => ({
_tag: 'ConflictError',
resource,
field,
value,
})
// Error to HTTP status mapping
export const toHttpStatus = (error: DomainError): number => {
switch (error._tag) {
case 'NotFoundError':
return 404
case 'ValidationError':
return 400
case 'ConflictError':
return 409
case 'Unauthenticated':
return 401
case 'Unauthorized':
return 403
case 'TokenExpired':
return 401
case 'InfrastructureError':
return 503
default:
return 500
}
}
// Error to response body
export const toResponseBody = (
error: DomainError
): { error: string; details?: unknown } => {
switch (error._tag) {
case 'NotFoundError':
return { error: `${error.resource} not found` }
case 'ValidationError':
return {
error: 'Validation failed',
details: { field: error.field, message: error.message },
}
case 'ConflictError':
return {
error: `${error.resource} with ${error.field} already exists`,
}
case 'Unauthenticated':
return { error: 'Authentication required' }
case 'Unauthorized':
return { error: `Permission denied: ${error.required}` }
case 'TokenExpired':
return { error: 'Token expired' }
case 'InfrastructureError':
return { error: 'Service temporarily unavailable' }
}
}
Error Recovery
// src/lib/recovery.ts
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
// Retry with exponential backoff
export const withRetry =
<R, E, A>(
maxAttempts: number,
baseDelayMs: number,
shouldRetry: (error: E) => boolean
) =>
(
operation: RTE.ReaderTaskEither<R, E, A>
): RTE.ReaderTaskEither<R, E, A> =>
pipe(
RTE.ask<R>(),
RTE.flatMap(deps => {
const attempt = (
remaining: number,
delay: number
): TE.TaskEither<E, A> =>
pipe(
operation(deps),
TE.orElse(error => {
if (remaining <= 0 || !shouldRetry(error)) {
return TE.left(error)
}
return pipe(
TE.fromTask(() => new Promise(r => setTimeout(r, delay))),
TE.flatMap(() => attempt(remaining - 1, delay * 2))
)
})
)
return RTE.fromTaskEither(attempt(maxAttempts - 1, baseDelayMs))
})
)
// Fallback to cached value
export const withFallback =
<R extends { cache: CacheClient }, E, A>(
cacheKey: string,
ttlSeconds: number
) =>
(
operation: RTE.ReaderTaskEither<R, E, A>
): RTE.ReaderTaskEither<R, E, A> =>
pipe(
RTE.ask<R>(),
RTE.flatMap(({ cache, ...rest }) =>
pipe(
operation,
// On success, cache the result
RTE.tap(result =>
RTE.fromTaskEither(cache.set(cacheKey, result, ttlSeconds))
),
// On failure, try to get cached value
RTE.orElse(error =>
pipe(
RTE.fromTaskEither(cache.get<A>(cacheKey)),
RTE.flatMap(cached =>
cached ? RTE.right(cached) : RTE.left(error)
)
)
)
)
)
)
// Circuit breaker
type CircuitState = 'closed' | 'open' | 'half-open'
export const createCircuitBreaker = <E>(
failureThreshold: number,
resetTimeoutMs: number,
isFailure: (error: E) => boolean
) => {
let state: CircuitState = 'closed'
let failures = 0
let lastFailure = 0
return <R, A>(
operation: RTE.ReaderTaskEither<R, E, A>
): RTE.ReaderTaskEither<R, E | { _tag: 'CircuitOpen' }, A> =>
pipe(
RTE.ask<R>(),
RTE.flatMap(deps => {
// Check if circuit should reset
if (
state === 'open' &&
Date.now() - lastFailure > resetTimeoutMs
) {
state = 'half-open'
}
if (state === 'open') {
return RTE.left({ _tag: 'CircuitOpen' as const })
}
return pipe(
operation,
RTE.tap(() => {
if (state === 'half-open') {
state = 'closed'
failures = 0
}
return RTE.right(undefined)
}),
RTE.tapError(error => {
if (isFailure(error)) {
failures++
lastFailure = Date.now()
if (failures >= failureThreshold) {
state = 'open'
}
}
return RTE.right(undefined)
})
)
})
)
}
Testing Strategies
Mocking Dependencies
// src/services/__tests__/user.service.test.ts
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import { describe, it, expect, vi } from 'vitest'
import * as UserService from '../user.service'
// Create mock dependencies
const createMockDeps = (overrides: Partial<UserDeps> = {}): UserDeps => ({
db: {
users: {
findUnique: vi.fn(() => Promise.resolve(null)),
create: vi.fn(data => Promise.resolve({ id: '1', ...data })),
update: vi.fn((where, data) => Promise.resolve({ id: where.id, ...data })),
},
},
hasher: {
hash: vi.fn(password => TE.right(`hashed_${password}`)),
verify: vi.fn(() => TE.right(true)),
},
mailer: {
send: vi.fn(() => TE.right(undefined)),
},
...overrides,
})
describe('UserService', () => {
describe('create', () => {
it('should create a user with hashed password', async () => {
const deps = createMockDeps()
const input = {
email: 'test@example.com',
password: 'secret123',
name: 'Test User',
}
const result = await UserService.create(input)(deps)()
expect(E.isRight(result)).toBe(true)
if (E.isRight(result)) {
expect(result.right.email).toBe(input.email)
}
expect(deps.hasher.hash).toHaveBeenCalledWith('secret123')
})
it('should fail when email already exists', async () => {
const existingUser = { id: '1', email: 'test@example.com' }
const deps = createMockDeps({
db: {
users: {
findUnique: vi.fn(() => Promise.resolve(existingUser)),
create: vi.fn(),
},
},
})
const result = await UserService.create({
email: 'test@example.com',
password: 'secret',
name: 'Test',
})(deps)()
expect(E.isLeft(result)).toBe(true)
if (E.isLeft(result)) {
expect(result.left._tag).toBe('EmailExists')
}
})
})
describe('findById', () => {
it('should return user when found', async () => {
const user = { id: '1', email: 'test@example.com', name: 'Test' }
const deps = createMockDeps({
db: {
users: {
findUnique: vi.fn(() => Promise.resolve(user)),
},
},
})
const result = await UserService.findById('1')(deps)()
expect(E.isRight(result)).toBe(true)
if (E.isRight(result)) {
expect(result.right).toEqual(user)
}
})
it('should return NotFound when user does not exist', async () => {
const deps = createMockDeps()
const result = await UserService.findById('nonexistent')(deps)()
expect(E.isLeft(result)).toBe(true)
if (E.isLeft(result)) {
expect(result.left._tag).toBe('UserNotFound')
expect(result.left.id).toBe('nonexistent')
}
})
})
})
Integration Testing with Test Containers
// src/__tests__/integration/user.integration.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql'
import { PrismaClient } from '@prisma/client'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { buildDeps, destroyDeps, AppDeps } from '../../deps'
import * as UserService from '../../services/user.service'
describe('UserService Integration', () => {
let container: PostgreSqlContainer
let deps: AppDeps
beforeAll(async () => {
// Start PostgreSQL container
container = await new PostgreSqlContainer().start()
// Build real dependencies with test database
process.env.DATABASE_URL = container.getConnectionUri()
const depsResult = await buildDeps()()
if (E.isLeft(depsResult)) {
throw new Error(`Failed to build deps: ${depsResult.left}`)
}
deps = depsResult.right
// Run migrations
await deps.db.$executeRaw`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`
// ... run Prisma migrations
}, 60000)
afterAll(async () => {
await destroyDeps(deps)()
await container.stop()
})
it('should create and retrieve a user', async () => {
// Create user
const createResult = await UserService.create({
email: 'integration@test.com',
password: 'password123',
name: 'Integration Test',
})(deps)()
expect(E.isRight(createResult)).toBe(true)
if (E.isLeft(createResult)) return
const user = createResult.right
// Retrieve user
const findResult = await UserService.findById(user.id)(deps)()
expect(E.isRight(findResult)).toBe(true)
if (E.isRight(findResult)) {
expect(findResult.right.email).toBe('integration@test.com')
}
})
})
Property-Based Testing
// src/__tests__/property/user.property.test.ts
import * as fc from 'fast-check'
import * as E from 'fp-ts/Either'
import { describe, it, expect } from 'vitest'
import { validateEmail, validatePassword } from '../../validation'
describe('Validation Properties', () => {
it('valid emails should pass validation', () => {
fc.assert(
fc.property(fc.emailAddress(), email => {
const result = validateEmail(email)
return E.isRight(result)
})
)
})
it('passwords meeting requirements should pass', () => {
const validPassword = fc
.tuple(
fc.stringOf(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'), {
minLength: 4,
}),
fc.stringOf(fc.constantFrom(...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), {
minLength: 1,
}),
fc.stringOf(fc.constantFrom(...'0123456789'), { minLength: 1 }),
fc.stringOf(fc.constantFrom(...'!@#$%^&*'), { minLength: 1 })
)
.map(parts => parts.join(''))
fc.assert(
fc.property(validPassword, password => {
const result = validatePassword(password)
return E.isRight(result)
})
)
})
it('empty strings should fail email validation', () => {
const result = validateEmail('')
expect(E.isLeft(result)).toBe(true)
})
})
Quick Reference
Common Imports
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import * as A from 'fp-ts/Array'
import * as T from 'fp-ts/Task'
import { pipe, flow } from 'fp-ts/function'
RTE Cheat Sheet
Operation
Description
RTE.right(a)
Lift value into success
RTE.left(e)
Create error
RTE.ask<R>()
Get dependencies
RTE.fromTaskEither(te)
Lift TaskEither
RTE.fromEither(e)
Lift Either
RTE.fromOption(onNone)(o)
Lift Option
RTE.flatMap(f)
Chain operations
RTE.map(f)
Transform success
RTE.mapLeft(f)
Transform error
RTE.tap(f)
Side effect on success
RTE.tapError(f)
Side effect on error
RTE.orElse(f)
Recover from error
RTE.getOrElse(f)
Extract with fallback
Service Template
// Template for a new service
import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
type MyServiceDeps = {
db: DatabaseClient
// ... other dependencies
}
type MyServiceError =
| { _tag: 'NotFound'; id: string }
| { _tag: 'ValidationFailed'; reason: string }
export const myOperation = (
input: Input
): RTE.ReaderTaskEither<MyServiceDeps, MyServiceError, Output> =>
pipe(
RTE.ask<MyServiceDeps>(),
RTE.flatMap(deps =>
// Your implementation here
RTE.right(output)
)
)