ts-procedures 3.1.0 → 3.3.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.
- package/README.md +222 -2
- package/build/errors.d.ts +19 -3
- package/build/errors.js +54 -5
- package/build/errors.js.map +1 -1
- package/build/errors.test.js +82 -0
- package/build/errors.test.js.map +1 -1
- package/build/exports.d.ts +1 -0
- package/build/exports.js +1 -0
- package/build/exports.js.map +1 -1
- package/build/implementations/http/hono-stream/index.d.ts +92 -0
- package/build/implementations/http/hono-stream/index.js +229 -0
- package/build/implementations/http/hono-stream/index.js.map +1 -0
- package/build/implementations/http/hono-stream/index.test.d.ts +1 -0
- package/build/implementations/http/hono-stream/index.test.js +681 -0
- package/build/implementations/http/hono-stream/index.test.js.map +1 -0
- package/build/implementations/http/hono-stream/types.d.ts +24 -0
- package/build/implementations/http/hono-stream/types.js +2 -0
- package/build/implementations/http/hono-stream/types.js.map +1 -0
- package/build/implementations/types.d.ts +15 -1
- package/build/index.d.ts +62 -3
- package/build/index.js +111 -6
- package/build/index.js.map +1 -1
- package/build/index.test.js +385 -2
- package/build/index.test.js.map +1 -1
- package/build/schema/compute-schema.d.ts +9 -2
- package/build/schema/compute-schema.js +9 -3
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -0
- package/build/schema/parser.js +42 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/types.d.ts +1 -0
- package/build/stack-utils.d.ts +25 -0
- package/build/stack-utils.js +95 -0
- package/build/stack-utils.js.map +1 -0
- package/build/stack-utils.test.d.ts +1 -0
- package/build/stack-utils.test.js +80 -0
- package/build/stack-utils.test.js.map +1 -0
- package/package.json +1 -1
- package/src/errors.test.ts +110 -0
- package/src/errors.ts +65 -3
- package/src/exports.ts +1 -0
- package/src/implementations/http/README.md +87 -55
- package/src/implementations/http/hono-stream/README.md +261 -0
- package/src/implementations/http/hono-stream/index.test.ts +1009 -0
- package/src/implementations/http/hono-stream/index.ts +327 -0
- package/src/implementations/http/hono-stream/types.ts +29 -0
- package/src/implementations/types.ts +17 -1
- package/src/index.test.ts +525 -41
- package/src/index.ts +210 -8
- package/src/schema/compute-schema.ts +15 -3
- package/src/schema/parser.ts +55 -4
- package/src/schema/types.ts +4 -0
- package/src/stack-utils.test.ts +94 -0
- package/src/stack-utils.ts +129 -0
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ProcedureError, ProcedureValidationError } from './errors.js'
|
|
1
|
+
import { ProcedureError, ProcedureValidationError, ProcedureYieldValidationError } from './errors.js'
|
|
2
2
|
import { computeSchema } from './schema/compute-schema.js'
|
|
3
3
|
import { Prettify, TJSONSchema, TSchemaLib } from './schema/types.js'
|
|
4
|
+
import { captureDefinitionInfo } from './stack-utils.js'
|
|
4
5
|
|
|
5
6
|
export type TNoContextProvided = unknown
|
|
6
7
|
|
|
@@ -8,8 +9,13 @@ export type TLocalContext = {
|
|
|
8
9
|
error: (message: string, meta?: object) => ProcedureError
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export type TStreamContext = TLocalContext & {
|
|
13
|
+
signal: AbortSignal
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
|
|
12
17
|
name: string
|
|
18
|
+
isStream?: false
|
|
13
19
|
config: {
|
|
14
20
|
description?: string
|
|
15
21
|
schema?: {
|
|
@@ -24,6 +30,25 @@ export type TProcedureRegistration<TContext = unknown, TExtendedConfig = unknown
|
|
|
24
30
|
handler: (ctx: TContext, params?: any) => Promise<any>
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
export type TStreamProcedureRegistration<TContext = unknown, TExtendedConfig = unknown> = {
|
|
34
|
+
name: string
|
|
35
|
+
isStream: true
|
|
36
|
+
config: {
|
|
37
|
+
description?: string
|
|
38
|
+
schema?: {
|
|
39
|
+
params?: TJSONSchema
|
|
40
|
+
yieldType?: TJSONSchema
|
|
41
|
+
returnType?: TJSONSchema
|
|
42
|
+
}
|
|
43
|
+
validation?: {
|
|
44
|
+
params?: (params: any) => { errors?: any[] }
|
|
45
|
+
yield?: (value: any) => { errors?: any[] }
|
|
46
|
+
}
|
|
47
|
+
} & TExtendedConfig
|
|
48
|
+
|
|
49
|
+
handler: (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unknown>(
|
|
28
53
|
/**
|
|
29
54
|
* Optionally provided builder to register Procedures
|
|
@@ -32,16 +57,19 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
32
57
|
onCreate?: (
|
|
33
58
|
procedure: Prettify<{
|
|
34
59
|
name: string
|
|
35
|
-
|
|
60
|
+
isStream?: boolean
|
|
61
|
+
handler: ((ctx: Prettify<TContext>, params?: any) => Promise<any>) | ((ctx: Prettify<TContext>, params?: any) => AsyncGenerator<any, any, unknown>)
|
|
36
62
|
config: Prettify<
|
|
37
63
|
{
|
|
38
64
|
description?: string
|
|
39
65
|
schema?: {
|
|
40
66
|
params?: TJSONSchema
|
|
67
|
+
yieldType?: TJSONSchema
|
|
41
68
|
returnType?: TJSONSchema
|
|
42
69
|
}
|
|
43
70
|
validation?: {
|
|
44
71
|
params?: (params: any) => { errors?: any[] }
|
|
72
|
+
yield?: (value: any) => { errors?: any[] }
|
|
45
73
|
}
|
|
46
74
|
} & TExtendedConfig
|
|
47
75
|
>
|
|
@@ -51,7 +79,7 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
51
79
|
) {
|
|
52
80
|
const procedures: Map<
|
|
53
81
|
string,
|
|
54
|
-
TProcedureRegistration<TContext, TExtendedConfig>
|
|
82
|
+
TProcedureRegistration<TContext, TExtendedConfig> | TStreamProcedureRegistration<TContext, TExtendedConfig>
|
|
55
83
|
> = new Map()
|
|
56
84
|
|
|
57
85
|
function Create<TName extends string, TParams, TReturnType>(
|
|
@@ -68,16 +96,19 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
68
96
|
params: TSchemaLib<TParams>
|
|
69
97
|
) => Promise<TSchemaLib<TReturnType>>
|
|
70
98
|
) {
|
|
99
|
+
// Capture definition location as first action
|
|
100
|
+
const definitionInfo = captureDefinitionInfo()
|
|
101
|
+
|
|
71
102
|
// BEFORE computeSchema - fail fast on duplicate
|
|
72
103
|
if (procedures.has(name)) {
|
|
73
104
|
throw new Error(`Procedure with name ${name} is already registered`)
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
const { jsonSchema, validations } = computeSchema(name, config.schema)
|
|
107
|
+
const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
|
|
77
108
|
|
|
78
109
|
// Create error factory once at registration time (outside handler)
|
|
79
110
|
const errorFactory = (message: string, meta?: object) => {
|
|
80
|
-
return new ProcedureError(name, message, meta)
|
|
111
|
+
return new ProcedureError(name, message, meta, definitionInfo)
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
const registeredProcedure = {
|
|
@@ -101,7 +132,8 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
101
132
|
throw new ProcedureValidationError(
|
|
102
133
|
name,
|
|
103
134
|
`Validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`,
|
|
104
|
-
errors
|
|
135
|
+
errors,
|
|
136
|
+
definitionInfo
|
|
105
137
|
)
|
|
106
138
|
}
|
|
107
139
|
}
|
|
@@ -121,9 +153,20 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
121
153
|
if (error instanceof ProcedureError) {
|
|
122
154
|
throw error
|
|
123
155
|
} else {
|
|
124
|
-
const err = new ProcedureError(
|
|
156
|
+
const err = new ProcedureError(
|
|
157
|
+
name,
|
|
158
|
+
`Error in handler for ${name} - ${error?.message}`,
|
|
159
|
+
undefined,
|
|
160
|
+
definitionInfo
|
|
161
|
+
)
|
|
125
162
|
err.cause = error // Preserve original error
|
|
126
|
-
|
|
163
|
+
// Preserve original stack but append definition info
|
|
164
|
+
if (error.stack && definitionInfo.definedAt) {
|
|
165
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
166
|
+
err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
167
|
+
} else if (error.stack) {
|
|
168
|
+
err.stack = error.stack
|
|
169
|
+
}
|
|
127
170
|
throw err
|
|
128
171
|
}
|
|
129
172
|
}
|
|
@@ -164,6 +207,164 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
164
207
|
}
|
|
165
208
|
}
|
|
166
209
|
|
|
210
|
+
function CreateStream<TName extends string, TParams, TYieldType, TReturnType = void>(
|
|
211
|
+
name: TName,
|
|
212
|
+
config: {
|
|
213
|
+
description?: string
|
|
214
|
+
schema?: {
|
|
215
|
+
params?: TParams
|
|
216
|
+
yieldType?: TYieldType
|
|
217
|
+
returnType?: TReturnType
|
|
218
|
+
}
|
|
219
|
+
validateYields?: boolean
|
|
220
|
+
} & TExtendedConfig,
|
|
221
|
+
handler: (
|
|
222
|
+
ctx: Prettify<TContext & TStreamContext>,
|
|
223
|
+
params: TSchemaLib<TParams>
|
|
224
|
+
) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
225
|
+
) {
|
|
226
|
+
// Capture definition location as first action
|
|
227
|
+
const definitionInfo = captureDefinitionInfo()
|
|
228
|
+
|
|
229
|
+
// BEFORE computeSchema - fail fast on duplicate
|
|
230
|
+
if (procedures.has(name)) {
|
|
231
|
+
throw new Error(`Procedure with name ${name} is already registered`)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { jsonSchema, validations } = computeSchema(name, config.schema, definitionInfo)
|
|
235
|
+
|
|
236
|
+
// Create error factory once at registration time (outside handler)
|
|
237
|
+
const errorFactory = (message: string, meta?: object) => {
|
|
238
|
+
return new ProcedureError(name, message, meta, definitionInfo)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const validateYields = config.validateYields ?? false
|
|
242
|
+
|
|
243
|
+
const registeredProcedure: TStreamProcedureRegistration<TContext, TExtendedConfig> = {
|
|
244
|
+
name,
|
|
245
|
+
isStream: true,
|
|
246
|
+
config: {
|
|
247
|
+
...config,
|
|
248
|
+
description: config.description,
|
|
249
|
+
schema: jsonSchema,
|
|
250
|
+
validation: {
|
|
251
|
+
params: validations.params,
|
|
252
|
+
yield: validations.yield,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
handler: async function* wrappedHandler(ctx: Prettify<TContext>, params: TSchemaLib<TParams>) {
|
|
257
|
+
// Create abort controller for this stream
|
|
258
|
+
const abortController = new AbortController()
|
|
259
|
+
|
|
260
|
+
// Validate params first
|
|
261
|
+
if (validations?.params) {
|
|
262
|
+
const { errors } = validations.params(params)
|
|
263
|
+
|
|
264
|
+
if (errors) {
|
|
265
|
+
throw new ProcedureValidationError(
|
|
266
|
+
name,
|
|
267
|
+
`Validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`,
|
|
268
|
+
errors,
|
|
269
|
+
definitionInfo
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const streamCtx: TStreamContext = {
|
|
275
|
+
error: errorFactory,
|
|
276
|
+
signal: abortController.signal,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const userGenerator = handler(
|
|
280
|
+
{
|
|
281
|
+
...ctx,
|
|
282
|
+
...streamCtx,
|
|
283
|
+
} as Prettify<TContext & TStreamContext>,
|
|
284
|
+
params
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
for await (const value of userGenerator) {
|
|
289
|
+
// Only validate if explicitly enabled via validateYields: true
|
|
290
|
+
if (validateYields && validations.yield) {
|
|
291
|
+
const { errors } = validations.yield(value)
|
|
292
|
+
if (errors) {
|
|
293
|
+
throw new ProcedureYieldValidationError(
|
|
294
|
+
name,
|
|
295
|
+
`Yield validation error for ${name} - ${errors.map((e) => e.message).join(', ')}`,
|
|
296
|
+
errors,
|
|
297
|
+
definitionInfo
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
yield value
|
|
303
|
+
}
|
|
304
|
+
} catch (error: any) {
|
|
305
|
+
if (error instanceof ProcedureError) {
|
|
306
|
+
throw error
|
|
307
|
+
} else {
|
|
308
|
+
const err = new ProcedureError(
|
|
309
|
+
name,
|
|
310
|
+
`Error in streaming handler for ${name} - ${error?.message}`,
|
|
311
|
+
undefined,
|
|
312
|
+
definitionInfo
|
|
313
|
+
)
|
|
314
|
+
err.cause = error
|
|
315
|
+
if (error.stack && definitionInfo.definedAt) {
|
|
316
|
+
const { file, line, column } = definitionInfo.definedAt
|
|
317
|
+
err.stack = error.stack + `\n--- Procedure "${name}" defined at ---\n at ${file}:${line}:${column}`
|
|
318
|
+
} else if (error.stack) {
|
|
319
|
+
err.stack = error.stack
|
|
320
|
+
}
|
|
321
|
+
throw err
|
|
322
|
+
}
|
|
323
|
+
} finally {
|
|
324
|
+
// Signal abort on early termination
|
|
325
|
+
abortController.abort()
|
|
326
|
+
}
|
|
327
|
+
} as (ctx: TContext, params?: any) => AsyncGenerator<any, any, unknown>,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
procedures.set(name, registeredProcedure)
|
|
331
|
+
|
|
332
|
+
if (builder?.onCreate) {
|
|
333
|
+
builder.onCreate(registeredProcedure)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const info = {
|
|
337
|
+
name,
|
|
338
|
+
isStream: true as const,
|
|
339
|
+
...registeredProcedure.config,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// return so can be called directly (ie: int/unit tests)
|
|
343
|
+
return {
|
|
344
|
+
[name]: registeredProcedure.handler,
|
|
345
|
+
procedure: registeredProcedure.handler,
|
|
346
|
+
info,
|
|
347
|
+
} as {
|
|
348
|
+
[K in TName]: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
349
|
+
} & {
|
|
350
|
+
procedure: (ctx: Prettify<TContext>, params: TSchemaLib<TParams>) => AsyncGenerator<TSchemaLib<TYieldType>, TSchemaLib<TReturnType> | void, unknown>
|
|
351
|
+
info: {
|
|
352
|
+
name: TName
|
|
353
|
+
isStream: true
|
|
354
|
+
description?: string
|
|
355
|
+
schema: {
|
|
356
|
+
params?: TParams
|
|
357
|
+
yieldType?: TYieldType
|
|
358
|
+
returnType?: TReturnType
|
|
359
|
+
}
|
|
360
|
+
validation?: {
|
|
361
|
+
params?: (params: any) => { errors?: any[] }
|
|
362
|
+
yield?: (value: any) => { errors?: any[] }
|
|
363
|
+
}
|
|
364
|
+
} & TExtendedConfig
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
167
368
|
return {
|
|
168
369
|
/**
|
|
169
370
|
* Get all registered procedures
|
|
@@ -194,5 +395,6 @@ export function Procedures<TContext = TNoContextProvided, TExtendedConfig = unkn
|
|
|
194
395
|
},
|
|
195
396
|
|
|
196
397
|
Create,
|
|
398
|
+
CreateStream,
|
|
197
399
|
}
|
|
198
400
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { schemaParser, TSchemaValidationError } from './parser.js'
|
|
2
2
|
import { ProcedureRegistrationError } from '../errors.js'
|
|
3
3
|
import { TJSONSchema } from './types.js'
|
|
4
|
+
import { DefinitionInfo } from '../stack-utils.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* This function is used to compute the JSON schema and validation functions
|
|
@@ -8,34 +9,42 @@ import { TJSONSchema } from './types.js'
|
|
|
8
9
|
*
|
|
9
10
|
* @param name The name of the procedure
|
|
10
11
|
* @param schema Procedure schema
|
|
12
|
+
* @param definitionInfo Optional definition info for error reporting
|
|
11
13
|
*/
|
|
12
|
-
export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
|
|
14
|
+
export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType, TYieldTypeSchemaType = unknown>(
|
|
13
15
|
name: string,
|
|
14
16
|
schema?: {
|
|
15
17
|
params?: TParamsSchemaType
|
|
16
18
|
returnType?: TReturnTypeSchemaType
|
|
19
|
+
yieldType?: TYieldTypeSchemaType
|
|
17
20
|
},
|
|
21
|
+
// Used for error stack trace details
|
|
22
|
+
definitionInfo?: DefinitionInfo
|
|
18
23
|
): {
|
|
19
24
|
jsonSchema: {
|
|
20
25
|
params?: TJSONSchema
|
|
21
26
|
returnType?: TJSONSchema
|
|
27
|
+
yieldType?: TJSONSchema
|
|
22
28
|
}
|
|
23
29
|
validations: {
|
|
24
30
|
params?: (params?: any) => { errors?: TSchemaValidationError[] }
|
|
31
|
+
yield?: (value?: any) => { errors?: TSchemaValidationError[] }
|
|
25
32
|
}
|
|
26
33
|
} {
|
|
27
|
-
const jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema } = {
|
|
34
|
+
const jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema; yieldType?: TJSONSchema } = {
|
|
28
35
|
params: undefined,
|
|
29
36
|
returnType: undefined,
|
|
37
|
+
yieldType: undefined,
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
const validations: {
|
|
33
41
|
params?: (params?: any) => { errors?: TSchemaValidationError[] }
|
|
42
|
+
yield?: (value?: any) => { errors?: TSchemaValidationError[] }
|
|
34
43
|
} = {}
|
|
35
44
|
|
|
36
45
|
if (schema) {
|
|
37
46
|
const {
|
|
38
|
-
jsonSchema: { params, returnType },
|
|
47
|
+
jsonSchema: { params, returnType, yieldType },
|
|
39
48
|
validation,
|
|
40
49
|
} = schemaParser(schema, (errors) => {
|
|
41
50
|
throw new ProcedureRegistrationError(
|
|
@@ -43,12 +52,15 @@ export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
|
|
|
43
52
|
`Error parsing schema for ${name} - ${Object.entries(errors)
|
|
44
53
|
.map(([key, error]) => `${key}: ${error}`)
|
|
45
54
|
.join(', ')}`,
|
|
55
|
+
definitionInfo
|
|
46
56
|
)
|
|
47
57
|
})
|
|
48
58
|
|
|
49
59
|
jsonSchema.params = params
|
|
50
60
|
jsonSchema.returnType = returnType
|
|
61
|
+
jsonSchema.yieldType = yieldType
|
|
51
62
|
validations.params = validation.params
|
|
63
|
+
validations.yield = validation.yield
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
return { jsonSchema, validations }
|
package/src/schema/parser.ts
CHANGED
|
@@ -4,8 +4,11 @@ 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 }
|
|
8
|
-
validation: {
|
|
7
|
+
jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema; yieldType?: TJSONSchema }
|
|
8
|
+
validation: {
|
|
9
|
+
params?: (params: any) => { errors?: TSchemaValidationError[] }
|
|
10
|
+
yield?: (value: any) => { errors?: TSchemaValidationError[] }
|
|
11
|
+
}
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export type TSchemaValidationError = AJV.ErrorObject
|
|
@@ -21,8 +24,8 @@ const ajv = addFormats(
|
|
|
21
24
|
)
|
|
22
25
|
|
|
23
26
|
export function schemaParser(
|
|
24
|
-
schema: { params?: unknown; returnType?: unknown },
|
|
25
|
-
onParseError: (errors: { params?: string; returnType?: string }) => void
|
|
27
|
+
schema: { params?: unknown; returnType?: unknown; yieldType?: unknown },
|
|
28
|
+
onParseError: (errors: { params?: string; returnType?: string; yieldType?: string }) => void
|
|
26
29
|
): TSchemaParsed {
|
|
27
30
|
const jsonSchema: TSchemaParsed['jsonSchema'] = {}
|
|
28
31
|
const validation: TSchemaParsed['validation'] = {}
|
|
@@ -93,5 +96,53 @@ export function schemaParser(
|
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
if (schema.yieldType) {
|
|
100
|
+
try {
|
|
101
|
+
const extracted = extractJsonSchema(schema.yieldType as TJSONSchema)
|
|
102
|
+
|
|
103
|
+
if (extracted) {
|
|
104
|
+
jsonSchema.yieldType = extracted
|
|
105
|
+
}
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
onParseError({
|
|
108
|
+
yieldType: `Error extracting json schema schema.yieldType - ${e.message}`,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!jsonSchema.yieldType) {
|
|
113
|
+
onParseError({
|
|
114
|
+
yieldType: `Error extracting json schema schema.yieldType - schema.yieldType might be empty or it is not a valid suretype or typebox type`,
|
|
115
|
+
})
|
|
116
|
+
} else {
|
|
117
|
+
let yieldValidator: AJV.ValidateFunction | undefined
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
yieldValidator = ajv.compile(jsonSchema.yieldType as TJSONSchema)
|
|
121
|
+
} catch (e: any) {
|
|
122
|
+
onParseError({
|
|
123
|
+
yieldType: `Error compiling schema.yieldType for validator - ${e.message}`,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
validation.yield = (value: any) => {
|
|
128
|
+
if (!yieldValidator) {
|
|
129
|
+
return { errors: [{ message: 'Validator not initialized', keyword: 'internal' } as TSchemaValidationError] }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const valid = yieldValidator(value)
|
|
133
|
+
|
|
134
|
+
if (!valid) {
|
|
135
|
+
const errors = yieldValidator.errors
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
errors: errors?.length ? errors : undefined,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
96
147
|
return { jsonSchema, validation }
|
|
97
148
|
}
|
package/src/schema/types.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type TSchemaLib<SchemaLibType> =
|
|
|
9
9
|
? Static<SchemaLibType>
|
|
10
10
|
: unknown
|
|
11
11
|
|
|
12
|
+
// AsyncGenerator type extraction for streaming procedures
|
|
13
|
+
export type TSchemaLibGenerator<TYield, TReturn = void> =
|
|
14
|
+
AsyncGenerator<TSchemaLib<TYield>, TSchemaLib<TReturn>, unknown>
|
|
15
|
+
|
|
12
16
|
export type TJSONSchema = Record<string, any>
|
|
13
17
|
|
|
14
18
|
export type Prettify<TObject> = {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { captureDefinitionInfo, formatDefinitionInfo, DefinitionInfo } from './stack-utils.js'
|
|
3
|
+
|
|
4
|
+
describe('Stack Utils', () => {
|
|
5
|
+
describe('captureDefinitionInfo', () => {
|
|
6
|
+
test('returns definition info with definedAt', () => {
|
|
7
|
+
const info = captureDefinitionInfo()
|
|
8
|
+
|
|
9
|
+
// Should capture the call site in this test file
|
|
10
|
+
expect(info).toBeDefined()
|
|
11
|
+
expect(info.definitionStack).toBeDefined()
|
|
12
|
+
expect(typeof info.definitionStack).toBe('string')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('definedAt contains file, line, column when available', () => {
|
|
16
|
+
const info = captureDefinitionInfo()
|
|
17
|
+
|
|
18
|
+
// The definedAt should be present since we're calling from user code (test file)
|
|
19
|
+
if (info.definedAt) {
|
|
20
|
+
expect(info.definedAt.file).toBeDefined()
|
|
21
|
+
expect(typeof info.definedAt.file).toBe('string')
|
|
22
|
+
expect(info.definedAt.line).toBeDefined()
|
|
23
|
+
expect(typeof info.definedAt.line).toBe('number')
|
|
24
|
+
expect(info.definedAt.line).toBeGreaterThan(0)
|
|
25
|
+
expect(info.definedAt.column).toBeDefined()
|
|
26
|
+
expect(typeof info.definedAt.column).toBe('number')
|
|
27
|
+
expect(info.definedAt.column).toBeGreaterThan(0)
|
|
28
|
+
expect(info.definedAt.raw).toBeDefined()
|
|
29
|
+
expect(typeof info.definedAt.raw).toBe('string')
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('definitionStack contains Error stack trace', () => {
|
|
34
|
+
const info = captureDefinitionInfo()
|
|
35
|
+
|
|
36
|
+
expect(info.definitionStack).toContain('Error')
|
|
37
|
+
expect(info.definitionStack).toContain('at ')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('formatDefinitionInfo', () => {
|
|
42
|
+
test('returns undefined when definedAt is not present', () => {
|
|
43
|
+
const info: DefinitionInfo = {}
|
|
44
|
+
const result = formatDefinitionInfo(info, 'TestProcedure')
|
|
45
|
+
|
|
46
|
+
expect(result).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('returns formatted string when definedAt is present', () => {
|
|
50
|
+
const info: DefinitionInfo = {
|
|
51
|
+
definedAt: {
|
|
52
|
+
file: '/app/procedures/test.ts',
|
|
53
|
+
line: 42,
|
|
54
|
+
column: 5,
|
|
55
|
+
raw: 'at Object.<anonymous> (/app/procedures/test.ts:42:5)',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
const result = formatDefinitionInfo(info, 'TestProcedure')
|
|
59
|
+
|
|
60
|
+
expect(result).toBeDefined()
|
|
61
|
+
expect(result).toContain('--- Procedure "TestProcedure" defined at ---')
|
|
62
|
+
expect(result).toContain('/app/procedures/test.ts:42:5')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('includes procedure name in formatted output', () => {
|
|
66
|
+
const info: DefinitionInfo = {
|
|
67
|
+
definedAt: {
|
|
68
|
+
file: '/path/to/file.ts',
|
|
69
|
+
line: 10,
|
|
70
|
+
column: 3,
|
|
71
|
+
raw: 'at /path/to/file.ts:10:3',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
const result = formatDefinitionInfo(info, 'MyCustomProcedure')
|
|
75
|
+
|
|
76
|
+
expect(result).toContain('"MyCustomProcedure"')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('integration with procedure creation', () => {
|
|
81
|
+
test('captures location from calling code', () => {
|
|
82
|
+
// Helper to simulate what happens in Create()
|
|
83
|
+
function simulateCreate() {
|
|
84
|
+
return captureDefinitionInfo()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const info = simulateCreate()
|
|
88
|
+
|
|
89
|
+
// Should have captured the location of the simulateCreate() call
|
|
90
|
+
expect(info).toBeDefined()
|
|
91
|
+
expect(info.definitionStack).toBeDefined()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a specific location in source code where a procedure was defined.
|
|
3
|
+
*/
|
|
4
|
+
export type DefinitionLocation = {
|
|
5
|
+
file: string
|
|
6
|
+
line: number
|
|
7
|
+
column: number
|
|
8
|
+
raw: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Contains information about where a procedure was defined.
|
|
13
|
+
*/
|
|
14
|
+
export type DefinitionInfo = {
|
|
15
|
+
definedAt?: DefinitionLocation
|
|
16
|
+
definitionStack?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internal ts-procedures files that should be skipped when finding user code.
|
|
21
|
+
* Only skip the core library files, not test files or user code.
|
|
22
|
+
*/
|
|
23
|
+
const INTERNAL_FILES = [
|
|
24
|
+
'/index.ts',
|
|
25
|
+
'/index.js',
|
|
26
|
+
'/errors.ts',
|
|
27
|
+
'/errors.js',
|
|
28
|
+
'/stack-utils.ts',
|
|
29
|
+
'/stack-utils.js',
|
|
30
|
+
'/compute-schema.ts',
|
|
31
|
+
'/compute-schema.js',
|
|
32
|
+
'/parser.ts',
|
|
33
|
+
'/parser.js',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Captures the stack trace at the call site and extracts the definition location.
|
|
38
|
+
* Finds the first stack frame outside of ts-procedures internal files.
|
|
39
|
+
*/
|
|
40
|
+
export function captureDefinitionInfo(): DefinitionInfo {
|
|
41
|
+
const err = new Error()
|
|
42
|
+
const stack = err.stack
|
|
43
|
+
|
|
44
|
+
if (!stack) {
|
|
45
|
+
return {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lines = stack.split('\n')
|
|
49
|
+
|
|
50
|
+
// Find the first frame that's not from ts-procedures internals
|
|
51
|
+
// Skip the first line (Error message) and frames from this module
|
|
52
|
+
let userFrame: string | undefined
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < lines.length; i++) {
|
|
55
|
+
const rawLine = lines[i]
|
|
56
|
+
if (!rawLine) continue
|
|
57
|
+
const line = rawLine.trim()
|
|
58
|
+
|
|
59
|
+
// Skip empty or invalid frames
|
|
60
|
+
if (!line.startsWith('at ')) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Skip frames from ts-procedures internal source files
|
|
65
|
+
const isInternalFile = INTERNAL_FILES.some(file => line.includes(file))
|
|
66
|
+
if (isInternalFile) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Skip frames from ts-procedures in node_modules (when used as a dependency)
|
|
71
|
+
if (line.includes('/node_modules/ts-procedures/') || line.includes('\\node_modules\\ts-procedures\\')) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Skip internal node frames
|
|
76
|
+
if (line.includes('node:') || line.startsWith('at Module.') || line.startsWith('at Object.<anonymous> (node:')) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
userFrame = line
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!userFrame) {
|
|
85
|
+
return { definitionStack: stack }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const definedAt = parseStackFrame(userFrame)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
definedAt,
|
|
92
|
+
definitionStack: stack,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parses a V8 stack frame line to extract file, line, and column info.
|
|
98
|
+
* Handles formats like:
|
|
99
|
+
* - "at Object.<anonymous> (/path/to/file.ts:10:5)"
|
|
100
|
+
* - "at functionName (/path/to/file.ts:10:5)"
|
|
101
|
+
* - "at /path/to/file.ts:10:5"
|
|
102
|
+
*/
|
|
103
|
+
function parseStackFrame(frame: string): DefinitionLocation | undefined {
|
|
104
|
+
// Match patterns like "(path:line:column)" or just "path:line:column"
|
|
105
|
+
const match = frame.match(/\(([^)]+):(\d+):(\d+)\)$/) || frame.match(/at ([^:]+):(\d+):(\d+)$/)
|
|
106
|
+
|
|
107
|
+
if (match && match[1] && match[2] && match[3]) {
|
|
108
|
+
return {
|
|
109
|
+
file: match[1],
|
|
110
|
+
line: parseInt(match[2], 10),
|
|
111
|
+
column: parseInt(match[3], 10),
|
|
112
|
+
raw: frame,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Formats definition info for appending to error stacks.
|
|
121
|
+
*/
|
|
122
|
+
export function formatDefinitionInfo(info: DefinitionInfo, procedureName: string): string | undefined {
|
|
123
|
+
if (!info.definedAt) {
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { file, line, column } = info.definedAt
|
|
128
|
+
return `\n--- Procedure "${procedureName}" defined at ---\n at ${file}:${line}:${column}`
|
|
129
|
+
}
|