@swimmesberger/elarion-jsonrpc-client-generator 0.2.3-preview.59.1 → 0.2.3-preview.60.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @swimmesberger/elarion-jsonrpc-client-generator
2
2
 
3
- Generate TypeScript RPC method contracts, Zod result schemas, and a portable fetch-based JSON-RPC client from an Elarion `rpc-schema.json` export.
3
+ Generate TypeScript RPC method contracts, Zod params/result schemas, and a portable fetch-based JSON-RPC client from an Elarion `rpc-schema.json` export.
4
4
 
5
5
  ```bash
6
6
  npm install --save-dev @swimmesberger/elarion-jsonrpc-client-generator
@@ -12,8 +12,8 @@ The generated files are:
12
12
  | File | Purpose |
13
13
  | --- | --- |
14
14
  | `rpc-types.ts` | `RpcMethods` interface mapping method names to params/result types. |
15
- | `rpc-schemas.ts` | `rpcResultSchemas` Zod map for runtime result validation. |
16
- | `rpc-client.ts` | Browser/Node.js fetch client with typed single calls, batching, headers, `AbortSignal`, JSON-RPC errors, and Zod-backed result validation. |
15
+ | `rpc-schemas.ts` | `rpcParamsSchemas` and `rpcResultSchemas` Zod maps for runtime params/result validation. |
16
+ | `rpc-client.ts` | Browser/Node.js fetch client with typed single calls, batching, headers, `AbortSignal`, JSON-RPC errors, and Zod-backed params/result validation. |
17
17
 
18
18
  The generated schema and client files import `zod`, so consuming applications should install `zod` as a runtime dependency.
19
19
 
@@ -57,6 +57,8 @@ const [clientResult, projectsResult] = await rpc.$batch([
57
57
 
58
58
  Result validation is enabled by default through `rpcResultSchemas`. Use `transformResult` for app-specific normalization before validation, or set `validateResults: false` when another layer validates responses.
59
59
 
60
+ Params validation is also enabled by default through `rpcParamsSchemas`: single calls and batch items are checked against the exported schema's constraints (lengths, ranges, patterns, formats) before the request is sent, and a failure throws `RpcParamsValidationError` locally — naming the method, with the underlying Zod error as `cause` — so tier-1 contract violations never reach the wire. Set `validateParams: false` to opt out.
61
+
60
62
  ## Client-side tracing
61
63
 
62
64
  The generated client never imports an OpenTelemetry SDK. Instead it exposes an optional `instrumentation` hook so client-side tracing stays a host decision and adds zero dependencies. The client calls `startSpan` once per request (and once per batch), reads the returned span's `headers` to inject trace context into the outgoing request, then calls `setError`/`end` as the request settles:
package/dist/generate.js CHANGED
@@ -38,8 +38,18 @@ export function generateRpcClientFiles(schema, options = {}) {
38
38
  '',
39
39
  "import { z } from 'zod'",
40
40
  '',
41
- 'export const rpcResultSchemas = {',
41
+ 'export const rpcParamsSchemas = {',
42
42
  ];
43
+ for (const method of methods) {
44
+ const definition = schema.methods[method];
45
+ const zodSchema = jsonSchemaToZod(stripNullable(definition.params), createContext(definition.params, `methods.${method}.params`), 1);
46
+ schemasLines.push(` ${JSON.stringify(method)}: ${zodSchema},`);
47
+ }
48
+ schemasLines.push('} as const');
49
+ schemasLines.push('');
50
+ schemasLines.push('export type RpcParamsSchemas = typeof rpcParamsSchemas');
51
+ schemasLines.push('');
52
+ schemasLines.push('export const rpcResultSchemas = {');
43
53
  for (const method of methods) {
44
54
  const definition = schema.methods[method];
45
55
  const zodSchema = jsonSchemaToZod(stripNullable(definition.result), createContext(definition.result, `methods.${method}.result`), 1);
@@ -19,17 +19,17 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
19
19
  return nullish(zodExpression, nullable);
20
20
  }
21
21
  if (base === 'string') {
22
- return nullish('z.string()', nullable);
22
+ return nullish(stringSchema(resolved), nullable);
23
23
  }
24
24
  if (base === 'number' || base === 'integer') {
25
- return nullish('z.number()', nullable);
25
+ return nullish(numberSchema(resolved, base), nullable);
26
26
  }
27
27
  if (base === 'boolean') {
28
28
  return nullish('z.boolean()', nullable);
29
29
  }
30
30
  if (base === 'array' && resolved.items) {
31
31
  const itemSchema = jsonSchemaToZod(stripNullable(resolved.items), childContext(ctx, 'items'), indent);
32
- return nullish(`z.array(${itemSchema})`, nullable);
32
+ return nullish(arraySchema(`z.array(${itemSchema})`, resolved), nullable);
33
33
  }
34
34
  if (base === 'object' && resolved.properties) {
35
35
  const required = new Set(resolved.required ?? []);
@@ -48,3 +48,59 @@ export function jsonSchemaToZod(schema, ctx, indent = 0) {
48
48
  function nullish(expression, nullable) {
49
49
  return nullable ? `${expression}.nullish()` : expression;
50
50
  }
51
+ // Constraint keywords apply to the inner type in a deterministic modifier order — base type, then
52
+ // `.int()`, then bounds (min-side before max-side), then format/regex refinements — with `.nullish()`
53
+ // always appended last by the caller.
54
+ function stringSchema(schema) {
55
+ let expression = 'z.string()';
56
+ if (schema.minLength !== undefined) {
57
+ expression += `.min(${schema.minLength})`;
58
+ }
59
+ if (schema.maxLength !== undefined) {
60
+ expression += `.max(${schema.maxLength})`;
61
+ }
62
+ if (schema.format === 'uuid') {
63
+ expression += '.uuid()';
64
+ }
65
+ else if (schema.format === 'email') {
66
+ expression += '.email()';
67
+ }
68
+ else if (schema.format === 'uri') {
69
+ expression += '.url()';
70
+ }
71
+ if (schema.pattern !== undefined) {
72
+ // new RegExp(<JSON string literal>) is deterministic and safe for any pattern content (slashes,
73
+ // quotes, backslashes) — a /.../ regex literal would need its own escaping rules.
74
+ expression += `.regex(new RegExp(${JSON.stringify(schema.pattern)}))`;
75
+ }
76
+ return expression;
77
+ }
78
+ function numberSchema(schema, base) {
79
+ let expression = 'z.number()';
80
+ if (base === 'integer') {
81
+ expression += '.int()';
82
+ }
83
+ if (schema.minimum !== undefined) {
84
+ expression += `.gte(${schema.minimum})`;
85
+ }
86
+ if (schema.exclusiveMinimum !== undefined) {
87
+ expression += `.gt(${schema.exclusiveMinimum})`;
88
+ }
89
+ if (schema.maximum !== undefined) {
90
+ expression += `.lte(${schema.maximum})`;
91
+ }
92
+ if (schema.exclusiveMaximum !== undefined) {
93
+ expression += `.lt(${schema.exclusiveMaximum})`;
94
+ }
95
+ return expression;
96
+ }
97
+ function arraySchema(expression, schema) {
98
+ let result = expression;
99
+ if (schema.minItems !== undefined) {
100
+ result += `.min(${schema.minItems})`;
101
+ }
102
+ if (schema.maxItems !== undefined) {
103
+ result += `.max(${schema.maxItems})`;
104
+ }
105
+ return result;
106
+ }
@@ -10,7 +10,7 @@ export function generateRpcClientSource(options) {
10
10
  `// Source: ${options.sourceLabel}`,
11
11
  '',
12
12
  `import type { RpcMethods } from '${typesImport}'`,
13
- `import { rpcResultSchemas } from '${schemasImport}'`,
13
+ `import { rpcParamsSchemas, rpcResultSchemas } from '${schemasImport}'`,
14
14
  '',
15
15
  'export type RpcMethod = keyof RpcMethods & string',
16
16
  "export type RpcParams<M extends RpcMethod> = RpcMethods[M]['params']",
@@ -61,6 +61,9 @@ export function generateRpcClientSource(options) {
61
61
  ' readonly fetch?: RpcFetch',
62
62
  ' readonly headers?: RpcHeaders',
63
63
  ' readonly idGenerator?: () => string | number',
64
+ ' // Validate request params against the generated Zod params schemas before sending. Defaults to true;',
65
+ ' // a failure throws RpcParamsValidationError locally without hitting the wire.',
66
+ ' readonly validateParams?: boolean',
64
67
  ' readonly validateResults?: boolean',
65
68
  ' readonly transformResult?: <M extends RpcMethod>(method: M, result: unknown) => unknown',
66
69
  ' readonly instrumentation?: RpcInstrumentation',
@@ -181,6 +184,19 @@ export function generateRpcClientSource(options) {
181
184
  ' }',
182
185
  '}',
183
186
  '',
187
+ '// Thrown locally (before the request reaches the wire) when params fail the generated Zod',
188
+ '// params schema. `cause` carries the underlying Zod error with the structured issues.',
189
+ 'export class RpcParamsValidationError extends Error {',
190
+ ' readonly method: RpcMethod',
191
+ '',
192
+ ' constructor(method: RpcMethod, cause: unknown) {',
193
+ ' const detail = cause instanceof Error ? `: ${cause.message}` : ' + "'.'",
194
+ ' super(`Invalid params for RPC method ${JSON.stringify(method)}${detail}`, { cause })',
195
+ " this.name = 'RpcParamsValidationError'",
196
+ ' this.method = method',
197
+ ' }',
198
+ '}',
199
+ '',
184
200
  'export function createRpcApi(options: RpcClientOptions): RpcApi {',
185
201
  ' const client = createRpcClient(options)',
186
202
  ' const calls: Record<string, unknown> = {}',
@@ -211,6 +227,7 @@ export function generateRpcClientSource(options) {
211
227
  '',
212
228
  ' let nextId = 1',
213
229
  ' const createId = options.idGenerator ?? (() => globalThis.crypto?.randomUUID?.() ?? String(nextId++))',
230
+ ' const validateParams = options.validateParams ?? true',
214
231
  ' const validateResults = options.validateResults ?? true',
215
232
  ' const idempotencyEnabled = options.idempotency?.enabled ?? true',
216
233
  ' const generateIdempotencyKey = options.idempotency?.generateKey ?? defaultIdempotencyKey',
@@ -244,6 +261,22 @@ export function generateRpcClientSource(options) {
244
261
  ' return response.json()',
245
262
  ' }',
246
263
  '',
264
+ ' // Pre-flight params validation runs on the caller-supplied params, before the idempotency key is',
265
+ ' // merged into params._meta (the params schema does not know _meta) and before any request is built,',
266
+ ' // so a failure never reaches the wire. The original params object is sent unchanged — Zod object',
267
+ ' // parsing strips unknown keys, so the parse output is deliberately discarded.',
268
+ ' function assertValidParams<M extends RpcMethod>(method: M, params: RpcParams<M>): void {',
269
+ ' if (!validateParams) {',
270
+ ' return',
271
+ ' }',
272
+ '',
273
+ ' try {',
274
+ ' rpcParamsSchemas[method].parse(params)',
275
+ ' } catch (error) {',
276
+ ' throw new RpcParamsValidationError(method, error)',
277
+ ' }',
278
+ ' }',
279
+ '',
247
280
  ' function parseResult<M extends RpcMethod>(method: M, result: unknown): RpcResult<M> {',
248
281
  ' const transformed = options.transformResult?.(method, result) ?? result',
249
282
  ' if (!validateResults) {',
@@ -259,6 +292,7 @@ export function generateRpcClientSource(options) {
259
292
  ' params: RpcParams<M>,',
260
293
  ' requestOptions?: RpcRequestOptions',
261
294
  ' ): Promise<RpcResult<M>> {',
295
+ ' assertValidParams(method, params)',
262
296
  ' const idempotencyKey = resolveIdempotencyKey(method, requestOptions?.idempotencyKey)',
263
297
  ' const request = createRequest(method, withIdempotencyKey(params, idempotencyKey), createId())',
264
298
  ' const context: RpcRequestContext = { methods: [method], batch: false }',
@@ -291,6 +325,11 @@ export function generateRpcClientSource(options) {
291
325
  ' return [] as unknown as BatchResult<T>',
292
326
  ' }',
293
327
  '',
328
+ ' // Validate every item before building any request, so an invalid batch never partially sends.',
329
+ ' for (const item of items) {',
330
+ ' assertValidParams(item.method, item.params)',
331
+ ' }',
332
+ '',
294
333
  ' const requests = items.map((item) =>',
295
334
  ' createRequest(',
296
335
  ' item.method,',
package/dist/schema.d.ts CHANGED
@@ -10,6 +10,15 @@ export interface JsonSchema {
10
10
  oneOf?: unknown[];
11
11
  anyOf?: unknown[];
12
12
  allOf?: unknown[];
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ pattern?: string;
16
+ minimum?: number;
17
+ maximum?: number;
18
+ exclusiveMinimum?: number;
19
+ exclusiveMaximum?: number;
20
+ minItems?: number;
21
+ maxItems?: number;
13
22
  }
14
23
  export interface RpcMethodSchema {
15
24
  params: JsonSchema;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmesberger/elarion-jsonrpc-client-generator",
3
- "version": "0.2.3-preview.59.1",
3
+ "version": "0.2.3-preview.60.1",
4
4
  "description": "Generate TypeScript RPC contracts, Zod schemas, and a fetch client from Elarion JSON-RPC schema exports.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",