@emeryld/rrroutes-contract 2.1.6 → 2.1.7

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.
Files changed (2) hide show
  1. package/README.md +216 -22
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
+ <!--
2
+ Summary:
3
+ - Added end-to-end examples: quick start registry build/consume, cache keys, path interpolation, CRUD toggles/extras, and socket event validation.
4
+ - Removed references to deprecated auth flags/legacy CLI notes; focused on current exports only.
5
+ - Missing (future): planned socket middleware/onReceive helpers from the TODO block once implemented.
6
+ -->
7
+
1
8
  # @emeryld/rrroutes-contract
2
9
 
3
- Core builder + registry utilities for RRRoutes. The contract package exposes the fluent DSL (`resource`, `withCrud`), the `finalize` helper, cache-key utilities, and all TypeScript inference helpers used by both the client and server packages.
10
+ Type-safe contract toolkit for RRRoutes. Ship the HTTP route DSL (`resource` + `withCrud`), registry/finalization helpers, cache-key builder used by the client package, and shared socket event contracts.
4
11
 
5
12
  ## Installation
6
13
 
@@ -10,52 +17,239 @@ pnpm add @emeryld/rrroutes-contract
10
17
  npm install @emeryld/rrroutes-contract
11
18
  ```
12
19
 
13
- `zod` ships as a dependency so you do not need to install it separately.
20
+ `zod` ships as a dependency—nothing extra to install.
14
21
 
15
- ## Quick start
22
+ ## Quick start (build + consume a registry)
16
23
 
17
24
  ```ts
18
- import { resource, finalize } from '@emeryld/rrroutes-contract';
25
+ import {
26
+ buildCacheKey,
27
+ compilePath,
28
+ finalize,
29
+ InferOutput,
30
+ InferParams,
31
+ InferQuery,
32
+ resource,
33
+ } from '@emeryld/rrroutes-contract';
19
34
  import { z } from 'zod';
20
35
 
21
- const users = resource('/v1')
36
+ // 1) Describe your API
37
+ const leaves = resource('/v1')
22
38
  .sub('users', (users) =>
23
39
  users
24
40
  .get({
25
- querySchema: z.object({ search: z.string().optional() }),
41
+ querySchema: z.object({
42
+ search: z.string().optional(),
43
+ limit: z.coerce.number().min(1).max(50).default(20),
44
+ }),
26
45
  outputSchema: z.array(z.object({ id: z.string().uuid(), email: z.string().email() })),
46
+ description: 'Find users',
27
47
  })
28
48
  .routeParameter('userId', z.string().uuid(), (user) =>
29
- user.get({
30
- outputSchema: z.object({ id: z.string().uuid(), email: z.string().email() }),
49
+ user.patch({
50
+ bodySchema: z.object({ name: z.string().min(1) }),
51
+ outputSchema: z.object({ ok: z.literal(true) }),
31
52
  }),
32
53
  )
33
54
  .done(),
34
55
  )
35
56
  .done();
36
57
 
37
- export const registry = finalize([...users]);
58
+ // 2) Freeze it into a registry for typed lookups
59
+ export const registry = finalize(leaves);
60
+
61
+ // 3) Consume a leaf with full types
62
+ const leaf = registry.byKey['PATCH /v1/users/:userId'];
63
+ type Params = InferParams<typeof leaf>; // { userId: string }
64
+ type Query = InferQuery<typeof leaf>; // never (no query)
65
+ type Output = InferOutput<typeof leaf>; // { ok: true }
66
+
67
+ // 4) Build URLs + cache keys (React Query friendly)
68
+ const url = compilePath(leaf.path, { userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2' });
69
+ const key = buildCacheKey({ leaf, params: { userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2' } });
70
+ // key => ['patch', 'v1', 'users', 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2', { userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2' }]
38
71
  ```
39
72
 
40
- `registry.all` retains the readonly tuple of leaves, while `registry.byKey['GET /v1/users']` gives you autocomplete-safe access to a single endpoint.
73
+ ## Detailed usage
41
74
 
42
- ## Scripts
75
+ ### Fluent route builder
43
76
 
44
- Run everything from the repo root using pnpm:
77
+ ```ts
78
+ import { resource } from '@emeryld/rrroutes-contract';
79
+ import { z } from 'zod';
45
80
 
46
- ```sh
47
- pnpm install
48
- pnpm --filter @emeryld/rrroutes-contract build # tsup + d.ts
49
- pnpm --filter @emeryld/rrroutes-contract test # optional, via Jest
81
+ const leaves = resource('/api') // base path, optional inherited cfg
82
+ .with({ feed: false }) // merges flags into descendants (extend NodeCfg via declaration merging if you add your own)
83
+ .sub('projects', (projects) =>
84
+ projects
85
+ .get({
86
+ feed: true, // infinite/feed for clients
87
+ querySchema: z.object({ cursor: z.string().optional(), limit: z.coerce.number().default(25) }),
88
+ outputSchema: z.object({
89
+ items: z.array(z.object({ id: z.string(), name: z.string() })),
90
+ nextCursor: z.string().optional(),
91
+ }),
92
+ })
93
+ .post({
94
+ bodySchema: z.object({ name: z.string().min(1) }),
95
+ outputSchema: z.object({ id: z.string(), name: z.string() }),
96
+ description: 'Create a project',
97
+ })
98
+ .routeParameter('projectId', z.string().uuid(), (project) =>
99
+ project
100
+ .get({ outputSchema: z.object({ id: z.string(), name: z.string() }) })
101
+ .patch({
102
+ bodySchema: z.object({ name: z.string().min(1) }),
103
+ outputSchema: z.object({ id: z.string(), name: z.string() }),
104
+ })
105
+ .sub('avatar', (avatar) =>
106
+ avatar
107
+ .put({
108
+ bodyFiles: [{ name: 'avatar', maxCount: 1 }], // signals multipart upload
109
+ bodySchema: z.object({ avatar: z.instanceof(Blob) }),
110
+ outputSchema: z.object({ ok: z.literal(true) }),
111
+ })
112
+ .done(),
113
+ )
114
+ .done(),
115
+ )
116
+ .done(),
117
+ )
118
+ .done();
50
119
  ```
51
120
 
52
- ## Publishing
121
+ - `sub(name, [cfg], builder?)` nests paths (`/api/projects`).
122
+ - `routeParameter(name, schema, builder)` creates `/:name` segments and merges param schemas downward.
123
+ - Methods (`get/post/put/patch/delete`) merge the active param schema unless you override via `paramsSchema`.
124
+ - `done()` closes a branch and returns the collected readonly tuple of leaves.
53
125
 
54
- Once the workspace is built, publish just this package from its folder:
126
+ ### Registry helpers, URL building, and typing
55
127
 
56
- ```sh
57
- cd packages/contract
58
- npm publish --access public
128
+ ```ts
129
+ import {
130
+ buildCacheKey,
131
+ compilePath,
132
+ finalize,
133
+ InferBody,
134
+ InferOutput,
135
+ SubsetRoutes,
136
+ } from '@emeryld/rrroutes-contract';
137
+
138
+ const registry = finalize(leaves);
139
+ const leaf = registry.byKey['PATCH /api/projects/:projectId'];
140
+
141
+ // TypeScript helpers
142
+ type Body = InferBody<typeof leaf>; // { name: string }
143
+ type Output = InferOutput<typeof leaf>; // { id: string; name: string }
144
+
145
+ // Runtime helpers
146
+ const url = compilePath(leaf.path, { projectId: '123' }); // "/api/projects/123"
147
+ const cacheKey = buildCacheKey({ leaf, params: { projectId: '123' } });
148
+
149
+ // Typed subsets for routers/microfrontends
150
+ type ProjectRoutes = SubsetRoutes<typeof registry.all, '/api/projects'>;
151
+ ```
152
+
153
+ - `finalize(leaves)` freezes the tuple and provides `byKey['METHOD /path']`, `all`, and `log(logger)`.
154
+ - `compilePath` throws if required params are missing; wrap user-provided values in try/catch.
155
+ - `buildCacheKey` produces the deterministic tuple the client package uses for React Query; reuse it for manual invalidation.
156
+
157
+ ### CRUD helper (`withCrud` / `resourceWithCrud`)
158
+
159
+ ```ts
160
+ import { CrudDefaultPagination, finalize, resource, withCrud } from '@emeryld/rrroutes-contract';
161
+ import { z } from 'zod';
162
+
163
+ const r = withCrud(resource('/v1'));
164
+
165
+ const leaves = r
166
+ .crud(
167
+ 'articles',
168
+ {
169
+ paramSchema: z.string().uuid(), // value schema; becomes :articlesId
170
+ itemOutputSchema: z.object({ id: z.string().uuid(), title: z.string(), body: z.string() }),
171
+ list: { querySchema: CrudDefaultPagination },
172
+ create: { bodySchema: z.object({ title: z.string(), body: z.string() }) },
173
+ update: { bodySchema: z.object({ title: z.string().optional(), body: z.string().optional() }) },
174
+ enable: { remove: false }, // opt out of DELETE
175
+ },
176
+ ({ collection }) =>
177
+ collection
178
+ .sub('stats', (stats) =>
179
+ stats
180
+ .get({
181
+ outputSchema: z.object({ total: z.number() }),
182
+ description: 'Extra endpoint alongside CRUD',
183
+ })
184
+ .done(),
185
+ )
186
+ .done(),
187
+ )
188
+ .done();
189
+
190
+ const registry = finalize(leaves);
191
+ // registry.byKey now includes the CRUD + extras routes with full types
59
192
  ```
60
193
 
61
- Make sure the version in `packages/contract/package.json` matches what you intend to release.
194
+ - Generated routes (unless disabled): GET feed list, POST create (requires `create.bodySchema`), GET item, PATCH update (requires `update.bodySchema`), DELETE remove.
195
+ - Defaults: list output `{ items: Item[], nextCursor?: string }`, remove output `{ ok: true }`.
196
+ - Pass `paramSchema` as a value schema; a compatible `z.object({ <name>Id: schema })` also works at runtime.
197
+ - `resourceWithCrud('/v1', {})` is a convenience wrapper if you want the `.crud` method available immediately.
198
+
199
+ ### Socket event contracts
200
+
201
+ Share a typed event map between client and server.
202
+
203
+ ```ts
204
+ import { defineSocketEvents, Payload } from '@emeryld/rrroutes-contract';
205
+ import { z } from 'zod';
206
+
207
+ const { config, events } = defineSocketEvents(
208
+ {
209
+ joinMetaMessage: z.object({ room: z.string() }),
210
+ leaveMetaMessage: z.object({ room: z.string() }),
211
+ pingPayload: z.object({ clientEcho: z.object({ sentAt: z.string() }) }),
212
+ pongPayload: z.object({ clientEcho: z.object({ sentAt: z.string() }).optional(), sinceMs: z.number().optional() }),
213
+ },
214
+ {
215
+ 'chat:message': { message: z.object({ roomId: z.string(), text: z.string(), userId: z.string() }) },
216
+ 'typing:update': { message: z.object({ roomId: z.string(), userId: z.string(), typing: z.boolean() }) },
217
+ },
218
+ );
219
+
220
+ // Typed payload extraction
221
+ type ChatPayload = Payload<typeof events, 'chat:message'>;
222
+ // ChatPayload -> { roomId: string; text: string; userId: string }
223
+
224
+ // Server-side guard example
225
+ function onChatMessage(raw: unknown) {
226
+ const parsed = events['chat:message'].message.parse(raw);
227
+ // parsed is strongly typed; safe to broadcast
228
+ }
229
+ ```
230
+
231
+ `config` mirrors the system events used by the socket client/server packages; `events` holds your app-specific payload schemas.
232
+
233
+ ### Common patterns/recipes
234
+
235
+ - **Module-per-area:** export leaf tuples per domain (`usersLeaves`, `projectsLeaves`), then spread before `finalize([...usersLeaves, ...projectsLeaves])`.
236
+ - **Shared defaults:** wrap `resource('/api')` in your own helper that immediately calls `.with(...)` for cross-cutting flags you add via declaration merging (e.g., auth tags, tracing hints).
237
+ - **React Query integration:** use `buildCacheKey` + `queryClient.invalidateQueries({ queryKey })` to keep caches in sync, or rely on the client package’s helpers which wrap the same logic.
238
+ - **Error handling at boundaries:** catch `compilePath` errors when interpolating user-provided params; surface a 400 instead of crashing your handler.
239
+
240
+ ### Edge cases and notes
241
+
242
+ - `paramsSchema` on a method overrides the merged schema from `routeParameter`—useful when you need stricter validation per verb.
243
+ - `bodyFiles` marks a route as multipart; servers can attach upload middleware, and clients should send `FormData`.
244
+ - CRUD helper only emits create/update routes when the matching `bodySchema` is provided; delete can be disabled via `enable.remove: false`.
245
+ - Feed-only behavior (`feed: true`) is intended for GET endpoints; clients treat them as infinite queries.
246
+
247
+ ## Scripts (monorepo)
248
+
249
+ Run from the repo root:
250
+
251
+ ```sh
252
+ pnpm --filter @emeryld/rrroutes-contract build # tsup + d.ts
253
+ pnpm --filter @emeryld/rrroutes-contract typecheck
254
+ pnpm --filter @emeryld/rrroutes-contract test # optional Jest suite
255
+ ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-contract",
3
3
  "description": "TypeScript contract definitions for RRRoutes",
4
- "version": "2.1.6",
4
+ "version": "2.1.7",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",