@xrystal/dto 26.6.27-v15 → 26.6.28-v1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xrystal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -24,20 +24,116 @@ npm install express elysia
24
24
  | 2 | **`res.builder`** / **`ctx.builder`** | Inside a handler — send API response to client |
25
25
  | 3 | **`DTO.Response` / `DTO.Log` / `DTO.Queue`** | No request context — cron, logger, queue, tests |
26
26
 
27
- **Rule of thumb:** client cevabı → `builder`. Düz JSON üretmek → static `DTO.*`.
27
+ **Rule of thumb:** client response → `builder`. Plain JSON without transport → static `DTO.*`.
28
28
 
29
- ### 1. Wire the runtime (once)
29
+ ### 1. Bootstrap — create DTO once, wire one adapter
30
+
31
+ **Step A — app entry (single instance):**
30
32
 
31
33
  ```typescript
32
- import express from 'express'
33
34
  import { DTO } from '@xrystal/dto'
34
35
 
35
- const runtime = new DTO({ withDefaultHeaders: true })
36
+ export const runtime = new DTO({
37
+ withDefaultHeaders: true,
38
+ // optional: codes, plugins, socket: { event: 'api:response' }
39
+ })
40
+ ```
41
+
42
+ Create `new DTO()` **once** at startup. Handlers reuse the same `runtime`; do not create a new DTO per request.
43
+
44
+ **Step B — register the adapter for your server (once, at startup):**
45
+
46
+ Each method returns **middleware or a plugin** — the framework calls it per request/connection. You do **not** call `runtime.express(req, res)` inside handlers.
47
+
48
+ | Stack | Wire once (bootstrap) | In handler |
49
+ |-------|------------------------|------------|
50
+ | **Express** | `app.use(runtime.express())` | `res.builder.as.ok(...).send()` |
51
+ | **Node `http`** | `server.on('request', chain(runtime.node(), handler))` | `res.builder…send()` |
52
+ | **Elysia** | `app.use(runtime.elysia())` | `builder.as.ok(...).send()` |
53
+ | **Fetch / Workers** | no global wire — `runtime.forRequest(request)` per call | `builder…send()` → `Response` |
54
+ | **WebSocket** | `wss.on('connection', ws => runtime.attachWebSocket(ws))` | `ws.builder…send()` |
55
+ | **Socket.IO** | `io.use(runtime.socketIo())` or `attachSocketIo` on connect | `socket.builder…send()` |
56
+
57
+ #### Express
58
+
59
+ ```typescript
60
+ import express from 'express'
61
+ import { runtime } from './dto.js'
62
+
36
63
  const app = express()
37
- app.use(runtime.express())
64
+ app.use(runtime.express()) // ← once: attaches req.ctx + res.builder
65
+
66
+ app.get('/users/:id', (req, res) =>
67
+ res.builder.as.ok({ id: req.params.id }, 'User fetched').send(),
68
+ )
69
+ ```
70
+
71
+ #### Elysia
72
+
73
+ ```typescript
74
+ import { Elysia } from 'elysia'
75
+ import { runtime } from './dto.js'
76
+
77
+ new Elysia()
78
+ .use(runtime.elysia()) // ← once: derive injects builder, requestId, startTime
79
+ .get('/users/:id', ({ builder, requestId, params }) =>
80
+ builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }).send(),
81
+ )
38
82
  ```
39
83
 
40
- This attaches `req.ctx` and `res.builder` on every request.
84
+ #### Socket.IO
85
+
86
+ ```typescript
87
+ import { Server } from 'socket.io'
88
+ import { runtime } from './dto.js'
89
+
90
+ const io = new Server(httpServer)
91
+
92
+ // Option 1 — middleware on every connection
93
+ io.use(runtime.socketIo())
94
+
95
+ io.on('connection', (socket) => {
96
+ socket.on('getUser', ({ id }) =>
97
+ socket.builder!.as.ok({ id }, 'User fetched').send(),
98
+ )
99
+ })
100
+
101
+ // Option 2 — attach manually in the connection handler
102
+ io.on('connection', (socket) => {
103
+ runtime.attachSocketIo(socket)
104
+ // socket.builder is now available
105
+ })
106
+ ```
107
+
108
+ #### WebSocket (raw `ws`)
109
+
110
+ ```typescript
111
+ import { WebSocketServer } from 'ws'
112
+ import { runtime } from './dto.js'
113
+
114
+ const wss = new WebSocketServer({ server: httpServer })
115
+
116
+ wss.on('connection', (ws) => {
117
+ runtime.attachWebSocket(ws) // or wss.use pattern via runtime.websocket() middleware
118
+ ws.on('message', () => ws.builder!.as.ok({ pong: true }).send())
119
+ })
120
+ ```
121
+
122
+ **HTTP + realtime in one app:** wire both transports at bootstrap — e.g. `app.use(runtime.express())` and `io.use(runtime.socketIo())`.
123
+
124
+ #### With xrystal-core
125
+
126
+ In a **standalone** app you pick the adapter explicitly (`runtime.express()`, `runtime.elysia()`, …).
127
+
128
+ In **xrystal-core**, framework selection lives in core via **`dtoMiddleware(config)`** — it reads your config and wires the correct adapter (`express`, `elysia`, `socketIo`, …). DTO does not pick the framework; core does.
129
+
130
+ Standalone apps call the adapter explicitly:
131
+
132
+ ```typescript
133
+ runtime.express() // not runtime.middleware('express')
134
+ runtime.elysia()
135
+ runtime.socketIo()
136
+ ```
41
137
 
42
138
  ### 2. Respond in a handler
43
139
 
@@ -99,20 +195,6 @@ Every shape DTO shares the same chain rules:
99
195
  - `.schema({ … })` for partial input — **last call wins**
100
196
  - `payload` in `.schema()` **replaces** the entire payload
101
197
 
102
- ### Elysia — same idea
103
-
104
- ```typescript
105
- import { Elysia } from 'elysia'
106
- import { DTO } from '@xrystal/dto'
107
-
108
- const runtime = new DTO()
109
- new Elysia()
110
- .use(runtime.elysia())
111
- .get('/users/:id', ({ builder, requestId, params }) =>
112
- builder.as.ok({ id: params.id }, 'User fetched', { meta: { requestId } }).send(),
113
- )
114
- ```
115
-
116
198
  ### Fetch API — Workers, Hono, Bun.serve
117
199
 
118
200
  ```typescript
@@ -176,25 +258,29 @@ WebSocket and Socket.IO send the full transport envelope (`statusCode`, `headers
176
258
 
177
259
  ### `new DTO()` shortcuts
178
260
 
261
+ Register at **bootstrap** (once). Handlers only use `builder`.
262
+
179
263
  ```typescript
180
264
  const runtime = new DTO({ withDefaultHeaders: true, socket: { event: 'api:response' } })
181
265
 
182
- runtime.express() // → Express middleware
266
+ // HTTP — returns middleware / plugin; pass to app.use()
267
+ runtime.express() // → (req, res, next) => void
268
+ runtime.elysia() // → Elysia plugin (derive builder per request)
183
269
  runtime.node() // → Node http middleware
184
- runtime.elysia() // → Elysia plugin
185
270
  runtime.fetch() // → (request: Request) => { builder, requestId, … }
186
271
  runtime.forRequest(request) // → one-shot Fetch context
187
272
 
273
+ // Realtime — returns connection middleware or attach helper
188
274
  runtime.websocket() // → (socket, next?) => void
189
275
  runtime.attachWebSocket(ws) // → attach builder to one socket
190
276
 
191
- runtime.socketIo() // → (socket, next?) => void
277
+ runtime.socketIo() // → (socket, next?) => void (Socket.IO middleware)
192
278
  runtime.attachSocketIo(socket) // → attach builder to one socket
193
279
  ```
194
280
 
195
281
  ### When to import adapters directly
196
282
 
197
- Most apps only need `new DTO().express()` (etc.). Import adapters / sinks when you:
283
+ Most apps only need `new DTO().elysia()` or `new DTO().express()` (etc.). Import adapters / sinks when you:
198
284
 
199
285
  - Integrate a **custom framework** — `createBuilderRuntime(expressSink(res), options, context)`
200
286
  - **Unit-test** transport without full middleware
@@ -221,59 +307,59 @@ Subpath imports (tree-shake friendly):
221
307
 
222
308
  ---
223
309
 
224
- ## Core vs DTO — tek adapter yeter mi?
310
+ ## Core vs DTO — is one adapter enough?
225
311
 
226
- Kütüphanenin **iki işi** var; mimari buna göre bölündü:
312
+ The library has **two jobs**; the layout reflects that:
227
313
 
228
- | İş | Katman | Adapter gerekir mi? |
229
- |----|--------|---------------------|
230
- | **Şema üret** | `shapes/` + `shared.ts` | **Hayır** — `DTO.Log.build()`, `DTO.Response.build()`, testler |
231
- | **Client'a gönder** | `send/` + `adapters/` | **Evet** — `.send()` bir transport'a yazmalı |
314
+ | Job | Layer | Adapter required? |
315
+ |-----|-------|-------------------|
316
+ | **Build schema** | `shapes/` + `shared.ts` | **No** — `DTO.Log.build()`, `DTO.Response.build()`, tests |
317
+ | **Push to client** | `send/` + `adapters/` | **Yes** — `.send()` must write to a transport |
232
318
 
233
319
  ```
234
320
  ┌─────────────────────────────────────────────┐
235
- │ new DTO() — tek giriş noktası │
321
+ │ new DTO() — single entry point │
236
322
  │ runtime.express() / .fetch() / … │
237
323
  └──────────────────┬──────────────────────────┘
238
324
  │
239
325
  ┌──────────────────▼──────────────────────────┐
240
- │ adapters/ — sadece runtime wiring │
326
+ │ adapters/ — runtime wiring only │
241
327
  │ middleware → res.builder + send sink │
242
328
  └──────────────────┬──────────────────────────┘
243
329
  │
244
330
  ┌──────────────────▼──────────────────────────┐
245
- │ send/ — Builder, presets, transport │
246
- │ .build() şema | .send() client'a yazar │
331
+ │ send/ — Builder, presets, transport │
332
+ │ .build() schema | .send() writes client │
247
333
  └──────────────────┬──────────────────────────┘
248
334
  │
249
335
  ┌──────────────────▼──────────────────────────┐
250
- │ shapes/ — ResponseDTO, LogDTO, QueueDTO │
251
- │ field sırası, .schema() ezme kuralları │
336
+ │ shapes/ — ResponseDTO, LogDTO, QueueDTO │
337
+ │ field order, .schema() override rules │
252
338
  └─────────────────────────────────────────────┘
253
339
  ```
254
340
 
255
- ### Tek adapter bağlamak yeter mi?
341
+ ### Is wiring one adapter enough?
256
342
 
257
- | Senaryo | Ne yaparsın |
258
- |---------|-------------|
259
- | **Normal HTTP app (Express vb.)** | `new DTO().express()` — başka adapter import etmezsin |
260
- | **Sadece JSON üret (log, queue, test)** | `DTO.Log` / `DTO.Queue` / `builder.build()` — adapter yok |
261
- | **Custom framework (Fastify vb.)** | `createBuilderRuntime(expressSink(res), …)` veya kendi sink'in |
262
- | **WS + HTTP aynı app'te** | Her transport için bir kez wire: `.express()` + `.websocket()` |
343
+ | Scenario | What to do |
344
+ |----------|------------|
345
+ | **Normal HTTP app** | Pick your stack: `runtime.express()`, `runtime.elysia()`, `runtime.fetch()`, … |
346
+ | **JSON only (log, queue, test)** | `DTO.Log` / `DTO.Queue` / `builder.build()` — no adapter |
347
+ | **Custom framework (Fastify, etc.)** | `createBuilderRuntime(expressSink(res), …)` or your own sink |
348
+ | **WS + HTTP in the same app** | Wire once per transport: e.g. `.elysia()` + `.socketIo()` |
263
349
 
264
- **Özet:** Handler'dan client'a cevap gönderiyorsan bir adapter şart — ama çoğu app'te `new DTO().express()` tek satır yeter. Adapter request başında sink'i builder'a **takır**; sink yalnızca `.send()` çağrıldığında çalışır. `.build()` saf şema döner, transport'a dokunmaz.
350
+ **Summary:** If you send handler responses to a client, you need an adapter — pick the one that matches your server. The adapter **attaches** the sink to the builder at request start; the sink runs **only** when `.send()` is called. `.build()` returns pure schema and never touches transport.
265
351
 
266
- | Layer | Import eder misin? | Ne yapar |
267
- |-------|-------------------|----------|
268
- | **shapes** | Nadiren (`@xrystal/dto/log`) | Saf JSON şema |
269
- | **send** | Nadiren (`@xrystal/dto/response`) | Builder, transport, presets |
270
- | **adapters** | Custom entegrasyon / subpath | `builder`'ı request'e takar |
271
- | **DTO** | Her zaman | Config + `runtime.express()` shortcut |
352
+ | Layer | Do you import it? | Role |
353
+ |-------|-------------------|------|
354
+ | **shapes** | Rarely (`@xrystal/dto/log`) | Pure JSON schema |
355
+ | **send** | Rarely (`@xrystal/dto/response`) | Builder, transport, presets |
356
+ | **adapters** | Custom integration / subpath | Attaches `builder` to each request |
357
+ | **DTO** | Always | Config + `runtime.elysia()` / `runtime.express()` shortcuts |
272
358
 
273
359
  ### Request lifecycle (Express)
274
360
 
275
361
  ```
276
- 1. Request gelir
362
+ 1. Request arrives
277
363
  2. ExpressAdapter middleware:
278
364
  ContextFactory → requestId
279
365
  createBuilderRuntime(expressSink(res), options, ctx)
@@ -284,7 +370,7 @@ Kütüphanenin **iki işi** var; mimari buna göre bölündü:
284
370
  → .send() → transport.ts → expressSink → res.json()
285
371
  ```
286
372
 
287
- Şema step 2'de oluşmaz — `.build()` veya `.send()` çağrıldığında assemble edilir.
373
+ Schema is **not** assembled at step 2 — only when you call `.build()` or `.send()`.
288
374
 
289
375
  ---
290
376
 
@@ -294,22 +380,24 @@ Kütüphanenin **iki işi** var; mimari buna göre bölündü:
294
380
  src/dto/
295
381
  ├── shared.ts BaseDTO, field rules, builder config
296
382
  ├── factories.ts ContextFactory, MetadataFactory
297
- ├── shapes/ ← görev 1: şema
383
+ ├── shapes/ ← job 1: schema
298
384
  │ ├── response.ts
299
385
  │ ├── log.ts
300
386
  │ └── queue.ts
301
- ├── send/ ← görev 2: build + push
387
+ ├── send/ ← job 2: build + push
302
388
  │ ├── builder.ts
303
389
  │ ├── presets.ts
304
390
  │ ├── transport.ts
305
391
  │ └── plugins.ts
306
392
  ├── adapters/ runtime wiring
307
- │ ├── http.ts Express, Node, Fetch, Elysia + *Sink helpers
393
+ │ ├── http.ts Express, Node, Fetch + *Sink helpers
394
+ │ ├── elysia-adapter.ts Elysia plugin + elysiaSink (isolated for tree-shaking)
308
395
  │ ├── realtime.ts WebSocket, Socket.IO
309
396
  │ ├── bind.ts DTO.express() shortcuts
310
397
  │ └── express.ts … @xrystal/dto/express subpath shims
311
398
  └── runtime/
312
- └── index.ts DTO class, Hooks, createBuilderRuntime
399
+ ├── builder-runtime.ts createBuilderRuntime (adapter-safe import)
400
+ └── index.ts DTO class, Hooks
313
401
  ```
314
402
 
315
403
  Subpath exports (`@xrystal/dto/express`, `/response`, …) unchanged — shims map to the folders above.
@@ -318,7 +406,7 @@ Subpath exports (`@xrystal/dto/express`, `/response`, …) unchanged — shims m
318
406
 
319
407
  | Import | Contents |
320
408
  |--------|----------|
321
- | `@xrystal/dto` | Everything (including all adapters & dispatchers) |
409
+ | `@xrystal/dto` | Everything (including all adapters & send sinks) |
322
410
  | `@xrystal/dto/dto` | `DTO`, `createDTO`, runtime helpers |
323
411
  | `@xrystal/dto/response` | `ResponseDTO`, `Builder`, `PresetTerminal`, transport helpers |
324
412
  | `@xrystal/dto/log` | `LogDTO` |
@@ -545,6 +633,7 @@ See source `dto/send/presets.ts` for the full list.
545
633
 
546
634
  ### Breaking / API changes
547
635
 
636
+ - **Removed `runtime.middleware()`** — DTO no longer resolves framework from a string. Pick an adapter explicitly (`runtime.express()`, `runtime.elysia()`, `runtime.socketIo()`, …) or use **`dtoMiddleware(config)`** from **xrystal-core** for config-driven wiring. Removed exports: `normalizeRuntimeKind`, `RuntimeKind`.
548
637
  - **Explicit terminal verbs** — every handler chain must end with `.send()` (dispatch to client) or `.build()` (schema only). No automatic dispatch.
549
638
  - **Preset terminal** — `.as.ok()`, `.as.fail()`, … return `PresetTerminal` with only `.build()` and `.send()`. No further chaining after presets (put headers **before** preset: `builder.header('X').as.ok(data).send()`).
550
639
  - **WebSocket / Socket.IO wire format** — emits full transport envelope (`statusCode`, `headers`, `cookies`, `body`) instead of bare body JSON. Update clients if they parsed the old shape.
@@ -564,12 +653,26 @@ See source `dto/send/presets.ts` for the full list.
564
653
  ### Package surface
565
654
 
566
655
  | Runtimes | 6 — Express, Node, Fetch, Elysia, WebSocket, Socket.IO |
567
- | Tests | 77 passing |
656
+ | Tests | 79 passing |
568
657
  | Subpaths | `/express`, `/node`, `/fetch`, `/elysia`, `/websocket`, `/socketio`, `/response`, `/log`, `/queue`, `/dto` |
569
658
 
570
659
  ### Migration cheat sheet
571
660
 
572
661
  ```typescript
662
+ // Removed — use explicit adapter or xrystal-core dtoMiddleware(config)
663
+ runtime.middleware('express') // ✗
664
+ runtime.middleware() // ✗
665
+ runtime.middleware('socketio') // ✗
666
+
667
+ // Standalone
668
+ app.use(runtime.express())
669
+ app.use(runtime.elysia())
670
+ io.use(runtime.socketIo())
671
+
672
+ // xrystal-core (config-driven)
673
+ import { dtoMiddleware } from 'xrystal-core/handlers' // your core export path
674
+ app.use(dtoMiddleware(config))
675
+
573
676
  // Before
574
677
  return res.builder.as.ok(data, 'Done')
575
678
 
@@ -585,16 +688,25 @@ const body = res.builder.as.ok(data).build()
585
688
 
586
689
  ---
587
690
 
588
- ## Release (maintainers)
691
+ ## Contributing
589
692
 
590
- Version lives in `package.json` (`YY.MM.DD-vN`). Git tag must match exactly.
693
+ Contributions are welcome. Open an issue to discuss larger changes, or submit a pull request with a focused diff and tests where behaviour changes.
694
+
695
+ Before opening a PR, run:
591
696
 
592
697
  ```bash
593
- npm run release # lint → test → build → commit → tag → push
594
- npm run tag # tag only (after manual commit)
595
- npm run version:check
698
+ npm test
699
+ npm run lint
596
700
  ```
597
701
 
598
- ---
702
+ ## License
703
+
704
+ [MIT](LICENSE) © [xrystal](https://github.com/xdloper)
705
+
706
+ ## Links
599
707
 
600
- MIT
708
+ | | |
709
+ |---|---|
710
+ | **npm** | [`@xrystal/dto`](https://www.npmjs.com/package/@xrystal/dto) |
711
+ | **Repository** | [github.com/xdloper/dto](https://github.com/xdloper/dto) |
712
+ | **Issues** | [github.com/xdloper/dto/issues](https://github.com/xdloper/dto/issues) |