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.
@@ -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
+ } & {}