# DTI use cases / DTI хэрэглээний жишээ

Энэ документ нь `@napp/dti-core`, `@napp/dti-client`, `@napp/dti-server`-ийн нийтлэг хэрэглээг жишээгээр тайлбарлана.  
This document explains common usage of `@napp/dti-core`, `@napp/dti-client`, and `@napp/dti-server` with examples.

## 1. Shared contract ашиглах / Using a Shared Contract

Contract-ийг client болон server хоёр талд import хийж ашиглана.  
Import and use the same contract on both the client and server sides.

```ts
import { z } from "zod";
import { createAction } from "@napp/dti-core";

export const userCreate = createAction("userCreate", {
    body: z.object({
        name: z.string(),
        age: z.number(),
    }),
    result: z.object({
        id: z.string(),
        name: z.string(),
    }),
}, {
    path: "/users",
    method: "POST",
    contentType: "json",
});
```

Энэ contract-оос `body`, `query`, `result` type автоматаар гарна. Заавал тусдаа `interface` зарлах шаардлагагүй.  
`body`, `query`, and `result` types are inferred automatically from this contract. A separate `interface` is not required.

## 2. JSON API endpoint / JSON API Endpoint

Server талд action contract-д handler холбож REST endpoint үүсгэнэ.  
On the server side, attach a handler to the action contract to create a REST endpoint.

Server:

```ts
import express from "express";
import { createDTIExpressRouter } from "@napp/dti-server";
import { userCreate } from "./contract";

const app = express();
const dti = createDTIExpressRouter();

dti.action(userCreate, async ({ body }) => {
    return {
        id: "user-001",
        name: body.name,
    };
});

app.use("/api", dti.router());
```

Client талд ижил contract-ийг ашиглан typed API call хийнэ.  
On the client side, use the same contract to make a typed API call.

Client:

```ts
import { DTIClient } from "@napp/dti-client";
import { userCreate } from "./contract";

const client = new DTIClient("/api");

const user = await client.call(userCreate, {
    body: {
        name: "Bat",
        age: 25,
    },
});
```

Server success үед дараах envelope буцаана.  
On success, the server returns the following envelope.

```json
{
  "success": true,
  "data": {
    "id": "user-001",
    "name": "Bat"
  }
}
```

## 3. Query request / Query Request

`query` schema нь URL query parameter-үүдийг validate болон type inference хийхэд ашиглагдана.  
The `query` schema is used to validate URL query parameters and infer their types.

```ts
export const userList = createAction("userList", {
    query: z.object({
        q: z.string().optional(),
        page: z.coerce.number().default(1),
    }),
    result: z.object({
        items: z.array(z.object({
            id: z.string(),
        })),
        total: z.number(),
    }),
}, {
    path: "/users",
    method: "GET",
});
```

```ts
const result = await client.call(userList, {
    query: {
        q: "bat",
        page: 1,
    },
});
```

## 4. Typed path params

`params` schema нь single болон nested resource route-ийн named placeholder-уудыг validate болон type inference хийхэд ашиглагдана.

```ts
export const tenantUserRead = createAction("tenantUserRead", {
    params: z.object({
        tenantId: z.string(),
        userId: z.coerce.number().int().positive(),
    }),
    result: z.object({
        id: z.number(),
    }),
}, {
    path: "/tenants/:tenantId/users/:userId",
    method: "GET",
});
```

```ts
dti.action(tenantUserRead, async ({ params }) => ({
    id: params.userId,
}));

const user = await client.call(tenantUserRead, {
    params: {
        tenantId: "acme",
        userId: 42,
    },
});
```

Client request path нь `/tenants/acme/users/42` болно. Placeholder бүр URL encode хийгдэнэ. Server талд HTTP string value-г schema parse хийх тул numeric type дээр `z.coerce.number()` ашиглана.

Route placeholder set болон schema field set яг таарна. Optional, repeated, wildcard болон custom regex syntax дэмжихгүй.

## 5. Form submit / Form Submit

`contentType: "form"` үед client тал `application/x-www-form-urlencoded` body үүсгэнэ.  
When `contentType: "form"` is used, the client creates an `application/x-www-form-urlencoded` body.

```ts
export const loginAction = createAction("loginAction", {
    body: z.object({
        username: z.string(),
        password: z.string(),
    }),
    result: z.object({
        token: z.string(),
    }),
}, {
    path: "/login",
    method: "POST",
    contentType: "form",
});
```

```ts
const result = await client.call(loginAction, {
    body: {
        username: "bat",
        password: "secret",
    },
});
```

## 6. Text response / Text Response

`responseType: "text"` үед server raw text буцаана.  
When `responseType: "text"` is used, the server returns raw text.

```ts
import { dtiText } from "@napp/dti-server";

export const healthText = createAction("healthText", {}, {
    path: "/health.txt",
    method: "GET",
    responseType: "text",
});

dti.action(healthText, async () => {
    return dtiText("ok", {
        headers: {
            "x-health": "ok",
        },
    });
});
```

```ts
const result = await client.call(healthText, {});
// result type: string
```

## 7. File download / File Download

`responseType: "file"` үед client тал `Blob` хүлээж авна.  
When `responseType: "file"` is used, the client receives a `Blob`.

```ts
import { dtiFile } from "@napp/dti-server";

export const reportDownload = createAction("reportDownload", {
    query: z.object({
        id: z.string(),
    }),
}, {
    path: "/reports/download",
    method: "GET",
    responseType: "file",
});

dti.action(reportDownload, async ({ query }) => {
    return dtiFile({
        body: `report:${query.id}`,
        filename: `${query.id}.txt`,
        contentType: "text/plain",
    });
});
```

```ts
const blob = await client.call(reportDownload, {
    query: {
        id: "report-001",
    },
});
```

## 8. Response header унших / Reading Response Headers

`callDetailed` нь parsed result болон raw `Response` хоёрыг буцаана.  
`callDetailed` returns both the parsed result and the raw `Response`.

```ts
const { result, response } = await client.callDetailed(userCreate, {
    body: {
        name: "Bat",
        age: 25,
    },
});

const traceId = response.headers.get("x-trace-id");
```

## 9. Custom request header / Custom Request Headers

Global header нь тухайн `DTIClient` instance-ийн бүх request-д нэмэгдэнэ.  
Global headers are added to every request made by that `DTIClient` instance.

Global header:

```ts
const client = new DTIClient("/api", {
    headers: {
        "x-client": "web",
    },
});
```

Per-call header нь зөвхөн тухайн request-д нэмэгдэнэ.  
Per-call headers are added only to the specific request.

Per-call header:

```ts
await client.call(userCreate, param, {
    headers: {
        "x-trace-id": "trace-001",
    },
});
```

Header merge дараалал / Header merge order:

1. client global `headers`
2. client global `auth`
3. per-call `headers`
4. per-call `auth`
5. DTI signing headers
6. `Content-Type` байхгүй бол library өөрөө нэмнэ / If `Content-Type` is missing, the library adds it automatically

## 10. Router auth / Router Auth

`auth` option өгсөн router дээр бүх action verify хийнэ. Public/private action-ийг нэг router дотор салгаж enable/disable хийхгүй.  
When the `auth` option is configured on a router, every action is verified. Public/private actions are not enabled or disabled separately inside the same router.

```ts
type AuthContext = {
    userId: string;
};

const dti = createDTIExpressRouter<AuthContext>({
    auth: async ({ req }) => {
        const token = req.header("authorization");
        if (!token) {
            throw new DTIError("Нэвтрэх шаардлагатай", { code: "AUTH_REQUIRED", status: 401 });
        }

        return {
            userId: "user-001",
        };
    },
});

dti.action(userCreate, async ({ auth, body }) => {
    return {
        id: auth.userId,
        name: body.name,
    };
});
```

## 11. Client auth / Client Auth

Client талд global болон per-call `auth` ашиглаж болно.  
The client side supports both global and per-call `auth`.

```ts
const client = new DTIClient("/api", {
    auth: async () => ({
        authorization: `Bearer ${token}`,
    }),
});

await client.call(userCreate, param, {
    auth: async () => ({
        "x-call-auth": "call-token",
    }),
});
```

## 12. Request signing / Request Signing

Request signing нь `DTIAction.signature(param)`-ийн буцаасан string дээр суурилна. Request body-г бүхэлд нь sign хийхгүй.  
Request signing is based on the string returned by `DTIAction.signature(param)`. It does not sign the entire request body.

Contract:

```ts
export const paymentCreate = createAction("paymentCreate", {
    body: z.object({
        invoiceId: z.string(),
        amount: z.number(),
    }),
    result: z.object({
        id: z.string(),
    }),
}, {
    path: "/payments",
    method: "POST",
    contentType: "json",
    signature: ({ body }) => `${body.invoiceId}:${body.amount}`,
});
```

Client:

```ts
const client = new DTIClient("/api", {
    sign: {
        keyId: "client-a",
        secret: "secret-a",
    },
});
```

Global sign-ийг per-call түвшинд удирдах дүрэм:

- `sign` байхгүй: global sign inherit хийнэ;
- `sign: false`: тухайн request дээр client signing disable хийнэ;
- `sign: { ... }`: global sign config-ийг бүхэлд нь override хийнэ.

```ts
await client.call(publicAction, param, {
    sign: false,
});
```

`sign: false` нь зөвхөн client signing-г disable хийнэ. Server signed router хэвээр байвал request `401 / DTI_SIGNATURE_REQUIRED` авна.

Server:

```ts
import type { INonceStore } from "@napp/dti-core";

class MemoryNonceStore implements INonceStore {
    private readonly values = new Map<string, number>();

    async consume(nonce: string, ttl: number): Promise<boolean> {
        const expiresAt = this.values.get(nonce);
        if (expiresAt && expiresAt > Date.now()) {
            return false;
        }

        this.values.set(nonce, Date.now() + ttl);
        return true;
    }
}

const nonceStore = new MemoryNonceStore();

const dti = createDTIExpressRouter({
    sign: {
        nonceStore,
        toleranceMs: 5 * 60 * 1000,
        getSecret: async ({ keyId }) => {
            return keyId === "client-a" ? "secret-a" : "";
        },
    },
});
```

Production орчинд `nonceStore.consume(...)` atomic байх ёстой. Redis ашиглавал `SET NX PX` зэрэг нэг operation-оор check-and-store хийх нь тохиромжтой.  
In production, `nonceStore.consume(...)` must be atomic. With Redis, a single check-and-store operation such as `SET NX PX` is recommended.

Signing enabled үед дараах header-үүд library-аас үүснэ / When signing is enabled, the library generates the following headers:

- `x-dti-key-id`
- `x-dti-timestamp`
- `x-dti-nonce`
- `x-dti-signature`

## 13. Client-ийг server-гүй test хийх / Testing the Client Without a Server

`DTIClient` дээр custom `fetcher` өгч server асаахгүйгээр test хийж болно.  
Provide a custom `fetcher` to `DTIClient` to test without starting a server.

```ts
const fetcher: typeof fetch = async (input, init) => {
    return new Response(JSON.stringify({
        success: true,
        data: {
            id: "user-001",
            name: "Bat",
        },
    }), {
        status: 200,
        headers: {
            "Content-Type": "application/json",
        },
    });
};

const client = new DTIClient("http://test", fetcher);

const result = await client.call(userCreate, {
    body: {
        name: "Bat",
        age: 25,
    },
});
```

## Холбоотой ADR / Related ADR

- [ADR-0001: DTI server standalone REST API](./decisions/ADR-0001-dti-server-standalone-rest-api.md)
- [ADR-0002: DTI server response envelope and REST status](./decisions/ADR-0002-dti-server-response-envelope-and-rest-status.md)
- [ADR-0003: DTI response type-ийн хүрээ](./decisions/ADR-0003-dti-response-types.md)
- [ADR-0004: DTI Express router auth option](./decisions/ADR-0004-dti-express-router-auth-option.md)
- [ADR-0005: DTI client header merge order](./decisions/ADR-0005-dti-client-header-merge-order.md)
- [ADR-0006: DTI request signature болон nonce store](./decisions/ADR-0006-dti-request-signature-and-nonce-store.md)
- [ADR-0007: DTI typed path params](./decisions/ADR-0007-dti-typed-path-params.md)
- [ADR-0008: DTI configurable signing header names](./decisions/ADR-0008-configurable-dti-sign-header-names.md)
