# @archtx/procedures

A **TypeScript** library for creating type-safe, schema-validated procedure calls (RPCs, API controllers, etc.) with a single function definition.

## Quick Start

```bash
npm install @archtx/procedures
```

```ts
import { Procedures } from '@archtx/procedures'
import { v } from 'suretype' // or use TypeBox

const { Create } = Procedures()

// Define a procedure with schema validation
const { GetUser, procedure, info } = Create(
  'GetUser',
  {
    description: 'Fetch a user by ID',
    schema: {
      args: v.object({ id: v.number().required() }),
      data: v.object({ name: v.string(), email: v.string() }),
    },
  },
  async (ctx, args) => {
    // args is typed as { id: number }
    return { name: 'John', email: 'john@example.com' }
  },
)

// Call the procedure directly
const user = await GetUser({}, { id: 1 })
```

## Features

- Single-function procedure definitions with `Create()`
- Type-safe context, arguments, and return values
- Built-in schema validation (Suretype or TypeBox)
- Automatic JSON Schema generation for documentation
- Pre-handler hooks for authentication/authorization
- Typed error handling with HTTP-style status codes
- Framework-agnostic registration callbacks

---

## Core Concepts

### The Procedures Factory

`Procedures()` creates a scoped procedure factory with shared configuration:

```ts
const { Create, getProcedures } = Procedures({
  onCreate: (registration) => {
    // Called when each procedure is created
    // Use this to register with your router/framework
  },
})
```

### Create Function Signature

```ts
Create(
  name: string,           // Unique procedure name
  config: ProcedureConfig, // Schema, hooks, description, extended config
  handler: HandlerFunction // Async function that executes the procedure
)
```

Returns an object with:
- **`[name]`**: Handler function (dynamic key matching the procedure name)
- **`procedure`**: Same handler function (generic reference)
- **`info`**: Metadata object with schema, description, and config

---

## Configuration Options

### Custom Context Type

Define a context type that will be passed to all handlers:

```ts
interface AppContext {
  authToken: string
  requestId: string
}

const { Create } = Procedures<AppContext>()

Create('MyProcedure', {}, async (ctx, args) => {
  // ctx.authToken and ctx.requestId are typed
  return ctx.authToken
})
```

### Extended Config Type

Add custom configuration properties to all procedures:

```ts
interface ApiConfig {
  route: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  rateLimit?: number
}

const { Create } = Procedures<unknown, ApiConfig>({
  onCreate: ({ name, config }) => {
    // config.route and config.method are available
    router[config.method.toLowerCase()](config.route, config.handler)
  },
})

const { info } = Create(
  'ListUsers',
  {
    route: '/api/users', // type-checked & required by ApiConfig
    method: 'GET', // type-checked & required by ApiConfig
    rateLimit: 100, // type-checked & optional by ApiConfig
  },
  async () => []
)

console.log(info.route) // '/api/users'
```

### Registration Callback (onCreate)

Use `onCreate` to integrate with your framework:

```ts
const { Create } = Procedures({
  onCreate: ({ handler, config, name }) => {
    // Register with Express
    app.post(`/rpc/${name}`, async (req, res) => {
      try {
        const result = await handler(req.context, req.body)
        res.json(result)
      } catch (error) {
        res.status(error.code).json({ error: error.message })
      }
    })
  },
})
```

---

## Schema Validation

Schemas provide type inference and runtime validation. Supports both **Suretype** and **TypeBox**.

### Using Suretype

```ts
import { v } from 'suretype'

Create('CreateUser', {
  schema: {
    args: v.object({
      name: v.string().required(),
      email: v.string().required(),
      age: v.number(),
    }),
    data: v.object({
      id: v.string(),
      name: v.string(),
    }),
  },
}, async (ctx, args) => {
  // args: { name: string, email: string, age?: number }
  return { id: 'user-123', name: args.name }
})
```

### Using TypeBox

```ts
import { Type } from 'typebox'

Create('CreateUser', {
  schema: {
    args: Type.Object({
      name: Type.String(),
      email: Type.String(),
      age: Type.Optional(Type.Number()),
    }),
    data: Type.Object({
      id: Type.String(),
      name: Type.String(),
    }),
  },
}, async (ctx, args) => {
  return { id: 'user-123', name: args.name }
})
```

### Validation Behavior

When arguments fail validation, a `ProcedureValidationError` is thrown automatically:

```ts
try {
  await CreateUser({}, { name: 'John' }) // missing required 'email'
} catch (error) {
  // error instanceof ProcedureValidationError
  // error.code === 422 (VALIDATION_ERROR)
  // error.errors contains detailed validation errors
}
```

### Accessing Generated JSON Schema

The `info` object contains the generated JSON Schema:

```ts
const { info } = Create('GetUser', {
  schema: {
    args: v.object({ id: v.number().required() }),
  },
}, handler)

console.log(info.schema)
// {
//   args: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] },
//   data: undefined
// }
```

---

## Hooks

Hooks run before the handler and can:
- Perform authentication/authorization
- Inject additional context
- Throw errors to prevent handler execution

### Basic Hook

```ts
Create('ProtectedResource', {
  hook: async (ctx) => {
    // Return additional context that merges into handler ctx
    return { timestamp: Date.now() }
  },
}, async (ctx, args) => {
  // ctx.timestamp is available and typed
  return ctx.timestamp
})
```

### Authentication Hook

```ts
interface AuthContext {
  authToken: string
}

const { Create } = Procedures<AuthContext>()

Create('GetProfile', {
  hook: async (ctx) => {
    if (!isValidToken(ctx.authToken)) {
      throw ctx.error(ProcedureCodes.UNAUTHORIZED, 'Invalid token')
    }

    const user = await getUserFromToken(ctx.authToken)
    return { user } // Adds 'user' to handler context
  },
}, async (ctx) => {
  // ctx.user is typed from hook return
  return { profile: ctx.user.profile }
})
```

### Hook Error Handling

- Throwing `ctx.error()` throws a `ProcedureError` with your code/message
- Throwing any other error wraps it in `ProcedureHookError` (code: 412)

```ts
hook: async (ctx) => {
  // This throws ProcedureError with code 401
  throw ctx.error(401, 'Unauthorized')

  // This would throw ProcedureHookError with code 412
  throw new Error('Something went wrong')
}
```

---

## Error Handling

### Error Types

| Error Class | Code | When Thrown |
|-------------|------|-------------|
| `ProcedureError` | Custom | Via `ctx.error()` in hooks/handlers |
| `ProcedureHookError` | 412 | Unhandled errors in hooks |
| `ProcedureValidationError` | 422 | Schema validation failures |
| `ProcedureRegistrationError` | N/A | Invalid schema during registration |

### Using ctx.error()

Create typed errors with HTTP-style codes:

```ts
async (ctx, args) => {
  const user = await findUser(args.id)

  if (!user) {
    throw ctx.error(ProcedureCodes.NOT_FOUND, 'User not found', { id: args.id })
  }

  return user
}
```

### ProcedureCodes

HTTP-inspired status codes:

```ts
import { ProcedureCodes } from '@archtx/procedures'

// Success codes
ProcedureCodes.OK                // 200
ProcedureCodes.CREATED           // 201
ProcedureCodes.NO_CONTENT        // 204

// Client error codes
ProcedureCodes.BAD_REQUEST       // 400
ProcedureCodes.UNAUTHORIZED      // 401
ProcedureCodes.FORBIDDEN         // 403
ProcedureCodes.NOT_FOUND         // 404
ProcedureCodes.CONFLICT          // 409
ProcedureCodes.VALIDATION_ERROR  // 422
ProcedureCodes.TOO_MANY_REQUESTS // 429

// Server error codes
ProcedureCodes.INTERNAL_ERROR    // 500
ProcedureCodes.HANDLER_ERROR     // 500
ProcedureCodes.NOT_IMPLEMENTED   // 501
ProcedureCodes.SERVICE_UNAVAILABLE // 503
```

### Catching Errors

```ts
import { ProcedureError, ProcedureValidationError } from '@archtx/procedures'

try {
  await MyProcedure(ctx, args)
} catch (error) {
  if (error instanceof ProcedureValidationError) {
    // Handle validation errors
    console.log(error.errors) // Array of validation errors
  } else if (error instanceof ProcedureError) {
    // Handle procedure errors
    console.log(error.code)          // HTTP-style code
    console.log(error.message)       // Error message
    console.log(error.procedureName) // 'MyProcedure'
    console.log(error.meta)          // Optional metadata
  }
}
```

---

## Advanced Usage

### Calling Procedures from Other Procedures

Procedures can call each other while maintaining context isolation:

```ts
const { GetUser } = Create('GetUser', {
  hook: async () => ({ source: 'GetUser' }),
}, async (ctx, args) => {
  return { id: args.id, source: ctx.source }
})

const { GetUserWithPosts } = Create('GetUserWithPosts', {
  hook: async () => ({ source: 'GetUserWithPosts' }),
}, async (ctx, args) => {
  // Call another procedure - it runs with its own hook context
  const user = await GetUser(ctx, { id: args.userId })
  // user.source === 'GetUser' (not 'GetUserWithPosts')
  return { user, posts: [] }
})
```

### Retrieving Registered Procedures

Use `getProcedures()` for introspection, documentation generation, or testing:

```ts
const { Create, getProcedures } = Procedures()

Create('UserCreate', { schema: { args: v.object({ name: v.string() }) } }, handler)
Create('UserDelete', { schema: { args: v.object({ id: v.number() }) } }, handler)

const procedures = getProcedures()
// Map<string, { name, config, handler }>

procedures.forEach((proc, name) => {
  console.log(`${name}: ${JSON.stringify(proc.config.schema)}`)
})
```

### Framework Integration Example

```ts
// procedures.ts
import { Procedures } from '@archtx/procedures'
import { v } from 'suretype'

interface RequestContext {
  userId: string
  requestId: string
}

interface RouteConfig {
  path: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
}

export const { Create, getProcedures } = Procedures<RequestContext, RouteConfig>({
  onCreate: ({ handler, config, name }) => {
    // Register with your router here
    console.log(`Registered: ${config.method} ${config.path} -> ${name}`)
  },
})

// user-procedures.ts
import { Create } from './procedures'

export const { GetUser } = Create(
  'GetUser',
  {
    path: '/api/users/:id',
    method: 'GET',
    description: 'Fetch user by ID',
    schema: {
      args: v.object({ id: v.string().required() }),
      data: v.object({ id: v.string(), name: v.string() }),
    },
    hook: async (ctx) => {
      // Verify user has permission
      if (!hasPermission(ctx.userId, 'read:users')) {
        throw ctx.error(403, 'Forbidden')
      }
      return {}
    },
  },
  async (ctx, args) => {
    return await db.users.findById(args.id)
  },
)
```

### Validation Access via Config

The `onCreate` callback receives validation functions for external use:

```ts
const { Create } = Procedures({
  onCreate: ({ config }) => {
    if (config.validation?.args) {
      // Pre-validate before calling handler
      const { errors } = config.validation.args(requestBody)
      if (errors) {
        return res.status(422).json({ errors })
      }
    }
  },
})
```

---

## API Reference

### Procedures(builder?)

Creates a procedure factory.

**Parameters:**
- `builder.onCreate?: (registration) => void` - Called when each procedure is created

**Returns:**
- `Create` - Function to create procedures
- `getProcedures` - Returns Map of all registered procedures

### Create(name, config, handler)

Creates a procedure.

**Parameters:**
- `name: string` - Unique procedure identifier
- `config.description?: string` - Human-readable description
- `config.schema?.args` - Suretype/TypeBox schema for arguments
- `config.schema?.data` - Suretype/TypeBox schema for return value
- `config.hook?: (ctx, args) => Promise<LocalContext>` - Pre-handler hook
- `handler: (ctx, args) => Promise<Data>` - Procedure implementation

**Returns:**
- `[name]: handler` - Named handler export
- `procedure: handler` - Generic handler reference
- `info` - Procedure metadata with compiled schema

---

## License

MIT
