ts-procedures 5.4.0 → 5.4.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.
Files changed (34) hide show
  1. package/package.json +1 -2
  2. package/src/errors.test.ts +0 -163
  3. package/src/errors.ts +0 -107
  4. package/src/exports.ts +0 -7
  5. package/src/implementations/http/README.md +0 -260
  6. package/src/implementations/http/express-rpc/README.md +0 -281
  7. package/src/implementations/http/express-rpc/index.test.ts +0 -957
  8. package/src/implementations/http/express-rpc/index.ts +0 -265
  9. package/src/implementations/http/express-rpc/types.ts +0 -16
  10. package/src/implementations/http/hono-api/index.test.ts +0 -1328
  11. package/src/implementations/http/hono-api/index.ts +0 -461
  12. package/src/implementations/http/hono-api/types.ts +0 -16
  13. package/src/implementations/http/hono-rpc/README.md +0 -358
  14. package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
  15. package/src/implementations/http/hono-rpc/index.ts +0 -237
  16. package/src/implementations/http/hono-rpc/types.ts +0 -16
  17. package/src/implementations/http/hono-stream/README.md +0 -526
  18. package/src/implementations/http/hono-stream/index.test.ts +0 -1676
  19. package/src/implementations/http/hono-stream/index.ts +0 -435
  20. package/src/implementations/http/hono-stream/types.ts +0 -29
  21. package/src/implementations/types.ts +0 -127
  22. package/src/index.test.ts +0 -1194
  23. package/src/index.ts +0 -512
  24. package/src/schema/compute-schema.test.ts +0 -128
  25. package/src/schema/compute-schema.ts +0 -88
  26. package/src/schema/extract-json-schema.test.ts +0 -25
  27. package/src/schema/extract-json-schema.ts +0 -15
  28. package/src/schema/parser.test.ts +0 -182
  29. package/src/schema/parser.ts +0 -215
  30. package/src/schema/resolve-schema-lib.test.ts +0 -19
  31. package/src/schema/resolve-schema-lib.ts +0 -29
  32. package/src/schema/types.ts +0 -20
  33. package/src/stack-utils.test.ts +0 -94
  34. package/src/stack-utils.ts +0 -129
package/src/index.ts DELETED
@@ -1,512 +0,0 @@
1
- import {
2
- ProcedureError,
3
- ProcedureValidationError,
4
- ProcedureYieldValidationError,
5
- } from './errors.js'
6
- import { computeSchema } from './schema/compute-schema.js'
7
- import { Prettify, TJSONSchema, TSchemaLib } from './schema/types.js'
8
- import { captureDefinitionInfo } from './stack-utils.js'
9
-
10
- export type TNoContextProvided = unknown
11
-
12
- export type TLocalContext = {
13
- error: (message: string, meta?: object) => ProcedureError
14
- signal?: AbortSignal
15
- }
16
-
17
- export type TStreamContext = TLocalContext & {
18
- signal: AbortSignal
19
- }
20
-
21
- export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
22
- name: string
23
- isStream?: false
24
- config: {
25
- description?: string
26
- schema?: {
27
- params?: TJSONSchema
28
- returnType?: TJSONSchema
29
- input?: Record<string, TJSONSchema>
30
- }
31
- validation?: {
32
- params?: (params: any) => { errors?: any[] }
33
- input?: Record<string, (value: any) => { errors?: any[] }>
34
- }
35
- } & TExtendedConfig
36
-
37
- handler: (ctx: TContext, params?: any) => Promise<any>
38
- }
39
-
40
- export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
41
- name: string
42
- isStream: true
43
- config: {
44
- description?: string
45
- schema?: {
46
- params?: TJSONSchema
47
- yieldType?: TJSONSchema
48
- returnType?: TJSONSchema
49
- input?: Record<string, TJSONSchema>
50
- }
51
- validation?: {
52
- params?: (params: any) => { errors?: any[] }
53
- yield?: (value: any) => { errors?: any[] }
54
- input?: Record<string, (value: any) => { errors?: any[] }>
55
- }
56
- } & TExtendedConfig
57
-
58
- handler: (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>
59
- }
60
-
61
- export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unknown>(
62
- /**
63
- * Optionally provided builder to register Procedures
64
- */
65
- builder?: {
66
- onCreate?: (
67
- procedure: Prettify<{
68
- name: string
69
- isStream?: boolean
70
- handler:
71
- | ((ctx: Prettify<TContext>, params?: any) => Promise<any>)
72
- | ((ctx: Prettify<TContext>, params?: any) => AsyncGenerator<any, any, unknown>)
73
- config: Prettify<
74
- {
75
- description?: string
76
- schema?: {
77
- params?: TJSONSchema
78
- yieldType?: TJSONSchema
79
- returnType?: TJSONSchema
80
- input?: Record<string, TJSONSchema>
81
- }
82
- validation?: {
83
- params?: (params: any) => { errors?: any[] }
84
- yield?: (value: any) => { errors?: any[] }
85
- input?: Record<string, (value: any) => { errors?: any[] }>
86
- }
87
- } & TExtendedConfig
88
- >
89
- }>
90
- ) => void
91
- }
92
- ) {
93
- const procedures: Map<
94
- string,
95
- | TProcedureRegistration<TContext, TExtendedConfig>
96
- | TStreamProcedureRegistration<TContext, TExtendedConfig>
97
- > = new Map()
98
-
99
- function Create<
100
- TName extends string,
101
- TParams,
102
- TReturnType,
103
- TInput extends Record<string, unknown> | undefined = undefined,
104
- >(
105
- name: TName,
106
- config: {
107
- description?: string
108
- schema?: {
109
- params?: TParams
110
- returnType?: TReturnType
111
- input?: TInput
112
- }
113
- } & TExtendedConfig,
114
- handler: (
115
- ctx: Prettify<TContext & TLocalContext & { isPrevalidated?: boolean }>,
116
- params: TInput extends Record<string, unknown>
117
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
118
- : TSchemaLib<TParams>
119
- ) => Promise<TSchemaLib<TReturnType>>
120
- ) {
121
- // Capture definition location as first action
122
- const definitionInfo = captureDefinitionInfo()
123
-
124
- // BEFORE computeSchema - fail fast on duplicate
125
- if (procedures.has(name)) {
126
- throw new Error(`Procedure with name ${name} is already registered`)
127
- }
128
-
129
- const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
130
-
131
- // Create error factory once at registration time (outside handler)
132
- const errorFactory = (message: string, meta?: object) => {
133
- return new ProcedureError(name, message, meta, definitionInfo)
134
- }
135
-
136
- const registeredProcedure = {
137
- name,
138
- config: {
139
- // ctx: config.hook, ??? why was this here
140
- ...config,
141
- description: config.description,
142
- schema: jsonSchema,
143
- validation: {
144
- params: validations.params,
145
- ...(validations.input && { input: validations.input }),
146
- },
147
- },
148
-
149
- handler: async (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => {
150
- try {
151
- // Skip validation if caller has already validated (e.g., HonoStreamAppBuilder)
152
- const isPrevalidated = (ctx as { isPrevalidated?: boolean }).isPrevalidated
153
- if (validations?.params && !isPrevalidated) {
154
- const { errors } = validations.params(params)
155
-
156
- if (errors) {
157
- throw new ProcedureValidationError(
158
- name,
159
- `Validation error for ${name}`,
160
- errors,
161
- definitionInfo
162
- )
163
- }
164
- }
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
-
185
- const localCtx: TLocalContext = {
186
- error: errorFactory, // Reuse pre-created factory
187
- }
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
191
- return await handler(
192
- {
193
- ...ctx,
194
- ...localCtx,
195
- } as Prettify<TContext & TLocalContext & { isPrevalidated?: boolean }>,
196
- params as any
197
- )
198
- } catch (error: any) {
199
- if (error instanceof ProcedureError) {
200
- throw error
201
- } else {
202
- const err = new ProcedureError(
203
- name,
204
- `Error in handler for ${name} - ${error?.message}`,
205
- undefined,
206
- definitionInfo
207
- )
208
- err.cause = error // Preserve original error
209
- // Preserve original stack but append definition info
210
- if (error.stack && definitionInfo.definedAt) {
211
- const { file, line, column } = definitionInfo.definedAt
212
- err.stack =
213
- error.stack +
214
- `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
215
- } else if (error.stack) {
216
- err.stack = error.stack
217
- }
218
- throw err
219
- }
220
- }
221
- },
222
- }
223
-
224
- procedures.set(name, registeredProcedure)
225
-
226
- if (builder?.onCreate) {
227
- builder.onCreate(registeredProcedure)
228
- }
229
-
230
- const info = {
231
- name,
232
- ...registeredProcedure.config,
233
- }
234
-
235
- // return so can be called directly (ie: int/unit tests)
236
- return {
237
- [name]: registeredProcedure.handler,
238
- procedure: registeredProcedure.handler,
239
- info,
240
- } as {
241
- [K in TName]: (
242
- ctx: Prettify<TContext>,
243
- params: TInput extends Record<string, unknown>
244
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
245
- : TSchemaLib<TParams>
246
- ) => Promise<TSchemaLib<TReturnType>>
247
- } & {
248
- procedure: (
249
- ctx: Prettify<TContext>,
250
- params: TInput extends Record<string, unknown>
251
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
252
- : TSchemaLib<TParams>
253
- ) => Promise<TSchemaLib<TReturnType>>
254
- info: {
255
- name: TName
256
- description?: string
257
- schema: {
258
- params?: TParams
259
- returnType?: TReturnType
260
- input?: TInput
261
- }
262
- validation?: {
263
- params?: (params: any) => { errors?: any[] }
264
- input?: Record<string, (value: any) => { errors?: any[] }>
265
- }
266
- } & TExtendedConfig
267
- }
268
- }
269
-
270
- function CreateStream<
271
- TName extends string,
272
- TParams,
273
- TYieldType,
274
- TReturnType = void,
275
- TInput extends Record<string, unknown> | undefined = undefined,
276
- >(
277
- name: TName,
278
- config: {
279
- description?: string
280
- schema?: {
281
- params?: TParams
282
- yieldType?: TYieldType
283
- returnType?: TReturnType
284
- input?: TInput
285
- }
286
- validateYields?: boolean
287
- } & TExtendedConfig,
288
- handler: (
289
- ctx: Prettify<TContext & TStreamContext & { isPrevalidated?: boolean }>,
290
- params: TInput extends Record<string, unknown>
291
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
292
- : TSchemaLib<TParams>
293
- ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
294
- ) {
295
- // Capture definition location as first action
296
- const definitionInfo = captureDefinitionInfo()
297
-
298
- // BEFORE computeSchema - fail fast on duplicate
299
- if (procedures.has(name)) {
300
- throw new Error(`Procedure with name ${name} is already registered`)
301
- }
302
-
303
- const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
304
-
305
- // Create error factory once at registration time (outside handler)
306
- const errorFactory = (message: string, meta?: object) => {
307
- return new ProcedureError(name, message, meta, definitionInfo)
308
- }
309
-
310
- const validateYields = config.validateYields ?? false
311
-
312
- const registeredProcedure: TStreamProcedureRegistration<TContext, TExtendedConfig> = {
313
- name,
314
- isStream: true,
315
- config: {
316
- ...config,
317
- description: config.description,
318
- schema: jsonSchema,
319
- validation: {
320
- params: validations.params,
321
- yield: validations.yield,
322
- ...(validations.input && { input: validations.input }),
323
- },
324
- },
325
-
326
- handler: async function* wrappedHandler(
327
- ctx: Prettify<TContext>,
328
- params: TSchemaLib<TParams>
329
- ) {
330
- // Create abort controller for this stream
331
- const abortController = new AbortController()
332
-
333
- // Skip validation if caller has already validated (e.g., HonoStreamAppBuilder)
334
- const isPrevalidated = (ctx as { isPrevalidated?: boolean }).isPrevalidated
335
- if (validations?.params && !isPrevalidated) {
336
- const { errors } = validations.params(params)
337
-
338
- if (errors) {
339
- throw new ProcedureValidationError(
340
- name,
341
- `Validation error for ${name}`,
342
- errors,
343
- definitionInfo
344
- )
345
- }
346
- }
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
-
365
- // Combine with external signal (e.g., from HTTP request) if provided
366
- const incomingSignal = (ctx as { signal?: AbortSignal }).signal
367
- const signal = incomingSignal
368
- ? AbortSignal.any([incomingSignal, abortController.signal])
369
- : abortController.signal
370
-
371
- const streamCtx: TStreamContext = {
372
- error: errorFactory,
373
- signal,
374
- }
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
378
- const userGenerator = handler(
379
- {
380
- ...ctx,
381
- ...streamCtx,
382
- } as Prettify<TContext & TStreamContext & { isPrevalidated?: boolean }>,
383
- params as any
384
- )
385
-
386
- try {
387
- for await (const value of userGenerator) {
388
- // Only validate if explicitly enabled via validateYields: true
389
- if (validateYields && validations.yield) {
390
- const { errors } = validations.yield(value)
391
- if (errors) {
392
- throw new ProcedureYieldValidationError(
393
- name,
394
- `Yield validation error for ${name}`,
395
- errors,
396
- definitionInfo
397
- )
398
- }
399
- }
400
-
401
- yield value
402
- }
403
- } catch (error: any) {
404
- if (error instanceof ProcedureError) {
405
- throw error
406
- } else {
407
- const err = new ProcedureError(
408
- name,
409
- `Error in streaming handler for ${name} - ${error?.message}`,
410
- undefined,
411
- definitionInfo
412
- )
413
- err.cause = error
414
- if (error.stack && definitionInfo.definedAt) {
415
- const { file, line, column } = definitionInfo.definedAt
416
- err.stack =
417
- error.stack +
418
- `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
419
- } else if (error.stack) {
420
- err.stack = error.stack
421
- }
422
- throw err
423
- }
424
- } finally {
425
- abortController.abort('stream-completed')
426
- }
427
- } as (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>,
428
- }
429
-
430
- procedures.set(name, registeredProcedure)
431
-
432
- if (builder?.onCreate) {
433
- builder.onCreate(registeredProcedure)
434
- }
435
-
436
- const info = {
437
- name,
438
- isStream: true as const,
439
- ...registeredProcedure.config,
440
- }
441
-
442
- // return so can be called directly (ie: int/unit tests)
443
- return {
444
- [name]: registeredProcedure.handler,
445
- procedure: registeredProcedure.handler,
446
- info,
447
- } as {
448
- [K in TName]: (
449
- ctx: Prettify<TContext>,
450
- params: TInput extends Record<string, unknown>
451
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
452
- : TSchemaLib<TParams>
453
- ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
454
- } & {
455
- procedure: (
456
- ctx: Prettify<TContext>,
457
- params: TInput extends Record<string, unknown>
458
- ? Prettify<{ [K in keyof TInput]: TSchemaLib<TInput[K]> }>
459
- : TSchemaLib<TParams>
460
- ) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
461
- info: {
462
- name: TName
463
- isStream: true
464
- description?: string
465
- schema: {
466
- params?: TParams
467
- yieldType?: TYieldType
468
- returnType?: TReturnType
469
- input?: TInput
470
- }
471
- validation?: {
472
- params?: (params: any) => { errors?: any[] }
473
- yield?: (value: any) => { errors?: any[] }
474
- input?: Record<string, (value: any) => { errors?: any[] }>
475
- }
476
- } & TExtendedConfig
477
- }
478
- }
479
-
480
- return {
481
- /**
482
- * Get all registered procedures
483
- */
484
- getProcedures: () => {
485
- return Array.from(procedures.values())
486
- },
487
-
488
- /**
489
- * Get a specific procedure by name
490
- */
491
- getProcedure: (name: string) => {
492
- return procedures.get(name)
493
- },
494
-
495
- /**
496
- * Remove a procedure by name
497
- */
498
- removeProcedure: (name: string) => {
499
- return procedures.delete(name)
500
- },
501
-
502
- /**
503
- * Clear all registered procedures
504
- */
505
- clear: () => {
506
- procedures.clear()
507
- },
508
-
509
- Create,
510
- CreateStream,
511
- }
512
- }
@@ -1,128 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { Type } from 'typebox'
3
- import { v } from 'suretype'
4
- import { computeSchema } from './compute-schema.js'
5
- import { ProcedureRegistrationError } from '../errors.js'
6
-
7
- describe('computeSchema', () => {
8
- it('should return empty schema and validations when no schema provided', () => {
9
- const result = computeSchema('test-procedure')
10
-
11
- expect(result).toEqual({
12
- jsonSchema: { params: undefined, returnType: undefined },
13
- validations: {}
14
- })
15
- })
16
-
17
- describe('with Typebox schema', () => {
18
- it('should correctly process params schema', () => {
19
- const schema = {
20
- params: Type.Object({
21
- name: Type.String(),
22
- age: Type.Number()
23
- })
24
- }
25
-
26
- const result = computeSchema('test-procedure', schema)
27
-
28
- expect(result.jsonSchema.params).toBeDefined()
29
- expect(result.validations.params).toBeDefined()
30
-
31
- // Test validation function
32
- const validInput = { name: 'John', age: 30 }
33
- expect(result.validations.params?.(validInput).errors).toBeUndefined()
34
-
35
- // Test invalid input
36
- const invalidInput = { name: 123, age: 'invalid' }
37
- expect(result.validations.params?.(invalidInput).errors).toBeDefined()
38
- })
39
-
40
- it('should correctly process returnType schema', () => {
41
- const schema = {
42
- returnType: Type.Object({
43
- result: Type.Boolean()
44
- })
45
- }
46
-
47
- const result = computeSchema('test-procedure', schema)
48
-
49
- expect(result.jsonSchema.returnType).toBeDefined()
50
- expect(result.validations.params).toBeUndefined()
51
- })
52
- })
53
-
54
- describe('with Suretype schema', () => {
55
- it('should correctly process params schema', () => {
56
- const schema = {
57
- params: v.object({
58
- name: v.string(),
59
- age: v.number()
60
- })
61
- }
62
-
63
- const result = computeSchema('test-procedure', schema)
64
-
65
- expect(result.jsonSchema.params).toBeDefined()
66
- expect(result.validations.params).toBeDefined()
67
-
68
- // Test validation function
69
- const validInput = { name: 'John', age: 30 }
70
- expect(result.validations.params?.(validInput).errors).toBeUndefined()
71
-
72
- // Test invalid input
73
- const invalidInput = { name: 123, age: 'invalid' }
74
- expect(result.validations.params?.(invalidInput).errors).toBeDefined()
75
- })
76
- })
77
-
78
- describe('error handling', () => {
79
- it('should throw ProcedureRegistrationError for invalid schema', () => {
80
- const invalidSchema = {
81
- params: {
82
- type: 'invalid-schema-type'
83
- }
84
- }
85
-
86
- expect(() => computeSchema('test-procedure', invalidSchema))
87
- .toThrow(ProcedureRegistrationError)
88
- })
89
-
90
- it('should include procedure name in error message', () => {
91
- const invalidSchema = {
92
- params: {
93
- type: 'invalid-schema-type'
94
- }
95
- }
96
-
97
- try {
98
- computeSchema('test-procedure', invalidSchema)
99
- } catch (error: any) {
100
- expect(error instanceof ProcedureRegistrationError).toBe(true)
101
- expect(error.message).toContain('test-procedure')
102
- }
103
- })
104
- })
105
-
106
- describe('combined schemas', () => {
107
- it('should handle both params and returnType schemas', () => {
108
- const schema = {
109
- params: Type.Object({
110
- input: Type.String()
111
- }),
112
- returnType: Type.Object({
113
- output: Type.Boolean()
114
- })
115
- }
116
-
117
- const result = computeSchema('test-procedure', schema)
118
-
119
- expect(result.jsonSchema.params).toBeDefined()
120
- expect(result.jsonSchema.returnType).toBeDefined()
121
- expect(result.validations.params).toBeDefined()
122
-
123
- // Test params validation
124
- const validInput = { input: 'test' }
125
- expect(result.validations.params?.(validInput).errors).toBeUndefined()
126
- })
127
- })
128
- })