@smartlyio/oats-runtime 4.4.0 → 4.4.1-alpha.18

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/src/server.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { assert } from './assert';
2
2
  import safeNavigation from '@smartlyio/safe-navigation';
3
3
  import { Make, MakeOptions, Maker, ValidationError, validationErrorPrinter } from './make';
4
+ import { serialize } from './serialize';
4
5
 
5
6
  export type { RedirectStatus } from './redirect';
6
7
 
@@ -128,17 +129,34 @@ function voidify(value: object | undefined | null) {
128
129
  return null;
129
130
  }
130
131
 
131
- function cleanHeaders<H>(maker: Maker<any, H>, headers: object) {
132
- const normalized = voidify(lowercaseObject(headers));
132
+ function cleanHeaders<H>(mode: Mode, maker: Maker<any, H>, headers: object) {
133
+ // note: we now expect that on client side headers are type conforming so do not need to lowercase those
134
+ // this is slightly breaking change is somebody has subverted the type checker
135
+ const normalized = voidify(mode === 'server' ? lowercaseObject(headers) : headers);
133
136
  const acceptsNull = maker(null);
134
137
  if (acceptsNull.isSuccess()) {
135
138
  return acceptsNull.success();
136
139
  }
137
140
  return maker(normalized, {
138
- unknownField: 'drop'
141
+ unknownField: 'drop',
142
+ convertFromNetwork: mode === 'server'
139
143
  }).success(throwRequestValidationError.bind(null, 'headers'));
140
144
  }
141
145
 
146
+ function serializeWhenClient(mode: Mode, value: any) {
147
+ if (mode === 'client') {
148
+ return serialize(value);
149
+ }
150
+ return value;
151
+ }
152
+
153
+ function getOutBody<Body extends RequestBody<any>>(mode: Mode, value: Body): Body {
154
+ if (!value) {
155
+ return value;
156
+ }
157
+ return { contentType: value.contentType, value: serializeWhenClient(mode, value.value) } as Body;
158
+ }
159
+
142
160
  export function safe<
143
161
  H extends Headers,
144
162
  P extends Params,
@@ -153,7 +171,7 @@ export function safe<
153
171
  body: Maker<any, Body>,
154
172
  response: Maker<any, R>,
155
173
  endpoint: Endpoint<H, P, Q, Body, R, RC>,
156
- { validationOptions = {} }: HandlerOptions = {}
174
+ { validationOptions = {}, mode }: HandlerOptions & InternalHandlerOptions = { mode: 'client' }
157
175
  ): Endpoint<
158
176
  Headers,
159
177
  Params,
@@ -163,24 +181,51 @@ export function safe<
163
181
  RequestContext
164
182
  > {
165
183
  return async ctx => {
184
+ // note: data coming to client adapter needs to be serialized and data coming from the adapter needs conversion from network
185
+ // note: data coming to server adapter needs to be deserialized and data coming from the adapter needs serialization
186
+ // this is all very clear.
187
+ const internalMakeOptions = { convertFromNetwork: mode === 'server' };
166
188
  const result = await endpoint({
167
189
  path: ctx.path,
168
190
  method: ctx.method,
169
191
  servers: ctx.servers,
170
192
  op: ctx.op,
171
- headers: cleanHeaders(headers, ctx.headers),
172
- params: params(voidify(ctx.params), validationOptions.params).success(
173
- throwRequestValidationError.bind(null, 'params')
193
+ headers: serializeWhenClient(mode, cleanHeaders(mode, headers, ctx.headers)),
194
+ // note: path params come to the adapter in network format always as those are gleaned from the path definition
195
+ params: params(voidify(ctx.params), {
196
+ ...validationOptions.params,
197
+ ...internalMakeOptions,
198
+ convertFromNetwork: true
199
+ }).success(throwRequestValidationError.bind(null, 'params')),
200
+ query: serializeWhenClient(
201
+ mode,
202
+ query(ctx.query || {}, { ...validationOptions.query, ...internalMakeOptions }).success(
203
+ throwRequestValidationError.bind(null, 'query')
204
+ )
174
205
  ),
175
- query: query(ctx.query || {}, validationOptions.query).success(
176
- throwRequestValidationError.bind(null, 'query')
206
+ body: getOutBody<Body>(
207
+ mode,
208
+ body(voidify(ctx.body), internalMakeOptions).success(
209
+ throwRequestValidationError.bind(null, 'body')
210
+ )
177
211
  ),
178
- body: body(voidify(ctx.body)).success(throwRequestValidationError.bind(null, 'body')),
179
212
  requestContext: ctx.requestContext as any
180
213
  });
181
- return response(result).success(
214
+ const responseValue = response(result, { convertFromNetwork: mode === 'client' }).success(
182
215
  throwResponseValidationError.bind(null, `body ${ctx.path}`, result.value.value)
183
216
  );
217
+ if (mode === 'client' || !responseValue) {
218
+ return responseValue;
219
+ }
220
+ // the response must be serialized for transferring from server to client
221
+ return {
222
+ ...responseValue,
223
+ headers: serialize(responseValue.headers),
224
+ value: {
225
+ ...responseValue.value,
226
+ value: serialize(responseValue.value.value)
227
+ }
228
+ };
184
229
  };
185
230
  }
186
231
 
@@ -202,7 +247,7 @@ interface CheckingTree {
202
247
  [method: string]: {
203
248
  safeHandler: (
204
249
  e: Endpoint<any, any, any, any, any, any>,
205
- opts?: HandlerOptions
250
+ opts?: HandlerOptions & InternalHandlerOptions
206
251
  ) => SafeEndpoint;
207
252
  op: string;
208
253
  servers: string[];
@@ -228,7 +273,10 @@ function createTree(handlers: Handler[]): CheckingTree {
228
273
  memo[element.path] = {};
229
274
  }
230
275
  memo[element.path][element.method] = {
231
- safeHandler: (e: Endpoint<any, any, any, any, any, any>, opts?: HandlerOptions) =>
276
+ safeHandler: (
277
+ e: Endpoint<any, any, any, any, any, any>,
278
+ opts?: HandlerOptions & InternalHandlerOptions
279
+ ) =>
232
280
  safe(
233
281
  element.headers,
234
282
  element.params,
@@ -260,6 +308,15 @@ export type ServerAdapter = (
260
308
  servers: string[]
261
309
  ) => void;
262
310
 
311
+ type Mode = 'client' | 'server';
312
+ export interface InternalHandlerOptions {
313
+ /** whether we are calling this on client side or in server side.
314
+ * This may have effect on eg network <-> ts property mapping.
315
+ * This value is set automatically by oats.
316
+ * */
317
+ mode: Mode;
318
+ }
319
+
263
320
  export interface HandlerOptions {
264
321
  /**
265
322
  * Options for request schema validation.
@@ -287,7 +344,7 @@ export function createHandlerFactory<Spec>(
287
344
  path,
288
345
  endpointWrapper.op,
289
346
  assertMethod(method),
290
- endpointWrapper.safeHandler(methodHandler, opts),
347
+ endpointWrapper.safeHandler(methodHandler, { ...opts, mode: 'server' }),
291
348
  endpointWrapper.servers
292
349
  );
293
350
  });
@@ -0,0 +1,63 @@
1
+ import { Type } from './reflection-type';
2
+ import { ValueClass } from './value-class';
3
+
4
+ export function withType<A>(to: A, type: Type[]): A {
5
+ if (type.length === 0) {
6
+ return to;
7
+ }
8
+ // todo: is it possible to confuse typing by re-use of valueclassed values
9
+ // eg. with value 'v: SomeValueClass' passing it to multiple makers
10
+ // -> no? the re-use happens only if the valueclass instance matches so it
11
+ // will not mutate the tagged reflection types
12
+ const previous = getTypeSet(to);
13
+ if (previous) {
14
+ type.forEach(type => previous.add(type));
15
+ return to;
16
+ }
17
+ const newType = new Set(type);
18
+ // tag each constructed object with a hidden type property
19
+ Object.defineProperty(to, reflection, {
20
+ enumerable: false,
21
+ value: newType
22
+ });
23
+ return to;
24
+ }
25
+
26
+ export function getTypeSet(value: Record<string, any>): Set<Type> | undefined {
27
+ if (!value || typeof value !== 'object') {
28
+ return;
29
+ }
30
+
31
+ // NOTE: prefer using the added reflection type instead of the contstructor type
32
+ // the added reflection type will have types from eg allOf
33
+ // @ts-ignore
34
+ const t: Set<Type> = value[reflection];
35
+ if (t && t.size > 0) {
36
+ return t;
37
+ }
38
+ if (value instanceof ValueClass) {
39
+ // a bit of leap here to trust that all ValueClasses have generated `reflection`
40
+ // @ts-ignore
41
+ const classType = new Set([value.constructor.reflection().definition]);
42
+ // tag each constructed object with a hidden type property
43
+ // this ensures that ValueClasses have a mutable reflection type property
44
+ Object.defineProperty(value, reflection, {
45
+ enumerable: false,
46
+ value: classType
47
+ });
48
+ return classType;
49
+ }
50
+ return;
51
+ }
52
+
53
+ /** Get reflection type from a made value for serialization.
54
+ * Note that only directly made object values or ValueClasses can be used
55
+ * */
56
+ export function getType(value: Record<string, any>): Type[] | undefined {
57
+ const t = getTypeSet(value);
58
+ if (t) {
59
+ return [...t];
60
+ }
61
+ }
62
+
63
+ const reflection = Symbol('reflection');