ts-procedures 1.0.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 +459 -0
- package/package.json +50 -0
- package/src/errors.ts +39 -0
- package/src/exports.ts +6 -0
- package/src/index.test.ts +364 -0
- package/src/index.ts +180 -0
- package/src/schema/compute-schema.test.ts +128 -0
- package/src/schema/compute-schema.ts +55 -0
- package/src/schema/extract-json-schema.test.ts +25 -0
- package/src/schema/extract-json-schema.ts +15 -0
- package/src/schema/parser.test.ts +156 -0
- package/src/schema/parser.ts +92 -0
- package/src/schema/resolve-schema-lib.test.ts +19 -0
- package/src/schema/resolve-schema-lib.ts +29 -0
- package/src/schema/types.ts +17 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { schemaParser, TSchemaValidationError } from './parser.js'
|
|
2
|
+
import { ProcedureRegistrationError } from '../errors.js'
|
|
3
|
+
import { TJSONSchema } from './types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This function is used to compute the JSON schema and validation functions
|
|
7
|
+
* for a given schema.
|
|
8
|
+
*
|
|
9
|
+
* @param name The name of the procedure
|
|
10
|
+
* @param schema Procedure schema
|
|
11
|
+
*/
|
|
12
|
+
export function computeSchema<TParamsSchemaType, TReturnTypeSchemaType>(
|
|
13
|
+
name: string,
|
|
14
|
+
schema?: {
|
|
15
|
+
params?: TParamsSchemaType
|
|
16
|
+
returnType?: TReturnTypeSchemaType
|
|
17
|
+
},
|
|
18
|
+
): {
|
|
19
|
+
jsonSchema: {
|
|
20
|
+
params?: TJSONSchema
|
|
21
|
+
returnType?: TJSONSchema
|
|
22
|
+
}
|
|
23
|
+
validations: {
|
|
24
|
+
params?: (params?: any) => { errors?: TSchemaValidationError[] }
|
|
25
|
+
}
|
|
26
|
+
} {
|
|
27
|
+
const jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema } = {
|
|
28
|
+
params: undefined,
|
|
29
|
+
returnType: undefined,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const validations: {
|
|
33
|
+
params?: (params?: any) => { errors?: TSchemaValidationError[] }
|
|
34
|
+
} = {}
|
|
35
|
+
|
|
36
|
+
if (schema) {
|
|
37
|
+
const {
|
|
38
|
+
jsonSchema: { params, returnType },
|
|
39
|
+
validation,
|
|
40
|
+
} = schemaParser(schema, (errors) => {
|
|
41
|
+
throw new ProcedureRegistrationError(
|
|
42
|
+
name,
|
|
43
|
+
`Error parsing schema for ${name} - ${Object.entries(errors)
|
|
44
|
+
.map(([key, error]) => `${key}: ${error}`)
|
|
45
|
+
.join(', ')}`,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
jsonSchema.params = params
|
|
50
|
+
jsonSchema.returnType = returnType
|
|
51
|
+
validations.params = validation.params
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { jsonSchema, validations }
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { Type } from 'typebox'
|
|
3
|
+
import { v } from 'suretype'
|
|
4
|
+
import { extractJsonSchema } from './extract-json-schema.js'
|
|
5
|
+
|
|
6
|
+
describe('extractJsonSchema()', () => {
|
|
7
|
+
const typebox = Type.Object({ name: Type.String() })
|
|
8
|
+
const suretype = v.object({ name: v.string().required() })
|
|
9
|
+
|
|
10
|
+
test('it extracts TypeBox json-schema', async () => {
|
|
11
|
+
expect(extractJsonSchema(typebox)).toMatchObject({
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: { name: { type: 'string' } },
|
|
14
|
+
required: ['name'],
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('it extracts Suretype json-schema', async () => {
|
|
19
|
+
expect(extractJsonSchema(suretype)).toMatchObject({
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: { name: { type: 'string' } },
|
|
22
|
+
required: ['name'],
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { extractSingleJsonSchema } from 'suretype'
|
|
2
|
+
import { isSuretypeSchema, isTypeboxSchema } from './resolve-schema-lib.js'
|
|
3
|
+
import { TJSONSchema } from './types.js'
|
|
4
|
+
|
|
5
|
+
export function extractJsonSchema(libSchema: unknown): TJSONSchema | undefined {
|
|
6
|
+
if (isTypeboxSchema(libSchema)) {
|
|
7
|
+
return libSchema
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (isSuretypeSchema(libSchema)) {
|
|
11
|
+
return extractSingleJsonSchema(libSchema)?.schema
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { extractSingleJsonSchema, v } from 'suretype'
|
|
3
|
+
import { schemaParser } from './parser.js'
|
|
4
|
+
import { Type } from 'typebox'
|
|
5
|
+
|
|
6
|
+
describe('schemaParser', () => {
|
|
7
|
+
test('it parses params to json-schema', async () => {
|
|
8
|
+
let done: () => void = () => void 0
|
|
9
|
+
const promise = new Promise<void>((r) => {
|
|
10
|
+
done = r
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const params = v.object({
|
|
14
|
+
name: v.string(),
|
|
15
|
+
age: v.number(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const result = schemaParser(
|
|
19
|
+
{
|
|
20
|
+
params: params,
|
|
21
|
+
},
|
|
22
|
+
(errors) => {
|
|
23
|
+
throw new Error(JSON.stringify(errors))
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
expect(result.jsonSchema.params).toEqual(
|
|
28
|
+
extractSingleJsonSchema(params)?.schema,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
done()
|
|
32
|
+
|
|
33
|
+
await promise
|
|
34
|
+
await promise
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('it parses params and generates a validator function', async () => {
|
|
38
|
+
let done: () => void = () => void 0
|
|
39
|
+
const promise = new Promise<void>((r) => {
|
|
40
|
+
done = r
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const params = v.object({
|
|
44
|
+
name: v.string(),
|
|
45
|
+
age: v.number().required(),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const result = schemaParser(
|
|
49
|
+
{
|
|
50
|
+
params: params,
|
|
51
|
+
},
|
|
52
|
+
(errors) => {
|
|
53
|
+
throw new Error(JSON.stringify(errors))
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
result.validation.params?.({
|
|
59
|
+
name: 'John',
|
|
60
|
+
age: 30,
|
|
61
|
+
})?.errors,
|
|
62
|
+
).toBeUndefined()
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
result.validation.params?.({
|
|
66
|
+
name: { name: '' },
|
|
67
|
+
age: 'poop',
|
|
68
|
+
})?.errors,
|
|
69
|
+
).toBeDefined()
|
|
70
|
+
|
|
71
|
+
expect(
|
|
72
|
+
result.validation.params?.({
|
|
73
|
+
name: { name: '' },
|
|
74
|
+
age: 'poop',
|
|
75
|
+
})?.errors?.length,
|
|
76
|
+
).toEqual(2)
|
|
77
|
+
|
|
78
|
+
done()
|
|
79
|
+
|
|
80
|
+
await promise
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('it parses returnType to json-schema', async () => {
|
|
84
|
+
let done: () => void = () => void 0
|
|
85
|
+
const promise = new Promise<void>((r) => {
|
|
86
|
+
done = r
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const returnType = v.object({
|
|
90
|
+
name: v.string(),
|
|
91
|
+
age: v.number(),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const result = schemaParser(
|
|
95
|
+
{
|
|
96
|
+
returnType: returnType,
|
|
97
|
+
},
|
|
98
|
+
(errors) => {
|
|
99
|
+
throw new Error(JSON.stringify(errors))
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(result.jsonSchema.returnType).toEqual(
|
|
104
|
+
extractSingleJsonSchema(returnType)?.schema,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
done()
|
|
108
|
+
|
|
109
|
+
await promise
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('it throws a meaningful error to the dev', async () => {
|
|
113
|
+
schemaParser(
|
|
114
|
+
// invalid params schema
|
|
115
|
+
{ params: { test: Type.String() } },
|
|
116
|
+
(errors) => {
|
|
117
|
+
expect(errors.params).toMatch(/Error extracting json schema schema.params/)
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
schemaParser(
|
|
122
|
+
// invalid returnType schema
|
|
123
|
+
{ returnType: 'string value' },
|
|
124
|
+
(errors) => {
|
|
125
|
+
expect(errors.returnType).toMatch(/Error extracting json schema schema.returnType/)
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('it parses multiple schemas correct', async () => {
|
|
131
|
+
const schema = schemaParser(
|
|
132
|
+
{
|
|
133
|
+
params: Type.Object({ a: Type.String() }),
|
|
134
|
+
returnType: Type.Object({ b: Type.Null() }),
|
|
135
|
+
},
|
|
136
|
+
(error) => {
|
|
137
|
+
throw new Error(JSON.stringify(error))
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const schema2= schemaParser(
|
|
142
|
+
{
|
|
143
|
+
params: Type.Object({ c: Type.String() }),
|
|
144
|
+
returnType: Type.Object({ d: Type.Number() }),
|
|
145
|
+
},
|
|
146
|
+
(error) => {
|
|
147
|
+
throw new Error(JSON.stringify(error))
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
expect(schema.validation.params?.({}).errors?.[0]?.message).toMatch(/must have required property 'a'/)
|
|
152
|
+
expect(schema2.validation.params?.({ c: 'test' })
|
|
153
|
+
).toMatchObject({})
|
|
154
|
+
expect(schema.validation.params?.({}).errors?.[0]?.message).toMatch(/must have required property 'a'/)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {default as addFormats} from 'ajv-formats'
|
|
2
|
+
import * as AJV from 'ajv'
|
|
3
|
+
import { extractJsonSchema } from './extract-json-schema.js'
|
|
4
|
+
import { TJSONSchema } from './types.js'
|
|
5
|
+
|
|
6
|
+
export type TSchemaParsed = {
|
|
7
|
+
jsonSchema: { params?: TJSONSchema; returnType?: TJSONSchema }
|
|
8
|
+
validation: { params?: (params: any) => { errors?: TSchemaValidationError[] } }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TSchemaValidationError = AJV.ErrorObject
|
|
12
|
+
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
const ajv = addFormats(
|
|
15
|
+
new AJV.Ajv({
|
|
16
|
+
allErrors: true,
|
|
17
|
+
coerceTypes: true,
|
|
18
|
+
removeAdditional: true,
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export function schemaParser(
|
|
23
|
+
schema: { params?: unknown; returnType?: unknown },
|
|
24
|
+
onParseError: (errors: { params?: string; returnType?: string }) => void,
|
|
25
|
+
): TSchemaParsed {
|
|
26
|
+
const jsonSchema: TSchemaParsed['jsonSchema'] = {}
|
|
27
|
+
const validation: TSchemaParsed['validation'] = {}
|
|
28
|
+
|
|
29
|
+
if (schema.params) {
|
|
30
|
+
try {
|
|
31
|
+
const extracted = extractJsonSchema(schema.params as TJSONSchema)
|
|
32
|
+
|
|
33
|
+
if (extracted) {
|
|
34
|
+
jsonSchema.params = extracted
|
|
35
|
+
}
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
onParseError({
|
|
38
|
+
params: `Error extracting json schema schema.params - ${e.message}`,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!jsonSchema.params) {
|
|
43
|
+
onParseError({
|
|
44
|
+
params: `Error extracting json schema schema.params - schema.params might be empty or it is not a valid suretype or typebox type`,
|
|
45
|
+
})
|
|
46
|
+
} else {
|
|
47
|
+
let paramsValidator: AJV.ValidateFunction
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
paramsValidator = ajv.compile(jsonSchema.params as TJSONSchema)
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
onParseError({
|
|
53
|
+
params: `Error compiling schema.params for validator - ${e.message}`,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validation.params = (params: any) => {
|
|
58
|
+
const valid = paramsValidator(params)
|
|
59
|
+
|
|
60
|
+
if (!valid) {
|
|
61
|
+
const errors = paramsValidator.errors
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
errors: errors?.length ? errors : undefined,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (schema.returnType) {
|
|
74
|
+
try {
|
|
75
|
+
const extracted = extractJsonSchema(schema.returnType as TJSONSchema)
|
|
76
|
+
|
|
77
|
+
jsonSchema.returnType = extracted
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
onParseError({
|
|
80
|
+
returnType: `Error extracting json schema schema.returnType - ${e.message}`,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!jsonSchema.returnType) {
|
|
85
|
+
onParseError({
|
|
86
|
+
returnType: `Error extracting json schema schema.returnType - schema.returnType might be empty or it is not a valid suretype or typebox type`,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { jsonSchema, validation }
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { isSuretypeSchema, isTypeboxSchema } from './resolve-schema-lib.js'
|
|
3
|
+
import { Type } from 'typebox'
|
|
4
|
+
import { v } from 'suretype'
|
|
5
|
+
|
|
6
|
+
describe('lib schema resolvers', () => {
|
|
7
|
+
const typebox = Type.Object({ name: Type.String() })
|
|
8
|
+
const suretype = v.object({ name: v.string() })
|
|
9
|
+
|
|
10
|
+
test('it recognizes TypeBox schema', async () => {
|
|
11
|
+
expect(isTypeboxSchema(typebox)).toBe(true)
|
|
12
|
+
expect(isTypeboxSchema(suretype)).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('it recognizes Suretype schema', async () => {
|
|
16
|
+
expect(isSuretypeSchema(suretype)).toBe(true)
|
|
17
|
+
expect(isSuretypeSchema(typebox)).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CoreValidator } from 'suretype'
|
|
2
|
+
import { Type } from 'typebox'
|
|
3
|
+
|
|
4
|
+
export type IsTypeboxSchema<TSchema> = TSchema extends {
|
|
5
|
+
static: unknown
|
|
6
|
+
params: unknown
|
|
7
|
+
}
|
|
8
|
+
? true
|
|
9
|
+
: false
|
|
10
|
+
|
|
11
|
+
export function isTypeboxSchema(schema: any): schema is Type.TSchema {
|
|
12
|
+
return (
|
|
13
|
+
// typebox v1
|
|
14
|
+
(typeof schema === 'object' && '~kind' in schema) ||
|
|
15
|
+
// @sinclair/typebox v0.3x
|
|
16
|
+
(typeof schema === 'object' && Symbol.for('TypeBox.Kind') in schema)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type IsSuretypeSchema<TSchema> = TSchema extends {
|
|
21
|
+
required: () => object
|
|
22
|
+
nullable?: never
|
|
23
|
+
}
|
|
24
|
+
? true
|
|
25
|
+
: false
|
|
26
|
+
|
|
27
|
+
export function isSuretypeSchema(schema: any): schema is CoreValidator<any> {
|
|
28
|
+
return typeof schema === 'object' && 'getJsonSchemaObject' in schema
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
import { CoreValidator, TypeOf } from 'suretype'
|
|
3
|
+
import { Static, TSchema } from 'typebox'
|
|
4
|
+
|
|
5
|
+
// Determine if the generic "SchemaLibType" is Suretype's CoreValidator or Typebox's TSchema
|
|
6
|
+
export type TSchemaLib<SchemaLibType> =
|
|
7
|
+
SchemaLibType extends CoreValidator<any>
|
|
8
|
+
? TypeOf<SchemaLibType>
|
|
9
|
+
: SchemaLibType extends TSchema
|
|
10
|
+
? Static<SchemaLibType>
|
|
11
|
+
: unknown
|
|
12
|
+
|
|
13
|
+
export type TJSONSchema = Record<string, any>
|
|
14
|
+
|
|
15
|
+
export type Prettify<TObject> = {
|
|
16
|
+
[Key in keyof TObject]: TObject[Key]
|
|
17
|
+
} & {}
|