ts-procedures 5.3.0 → 5.4.0

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 (38) hide show
  1. package/README.md +90 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
  4. package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
  5. package/agent_config/claude-code/skills/guide/patterns.md +155 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +22 -0
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
  8. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  9. package/agent_config/copilot/copilot-instructions.md +35 -0
  10. package/agent_config/cursor/cursorrules +35 -0
  11. package/build/implementations/http/hono-api/index.d.ts +102 -0
  12. package/build/implementations/http/hono-api/index.js +339 -0
  13. package/build/implementations/http/hono-api/index.js.map +1 -0
  14. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  15. package/build/implementations/http/hono-api/index.test.js +983 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  17. package/build/implementations/http/hono-api/types.d.ts +13 -0
  18. package/build/implementations/http/hono-api/types.js +2 -0
  19. package/build/implementations/http/hono-api/types.js.map +1 -0
  20. package/build/implementations/types.d.ts +44 -0
  21. package/build/index.d.ts +28 -6
  22. package/build/index.js +28 -0
  23. package/build/index.js.map +1 -1
  24. package/build/schema/compute-schema.d.ts +5 -0
  25. package/build/schema/compute-schema.js +8 -1
  26. package/build/schema/compute-schema.js.map +1 -1
  27. package/build/schema/parser.d.ts +6 -5
  28. package/build/schema/parser.js +54 -0
  29. package/build/schema/parser.js.map +1 -1
  30. package/package.json +8 -3
  31. package/src/implementations/http/README.md +45 -2
  32. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  33. package/src/implementations/http/hono-api/index.ts +461 -0
  34. package/src/implementations/http/hono-api/types.ts +16 -0
  35. package/src/implementations/types.ts +52 -0
  36. package/src/index.ts +87 -10
  37. package/src/schema/compute-schema.ts +23 -2
  38. package/src/schema/parser.ts +70 -3
package/src/index.ts CHANGED
@@ -26,9 +26,11 @@ export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown
26
26
  schema?: {
27
27
  params?: TJSONSchema
28
28
  returnType?: TJSONSchema
29
+ input?: Record<string, TJSONSchema>
29
30
  }
30
31
  validation?: {
31
32
  params?: (params: any) => { errors?: any[] }
33
+ input?: Record<string, (value: any) => { errors?: any[] }>
32
34
  }
33
35
  } & TExtendedConfig
34
36
 
@@ -44,10 +46,12 @@ export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = u
44
46
  params?: TJSONSchema
45
47
  yieldType?: TJSONSchema
46
48
  returnType?: TJSONSchema
49
+ input?: Record<string, TJSONSchema>
47
50
  }
48
51
  validation?: {
49
52
  params?: (params: any) => { errors?: any[] }
50
53
  yield?: (value: any) => { errors?: any[] }
54
+ input?: Record<string, (value: any) => { errors?: any[] }>
51
55
  }
52
56
  } & TExtendedConfig
53
57
 
@@ -73,10 +77,12 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
73
77
  params?: TJSONSchema
74
78
  yieldType?: TJSONSchema
75
79
  returnType?: TJSONSchema
80
+ input?: Record<string, TJSONSchema>
76
81
  }
77
82
  validation?: {
78
83
  params?: (params: any) => { errors?: any[] }
79
84
  yield?: (value: any) => { errors?: any[] }
85
+ input?: Record<string, (value: any) => { errors?: any[] }>
80
86
  }
81
87
  } & TExtendedConfig
82
88
  >
@@ -90,18 +96,26 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
90
96
  | TStreamProcedureRegistration<TContext, TExtendedConfig>
91
97
  > = new Map()
92
98
 
93
- function Create<TName extends string, TParams, TReturnType>(
99
+ function Create<
100
+ TName extends string,
101
+ TParams,
102
+ TReturnType,
103
+ TInput extends Record<string, unknown> | undefined = undefined,
104
+ >(
94
105
  name: TName,
95
106
  config: {
96
107
  description?: string
97
108
  schema?: {
98
109
  params?: TParams
99
110
  returnType?: TReturnType
111
+ input?: TInput
100
112
  }
101
113
  } & TExtendedConfig,
102
114
  handler: (
103
115
  ctx: Prettify<TContext & TLocalContext & { isPrevalidated?: boolean }>,
104
- params: TSchemaLib<TParams>
116
+ params: TInput extends Record<string, unknown>
117
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
118
+ : TSchemaLib<TParams>
105
119
  ) => Promise<TSchemaLib<TReturnType>>
106
120
  ) {
107
121
  // Capture definition location as first action
@@ -128,6 +142,7 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
128
142
  schema: jsonSchema,
129
143
  validation: {
130
144
  params: validations.params,
145
+ ...(validations.input && { input: validations.input }),
131
146
  },
132
147
  },
133
148
 
@@ -148,16 +163,37 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
148
163
  }
149
164
  }
150
165
 
166
+ // Validate each input channel independently for better error messages.
167
+ // Double validation (per-channel + merged) is intentional for developer experience;
168
+ // the cost is negligible — revisit if validation becomes a performance bottleneck.
169
+ if (validations?.input && !isPrevalidated) {
170
+ for (const [channel, validator] of Object.entries(validations.input)) {
171
+ const channelValue = (params as Record<string, unknown>)?.[channel]
172
+ const { errors } = validator(channelValue)
173
+
174
+ if (errors) {
175
+ throw new ProcedureValidationError(
176
+ name,
177
+ `Validation error for ${name} in input.${channel}`,
178
+ errors,
179
+ definitionInfo
180
+ )
181
+ }
182
+ }
183
+ }
184
+
151
185
  const localCtx: TLocalContext = {
152
186
  error: errorFactory, // Reuse pre-created factory
153
187
  }
154
188
 
189
+ // params is correctly typed at the public API boundary via conditional type;
190
+ // cast here because TS cannot narrow conditional generics inside implementations
155
191
  return await handler(
156
192
  {
157
193
  ...ctx,
158
194
  ...localCtx,
159
195
  } as Prettify<TContext & TLocalContext & { isPrevalidated?: boolean }>,
160
- params
196
+ params as any
161
197
  )
162
198
  } catch (error: any) {
163
199
  if (error instanceof ProcedureError) {
@@ -204,12 +240,16 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
204
240
  } as {
205
241
  [K in TName]: (
206
242
  ctx: Prettify<TContext>,
207
- params: TSchemaLib<TParams>
243
+ params: TInput extends Record<string, unknown>
244
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
245
+ : TSchemaLib<TParams>
208
246
  ) => Promise<TSchemaLib<TReturnType>>
209
247
  } & {
210
248
  procedure: (
211
249
  ctx: Prettify<TContext>,
212
- params: TSchemaLib<TParams>
250
+ params: TInput extends Record<string, unknown>
251
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
252
+ : TSchemaLib<TParams>
213
253
  ) => Promise<TSchemaLib<TReturnType>>
214
254
  info: {
215
255
  name: TName
@@ -217,15 +257,23 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
217
257
  schema: {
218
258
  params?: TParams
219
259
  returnType?: TReturnType
260
+ input?: TInput
220
261
  }
221
262
  validation?: {
222
263
  params?: (params: any) => { errors?: any[] }
264
+ input?: Record<string, (value: any) => { errors?: any[] }>
223
265
  }
224
266
  } & TExtendedConfig
225
267
  }
226
268
  }
227
269
 
228
- function CreateStream<TName extends string, TParams, TYieldType, TReturnType = void>(
270
+ function CreateStream<
271
+ TName extends string,
272
+ TParams,
273
+ TYieldType,
274
+ TReturnType = void,
275
+ TInput extends Record<string, unknown> | undefined = undefined,
276
+ >(
229
277
  name: TName,
230
278
  config: {
231
279
  description?: string
@@ -233,12 +281,15 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
233
281
  params?: TParams
234
282
  yieldType?: TYieldType
235
283
  returnType?: TReturnType
284
+ input?: TInput
236
285
  }
237
286
  validateYields?: boolean
238
287
  } & TExtendedConfig,
239
288
  handler: (
240
289
  ctx: Prettify<TContext & TStreamContext & { isPrevalidated?: boolean }>,
241
- params: TSchemaLib<TParams>
290
+ params: TInput extends Record<string, unknown>
291
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
292
+ : TSchemaLib<TParams>
242
293
  ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
243
294
  ) {
244
295
  // Capture definition location as first action
@@ -268,6 +319,7 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
268
319
  validation: {
269
320
  params: validations.params,
270
321
  yield: validations.yield,
322
+ ...(validations.input && { input: validations.input }),
271
323
  },
272
324
  },
273
325
 
@@ -293,6 +345,23 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
293
345
  }
294
346
  }
295
347
 
348
+ // Validate each input channel independently (see Create for rationale)
349
+ if (validations?.input && !isPrevalidated) {
350
+ for (const [channel, validator] of Object.entries(validations.input)) {
351
+ const channelValue = (params as Record<string, unknown>)?.[channel]
352
+ const { errors } = validator(channelValue)
353
+
354
+ if (errors) {
355
+ throw new ProcedureValidationError(
356
+ name,
357
+ `Validation error for ${name} in input.${channel}`,
358
+ errors,
359
+ definitionInfo
360
+ )
361
+ }
362
+ }
363
+ }
364
+
296
365
  // Combine with external signal (e.g., from HTTP request) if provided
297
366
  const incomingSignal = (ctx as { signal?: AbortSignal }).signal
298
367
  const signal = incomingSignal
@@ -304,12 +373,14 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
304
373
  signal,
305
374
  }
306
375
 
376
+ // params is correctly typed at the public API boundary via conditional type;
377
+ // cast here because TS cannot narrow conditional generics inside implementations
307
378
  const userGenerator = handler(
308
379
  {
309
380
  ...ctx,
310
381
  ...streamCtx,
311
382
  } as Prettify<TContext & TStreamContext & { isPrevalidated?: boolean }>,
312
- params
383
+ params as any
313
384
  )
314
385
 
315
386
  try {
@@ -376,12 +447,16 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
376
447
  } as {
377
448
  [K in TName]: (
378
449
  ctx: Prettify<TContext>,
379
- params: TSchemaLib<TParams>
450
+ params: TInput extends Record<string, unknown>
451
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
452
+ : TSchemaLib<TParams>
380
453
  ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
381
454
  } & {
382
455
  procedure: (
383
456
  ctx: Prettify<TContext>,
384
- params: TSchemaLib<TParams>
457
+ params: TInput extends Record<string, unknown>
458
+ ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
459
+ : TSchemaLib<TParams>
385
460
  ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
386
461
  info: {
387
462
  name: TName
@@ -391,10 +466,12 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
391
466
  params?: TParams
392
467
  yieldType?: TYieldType
393
468
  returnType?: TReturnType
469
+ input?: TInput
394
470
  }
395
471
  validation?: {
396
472
  params?: (params: any) => { errors?: any[] }
397
473
  yield?: (value: any) => { errors?: any[] }
474
+ input?: Record<string, (value: any) => { errors?: any[] }>
398
475
  }
399
476
  } & TExtendedConfig
400
477
  }
@@ -17,6 +17,7 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType, TYieldTy
17
17
  params?: TParamsSchemaType
18
18
  returnType?: TReturnTypeSchemaType
19
19
  yieldType?: TYieldTypeSchemaType
20
+ input?: Record<string, unknown>
20
21
  },
21
22
  // Used for error stack trace details
22
23
  definitionInfo?: DefinitionInfo
@@ -25,26 +26,44 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType, TYieldTy
25
26
  params?: TJSONSchema
26
27
  returnType?: TJSONSchema
27
28
  yieldType?: TJSONSchema
29
+ input?: Record<string, TJSONSchema>
28
30
  }
29
31
  validations: {
30
32
  params?: (params?: any) => { errors?: TSchemaValidationError[] }
31
33
  yield?: (value?: any) => { errors?: TSchemaValidationError[] }
34
+ input?: Record<string, (value?: any) => { errors?: TSchemaValidationError[] }>
32
35
  }
33
36
  } {
34
- const jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema; yieldType?: TJSONSchema } = {
37
+ const jsonSchema: {
38
+ params?: TJSONSchema
39
+ returnType?: TJSONSchema
40
+ yieldType?: TJSONSchema
41
+ input?: Record<string, TJSONSchema>
42
+ } = {
35
43
  params: undefined,
36
44
  returnType: undefined,
37
45
  yieldType: undefined,
46
+ input: undefined,
38
47
  }
39
48
 
40
49
  const validations: {
41
50
  params?: (params?: any) => { errors?: TSchemaValidationError[] }
42
51
  yield?: (value?: any) => { errors?: TSchemaValidationError[] }
52
+ input?: Record<string, (value?: any) => { errors?: TSchemaValidationError[] }>
43
53
  } = {}
44
54
 
55
+ // Mutual exclusivity: params and input cannot both be defined
56
+ if (schema?.params && schema?.input) {
57
+ throw new ProcedureRegistrationError(
58
+ name,
59
+ `schema.params and schema.input are mutually exclusive for procedure "${name}". Use schema.params for flat input or schema.input for structured multi-channel input.`,
60
+ definitionInfo
61
+ )
62
+ }
63
+
45
64
  if (schema) {
46
65
  const {
47
- jsonSchema: { params, returnType, yieldType },
66
+ jsonSchema: { params, returnType, yieldType, input },
48
67
  validation,
49
68
  } = schemaParser(schema, (errors) => {
50
69
  throw new ProcedureRegistrationError(
@@ -59,8 +78,10 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType, TYieldTy
59
78
  jsonSchema.params = params
60
79
  jsonSchema.returnType = returnType
61
80
  jsonSchema.yieldType = yieldType
81
+ jsonSchema.input = input
62
82
  validations.params = validation.params
63
83
  validations.yield = validation.yield
84
+ validations.input = validation.input
64
85
  }
65
86
 
66
87
  return { jsonSchema, validations }
@@ -4,10 +4,16 @@ import { extractJsonSchema } from './extract-json-schema.js'
4
4
  import { TJSONSchema } from './types.js'
5
5
 
6
6
  export type TSchemaParsed = {
7
- jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema; yieldType?: TJSONSchema }
7
+ jsonSchema: {
8
+ params?: TJSONSchema
9
+ returnType?: TJSONSchema
10
+ yieldType?: TJSONSchema
11
+ input?: Record<string, TJSONSchema>
12
+ }
8
13
  validation: {
9
14
  params?: (params: any) => { errors?: TSchemaValidationError[] }
10
15
  yield?: (value: any) => { errors?: TSchemaValidationError[] }
16
+ input?: Record<string, (value: any) => { errors?: TSchemaValidationError[] }>
11
17
  }
12
18
  }
13
19
 
@@ -24,8 +30,8 @@ const ajv = addFormats(
24
30
  )
25
31
 
26
32
  export function schemaParser(
27
- schema: { params?: unknown; returnType?: unknown; yieldType?: unknown },
28
- onParseError: (errors: { params?: string; returnType?: string; yieldType?: string }) => void
33
+ schema: { params?: unknown; returnType?: unknown; yieldType?: unknown; input?: Record<string, unknown> },
34
+ onParseError: (errors: Record<string, string>) => void
29
35
  ): TSchemaParsed {
30
36
  const jsonSchema: TSchemaParsed['jsonSchema'] = {}
31
37
  const validation: TSchemaParsed['validation'] = {}
@@ -144,5 +150,66 @@ export function schemaParser(
144
150
  }
145
151
  }
146
152
 
153
+ if (schema.input) {
154
+ jsonSchema.input = {}
155
+ validation.input = {}
156
+
157
+ for (const [channelName, channelSchema] of Object.entries(schema.input)) {
158
+ if (!channelSchema) continue
159
+
160
+ let channelJsonSchema: TJSONSchema | undefined
161
+
162
+ try {
163
+ const extracted = extractJsonSchema(channelSchema as TJSONSchema)
164
+ if (extracted) {
165
+ channelJsonSchema = extracted
166
+ jsonSchema.input[channelName] = extracted
167
+ }
168
+ } catch (e: any) {
169
+ onParseError({
170
+ [`input.${channelName}`]: `Error extracting json schema schema.input.${channelName} - ${e.message}`,
171
+ })
172
+ }
173
+
174
+ if (!channelJsonSchema) {
175
+ onParseError({
176
+ [`input.${channelName}`]: `Error extracting json schema schema.input.${channelName} - might be empty or not a valid suretype or typebox type`,
177
+ })
178
+ } else {
179
+ let channelValidator: AJV.ValidateFunction | undefined
180
+
181
+ try {
182
+ channelValidator = ajv.compile(channelJsonSchema as TJSONSchema)
183
+ } catch (e: any) {
184
+ onParseError({
185
+ [`input.${channelName}`]: `Error compiling schema.input.${channelName} for validator - ${e.message}`,
186
+ })
187
+ }
188
+
189
+ validation.input[channelName] = (value: any) => {
190
+ if (!channelValidator) {
191
+ return {
192
+ errors: [
193
+ { message: 'Validator not initialized', keyword: 'internal' } as TSchemaValidationError,
194
+ ],
195
+ }
196
+ }
197
+
198
+ const valid = channelValidator(value)
199
+
200
+ if (!valid) {
201
+ const errors = channelValidator.errors
202
+
203
+ return {
204
+ errors: errors?.length ? errors : undefined,
205
+ }
206
+ }
207
+
208
+ return {}
209
+ }
210
+ }
211
+ }
212
+ }
213
+
147
214
  return { jsonSchema, validation }
148
215
  }