@sapporta/rest-open-api 3.52.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.
@@ -0,0 +1,235 @@
1
+ import { z } from 'zod';
2
+ import { ZOD_SYNC, ZOD_ASYNC } from './test-helpers';
3
+ import { getParamsFromPathOnly, getPathParameterSchema } from './path-params';
4
+ import { ParameterObject } from 'openapi3-ts';
5
+ import { ContractAnyType } from '@sapporta/rest-core';
6
+
7
+ /**
8
+ * Theres a few permutations to deal with, mainly sync and async, so we build up CASES then run them against both
9
+ */
10
+ const CASES: {
11
+ type: 'zod';
12
+ name: string;
13
+ path: string;
14
+ pathParams: unknown;
15
+ expected: ParameterObject[];
16
+ only?: true;
17
+ }[] = [
18
+ {
19
+ type: 'zod',
20
+ name: 'no params, no schema',
21
+ path: '/',
22
+ pathParams: undefined,
23
+ expected: [],
24
+ },
25
+ {
26
+ type: 'zod',
27
+ name: 'single param, no schema',
28
+ path: '/:id',
29
+ pathParams: undefined,
30
+ expected: [
31
+ {
32
+ name: 'id',
33
+ in: 'path',
34
+ required: true,
35
+ schema: {
36
+ type: 'string',
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ type: 'zod',
43
+ name: 'multiple params, no schema',
44
+ pathParams: undefined,
45
+ path: '/:id/:name',
46
+ expected: [
47
+ {
48
+ name: 'id',
49
+ in: 'path',
50
+ required: true,
51
+ schema: {
52
+ type: 'string',
53
+ },
54
+ },
55
+ {
56
+ name: 'name',
57
+ in: 'path',
58
+ required: true,
59
+ schema: {
60
+ type: 'string',
61
+ },
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ type: 'zod',
67
+ name: 'single param, zod schema',
68
+ path: '/:id',
69
+ pathParams: z.object({
70
+ id: z.string().describe('The id of the post'),
71
+ }),
72
+ expected: [
73
+ {
74
+ name: 'id',
75
+ description: 'The id of the post',
76
+ in: 'path',
77
+ required: true,
78
+ schema: {
79
+ type: 'string',
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ {
85
+ type: 'zod',
86
+ name: 'multiple params, zod schema',
87
+ path: '/:id/:name',
88
+ pathParams: z.object({
89
+ id: z.string(),
90
+ name: z.string(),
91
+ }),
92
+ expected: [
93
+ {
94
+ name: 'id',
95
+ in: 'path',
96
+ required: true,
97
+ schema: {
98
+ type: 'string',
99
+ },
100
+ },
101
+ {
102
+ name: 'name',
103
+ in: 'path',
104
+ required: true,
105
+ schema: {
106
+ type: 'string',
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ {
112
+ type: 'zod',
113
+ name: 'multiple params, partial zod schema',
114
+ path: '/:id/:name/:extra',
115
+ pathParams: z.object({
116
+ id: z.string().optional(),
117
+ name: z.string().optional(),
118
+ // extra is not in the schema
119
+ }),
120
+ expected: [
121
+ {
122
+ name: 'id',
123
+ in: 'path',
124
+ schema: {
125
+ type: 'string',
126
+ },
127
+ },
128
+ {
129
+ name: 'name',
130
+ in: 'path',
131
+ schema: {
132
+ type: 'string',
133
+ },
134
+ },
135
+ {
136
+ name: 'extra',
137
+ in: 'path',
138
+ required: true,
139
+ schema: {
140
+ type: 'string',
141
+ },
142
+ },
143
+ ],
144
+ },
145
+ {
146
+ type: 'zod',
147
+ name: 'single param, zod schema, optional',
148
+ path: '/:id',
149
+ pathParams: z.object({
150
+ id: z.string().optional(),
151
+ }),
152
+ expected: [
153
+ {
154
+ name: 'id',
155
+ in: 'path',
156
+ schema: {
157
+ type: 'string',
158
+ },
159
+ },
160
+ ],
161
+ },
162
+ ];
163
+
164
+ const hasOnly = CASES.some((c) => c.only);
165
+ const CASES_TO_RUN = hasOnly ? CASES.filter((c) => c.only) : CASES;
166
+
167
+ describe('path-params', () => {
168
+ it.each(CASES_TO_RUN)(
169
+ 'sync - $name ($type)',
170
+ ({ type, name, path, pathParams, expected }) => {
171
+ const res = getPathParameterSchema.sync({
172
+ transformSchema: ZOD_SYNC,
173
+ appRoute: {
174
+ method: 'GET',
175
+ path,
176
+ pathParams: pathParams as ContractAnyType | undefined,
177
+ responses: {
178
+ 200: z.object({
179
+ name: z.string(),
180
+ }),
181
+ },
182
+ },
183
+ id: 'testFunc',
184
+ concatenatedPath: 'testFunc',
185
+ });
186
+
187
+ expect(res).toEqual(expected);
188
+ },
189
+ );
190
+
191
+ it.each(CASES_TO_RUN)(
192
+ 'async - $name ($type)',
193
+ async ({ type, name, path, pathParams, expected }) => {
194
+ const res = await getPathParameterSchema.async({
195
+ transformSchema: ZOD_ASYNC,
196
+ appRoute: {
197
+ method: 'GET',
198
+ path,
199
+ pathParams: pathParams as ContractAnyType | undefined,
200
+ responses: {
201
+ 200: z.object({
202
+ name: z.string(),
203
+ }),
204
+ },
205
+ },
206
+ id: 'testFunc',
207
+ concatenatedPath: 'testFunc',
208
+ });
209
+
210
+ expect(res).toEqual(expected);
211
+ },
212
+ );
213
+ });
214
+
215
+ describe('getParamsFromPathOnly', () => {
216
+ it('should return the correct params', () => {
217
+ const params = getParamsFromPathOnly('/posts/:id/comments/:commentId');
218
+ const paramsArray = Array.from(params.values());
219
+
220
+ expect(paramsArray).toEqual([
221
+ {
222
+ name: 'id',
223
+ in: 'path',
224
+ required: true,
225
+ schema: { type: 'string' },
226
+ },
227
+ {
228
+ name: 'commentId',
229
+ in: 'path',
230
+ required: true,
231
+ schema: { type: 'string' },
232
+ },
233
+ ]);
234
+ });
235
+ });
@@ -0,0 +1,140 @@
1
+ import { AppRoute } from '@sapporta/rest-core';
2
+ import {
3
+ AsyncAndSyncHelper,
4
+ GetAsyncFunction,
5
+ GetSyncFunction,
6
+ SchemaTransformerAsync,
7
+ SchemaTransformerSync,
8
+ } from '../types';
9
+ import { ParameterObject } from 'openapi3-ts';
10
+ import { schemaObjectToParameters } from './utils';
11
+
12
+ type GetPathParameterHelper = AsyncAndSyncHelper<
13
+ {
14
+ appRoute: AppRoute;
15
+ id: string;
16
+ concatenatedPath: string;
17
+ },
18
+ {
19
+ transformSchema: SchemaTransformerSync;
20
+ },
21
+ {
22
+ transformSchema: SchemaTransformerAsync;
23
+ },
24
+ Array<ParameterObject>
25
+ >;
26
+
27
+ /**
28
+ * We build up the params from both the path and the schema.
29
+ *
30
+ * This builds up the record of name -> parameter object.
31
+ */
32
+ export const getParamsFromPathOnly = (
33
+ path: string,
34
+ ): Map<string, ParameterObject> => {
35
+ const params = new Map<string, ParameterObject>();
36
+
37
+ const paramsInPath =
38
+ path.match(/:([^/]+)/g)?.map((param) => param.slice(1)) || [];
39
+
40
+ for (const param of paramsInPath) {
41
+ params.set(param, {
42
+ name: param,
43
+ in: 'path' as const,
44
+ required: true,
45
+ schema: { type: 'string' },
46
+ });
47
+ }
48
+
49
+ return params;
50
+ };
51
+
52
+ /**
53
+ * Should return schema params as priority, and then path params as fallback
54
+ *
55
+ * @param pathParams - params inferred from the path i.e. just from the string
56
+ * @param schemaParams - params from the schema
57
+ */
58
+ const mergeParams = (
59
+ pathParams: Map<string, ParameterObject>,
60
+ schemaParams: Map<string, ParameterObject>,
61
+ ): Array<ParameterObject> => {
62
+ const resultMap = new Map<string, ParameterObject>();
63
+
64
+ for (const [name, param] of pathParams.entries()) {
65
+ resultMap.set(name, param);
66
+ }
67
+
68
+ for (const [name, param] of schemaParams.entries()) {
69
+ resultMap.set(name, param);
70
+ }
71
+
72
+ return Array.from(resultMap.values());
73
+ };
74
+
75
+ const syncFunc: GetSyncFunction<GetPathParameterHelper> = ({
76
+ transformSchema,
77
+ appRoute,
78
+ id,
79
+ concatenatedPath,
80
+ }) => {
81
+ const schema = appRoute.pathParams;
82
+
83
+ const paramsMap = getParamsFromPathOnly(appRoute.path);
84
+
85
+ const transformedSchema = transformSchema({
86
+ schema,
87
+ appRoute,
88
+ id,
89
+ concatenatedPath,
90
+ type: 'path',
91
+ });
92
+
93
+ if (!transformedSchema) {
94
+ return Array.from(paramsMap.values());
95
+ }
96
+
97
+ const schemaParams = schemaObjectToParameters(transformedSchema, 'path');
98
+
99
+ const schemaParamsMap = new Map(
100
+ schemaParams.map((param) => [param.name, param]),
101
+ );
102
+
103
+ return mergeParams(paramsMap, schemaParamsMap);
104
+ };
105
+
106
+ const asyncFunc: GetAsyncFunction<GetPathParameterHelper> = async ({
107
+ transformSchema,
108
+ appRoute,
109
+ id,
110
+ concatenatedPath,
111
+ }) => {
112
+ const schema = appRoute.pathParams;
113
+
114
+ const paramsMap = getParamsFromPathOnly(appRoute.path);
115
+
116
+ const transformedSchema = await transformSchema({
117
+ schema,
118
+ appRoute,
119
+ id,
120
+ concatenatedPath,
121
+ type: 'path',
122
+ });
123
+
124
+ if (!transformedSchema) {
125
+ return Array.from(paramsMap.values());
126
+ }
127
+
128
+ const schemaParams = schemaObjectToParameters(transformedSchema, 'path');
129
+
130
+ const schemaParamsMap = new Map(
131
+ schemaParams.map((param) => [param.name, param]),
132
+ );
133
+
134
+ return mergeParams(paramsMap, schemaParamsMap);
135
+ };
136
+
137
+ export const getPathParameterSchema: GetPathParameterHelper = {
138
+ sync: syncFunc,
139
+ async: asyncFunc,
140
+ };
@@ -0,0 +1,129 @@
1
+ import { z } from 'zod';
2
+ import { getQueryParameterSchema } from './query-params';
3
+ import { ZOD_SYNC, ZOD_ASYNC } from './test-helpers';
4
+
5
+ describe('query-params', () => {
6
+ describe('zod', () => {
7
+ it('sync - single required param ', async () => {
8
+ const res = getQueryParameterSchema.sync({
9
+ transformSchema: ZOD_SYNC,
10
+ appRoute: {
11
+ method: 'GET',
12
+ path: '/',
13
+ query: z.object({
14
+ name: z.string(),
15
+ }),
16
+ responses: {
17
+ 200: z.object({
18
+ name: z.string(),
19
+ }),
20
+ },
21
+ },
22
+ id: 'testFunc',
23
+ concatenatedPath: 'testFunc',
24
+ });
25
+
26
+ expect(res).toEqual([
27
+ {
28
+ name: 'name',
29
+ in: 'query',
30
+ required: true,
31
+ schema: {
32
+ type: 'string',
33
+ },
34
+ },
35
+ ]);
36
+ });
37
+
38
+ it('sync - single optional param', async () => {
39
+ const res = getQueryParameterSchema.sync({
40
+ transformSchema: ZOD_SYNC,
41
+ appRoute: {
42
+ method: 'GET',
43
+ path: '/',
44
+ query: z.object({
45
+ name: z.string().optional(),
46
+ }),
47
+ responses: {
48
+ 200: z.object({
49
+ name: z.string(),
50
+ }),
51
+ },
52
+ },
53
+ id: 'testFunc',
54
+ concatenatedPath: 'testFunc',
55
+ });
56
+
57
+ expect(res).toEqual([
58
+ {
59
+ name: 'name',
60
+ in: 'query',
61
+ schema: {
62
+ type: 'string',
63
+ },
64
+ },
65
+ ]);
66
+ });
67
+
68
+ it('async - single required param ', async () => {
69
+ const res = await getQueryParameterSchema.async({
70
+ transformSchema: ZOD_ASYNC,
71
+ appRoute: {
72
+ method: 'GET',
73
+ path: '/',
74
+ query: z.object({
75
+ name: z.string(),
76
+ }),
77
+ responses: {
78
+ 200: z.object({
79
+ name: z.string(),
80
+ }),
81
+ },
82
+ },
83
+ id: 'testFunc',
84
+ concatenatedPath: 'testFunc',
85
+ });
86
+
87
+ expect(res).toEqual([
88
+ {
89
+ name: 'name',
90
+ in: 'query',
91
+ required: true,
92
+ schema: {
93
+ type: 'string',
94
+ },
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it('async - single optional param', async () => {
100
+ const result = await getQueryParameterSchema.async({
101
+ transformSchema: ZOD_ASYNC,
102
+ appRoute: {
103
+ method: 'GET',
104
+ path: '/',
105
+ query: z.object({
106
+ name: z.string().optional(),
107
+ }),
108
+ responses: {
109
+ 200: z.object({
110
+ name: z.string(),
111
+ }),
112
+ },
113
+ },
114
+ id: 'testFunc',
115
+ concatenatedPath: 'testFunc',
116
+ });
117
+
118
+ expect(result).toEqual([
119
+ {
120
+ name: 'name',
121
+ in: 'query',
122
+ schema: {
123
+ type: 'string',
124
+ },
125
+ },
126
+ ]);
127
+ });
128
+ });
129
+ });
@@ -0,0 +1,89 @@
1
+ import { AppRoute, isStandardSchema } from '@sapporta/rest-core';
2
+ import {
3
+ AsyncAndSyncHelper,
4
+ GetAsyncFunction,
5
+ GetSyncFunction,
6
+ SchemaTransformerAsync,
7
+ SchemaTransformerSync,
8
+ } from '../types';
9
+ import { ParameterObject } from 'openapi3-ts';
10
+ import { schemaObjectToParameters } from './utils';
11
+
12
+ type GetQueryParameterHelper = AsyncAndSyncHelper<
13
+ {
14
+ appRoute: AppRoute;
15
+ id: string;
16
+ concatenatedPath: string;
17
+ jsonQuery?: boolean;
18
+ },
19
+ {
20
+ transformSchema: SchemaTransformerSync;
21
+ },
22
+ {
23
+ transformSchema: SchemaTransformerAsync;
24
+ },
25
+ Array<ParameterObject>
26
+ >;
27
+
28
+ const syncFunc: GetSyncFunction<GetQueryParameterHelper> = ({
29
+ transformSchema,
30
+ appRoute,
31
+ id,
32
+ concatenatedPath,
33
+ jsonQuery = false,
34
+ }) => {
35
+ const schema = appRoute.query;
36
+ const isSchema = isStandardSchema(schema);
37
+
38
+ if (!isSchema) {
39
+ return [];
40
+ }
41
+
42
+ const transformedSchema = transformSchema({
43
+ schema,
44
+ appRoute,
45
+ id,
46
+ concatenatedPath,
47
+ type: 'query',
48
+ });
49
+
50
+ if (!transformedSchema) {
51
+ return [];
52
+ }
53
+
54
+ return schemaObjectToParameters(transformedSchema, 'query', jsonQuery);
55
+ };
56
+
57
+ const asyncFunc: GetAsyncFunction<GetQueryParameterHelper> = async ({
58
+ transformSchema,
59
+ appRoute,
60
+ id,
61
+ concatenatedPath,
62
+ jsonQuery = false,
63
+ }) => {
64
+ const schema = appRoute.query;
65
+ const isSchema = isStandardSchema(schema);
66
+
67
+ if (!isSchema) {
68
+ return [];
69
+ }
70
+
71
+ const transformedSchema = await transformSchema({
72
+ schema,
73
+ appRoute,
74
+ id,
75
+ concatenatedPath,
76
+ type: 'query',
77
+ });
78
+
79
+ if (!transformedSchema) {
80
+ return [];
81
+ }
82
+
83
+ return schemaObjectToParameters(transformedSchema, 'query', jsonQuery);
84
+ };
85
+
86
+ export const getQueryParameterSchema: GetQueryParameterHelper = {
87
+ sync: syncFunc,
88
+ async: asyncFunc,
89
+ };
@@ -0,0 +1,26 @@
1
+ import { SchemaObject } from 'openapi3-ts';
2
+ import { z } from 'zod';
3
+ import { SchemaTransformerAsync, SchemaTransformerSync } from '../types';
4
+
5
+ const stripJsonSchemaMetadata = (schema: unknown): SchemaObject => {
6
+ if (!schema || typeof schema !== 'object') {
7
+ return schema as SchemaObject;
8
+ }
9
+
10
+ const { $schema, ...rest } = schema as Record<string, unknown>;
11
+ return rest as SchemaObject;
12
+ };
13
+
14
+ export const zodV4SchemaTransformer: SchemaTransformerSync = ({ schema }) => {
15
+ if (!schema || typeof schema !== 'object' || !('_zod' in schema)) {
16
+ return null;
17
+ }
18
+
19
+ return stripJsonSchemaMetadata(z.toJSONSchema(schema as z.ZodTypeAny));
20
+ };
21
+
22
+ export const ZOD_SYNC = zodV4SchemaTransformer;
23
+
24
+ export const ZOD_ASYNC: SchemaTransformerAsync = async (args) => {
25
+ return zodV4SchemaTransformer(args);
26
+ };