@opra/core 0.33.11 → 1.0.0-alpha.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.
Files changed (155) hide show
  1. package/cjs/augmentation/18n.augmentation.js +17 -4
  2. package/cjs/augmentation/http-controller.augmentation.js +25 -0
  3. package/cjs/constants.js +5 -0
  4. package/cjs/execution-context.js +25 -12
  5. package/cjs/{services → helpers}/logger.js +1 -2
  6. package/cjs/{services/api-service.js → helpers/service-base.js} +8 -8
  7. package/cjs/http/express-adapter.js +164 -0
  8. package/cjs/http/http-adapter.js +27 -0
  9. package/cjs/http/http-context.js +116 -0
  10. package/cjs/http/impl/asset-cache.js +21 -0
  11. package/cjs/http/impl/http-handler.js +575 -0
  12. package/cjs/http/{http-server-request.js → impl/http-incoming.host.js} +21 -46
  13. package/cjs/http/{http-server-response.js → impl/http-outgoing.host.js} +7 -26
  14. package/cjs/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +24 -22
  15. package/cjs/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +13 -54
  16. package/cjs/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +11 -14
  17. package/cjs/http/interfaces/http-incoming.interface.js +25 -0
  18. package/cjs/http/interfaces/http-outgoing.interface.js +22 -0
  19. package/cjs/http/interfaces/node-incoming-message.interface.js +63 -0
  20. package/cjs/http/interfaces/node-outgoing-message.interface.js +15 -0
  21. package/cjs/http/utils/body-reader.js +215 -0
  22. package/cjs/http/{helpers → utils}/convert-to-raw-headers.js +1 -2
  23. package/cjs/http/{helpers → utils}/match-known-fields.js +11 -9
  24. package/cjs/http/utils/wrap-exception.js +34 -0
  25. package/cjs/index.js +25 -25
  26. package/cjs/platform-adapter.js +21 -0
  27. package/cjs/type-guards.js +23 -0
  28. package/esm/augmentation/18n.augmentation.js +20 -7
  29. package/esm/augmentation/http-controller.augmentation.js +23 -0
  30. package/esm/constants.js +2 -0
  31. package/esm/execution-context.js +25 -13
  32. package/esm/{services → helpers}/logger.js +1 -2
  33. package/esm/{services/api-service.js → helpers/service-base.js} +6 -6
  34. package/esm/http/express-adapter.js +159 -0
  35. package/esm/http/http-adapter.js +23 -0
  36. package/esm/http/http-context.js +111 -0
  37. package/esm/http/impl/asset-cache.js +17 -0
  38. package/esm/http/impl/http-handler.js +570 -0
  39. package/esm/http/{http-server-request.js → impl/http-incoming.host.js} +17 -43
  40. package/esm/http/{http-server-response.js → impl/http-outgoing.host.js} +6 -26
  41. package/esm/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +22 -20
  42. package/esm/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +11 -52
  43. package/esm/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +9 -12
  44. package/esm/http/interfaces/http-incoming.interface.js +22 -0
  45. package/esm/http/interfaces/http-outgoing.interface.js +19 -0
  46. package/esm/http/interfaces/node-incoming-message.interface.js +60 -0
  47. package/esm/http/interfaces/node-outgoing-message.interface.js +12 -0
  48. package/esm/http/utils/body-reader.js +210 -0
  49. package/esm/http/{helpers → utils}/convert-to-headers.js +1 -1
  50. package/esm/http/{helpers → utils}/convert-to-raw-headers.js +2 -3
  51. package/esm/http/{helpers → utils}/match-known-fields.js +11 -9
  52. package/esm/http/utils/wrap-exception.js +30 -0
  53. package/esm/index.js +25 -26
  54. package/esm/platform-adapter.js +19 -1
  55. package/esm/type-guards.js +16 -0
  56. package/package.json +22 -10
  57. package/types/augmentation/18n.augmentation.d.ts +32 -2
  58. package/types/augmentation/http-controller.augmentation.d.ts +21 -0
  59. package/types/constants.d.ts +2 -0
  60. package/types/execution-context.d.ts +24 -26
  61. package/types/helpers/service-base.d.ts +10 -0
  62. package/types/http/express-adapter.d.ts +13 -0
  63. package/types/http/http-adapter.d.ts +27 -0
  64. package/types/http/http-context.d.ts +44 -0
  65. package/types/http/impl/asset-cache.d.ts +5 -0
  66. package/types/http/impl/http-handler.d.ts +73 -0
  67. package/types/http/impl/http-incoming.host.d.ts +23 -0
  68. package/types/http/impl/http-outgoing.host.d.ts +17 -0
  69. package/types/http/{helpers/multipart-helper.d.ts → impl/multipart-reader.d.ts} +8 -6
  70. package/types/http/impl/{http-incoming-message.host.d.ts → node-incoming-message.host.d.ts} +9 -22
  71. package/types/http/impl/{http-outgoing-message.host.d.ts → node-outgoing-message.host.d.ts} +11 -27
  72. package/types/http/{http-server-request.d.ts → interfaces/http-incoming.interface.d.ts} +28 -17
  73. package/types/http/{http-server-response.d.ts → interfaces/http-outgoing.interface.d.ts} +17 -10
  74. package/types/http/interfaces/node-incoming-message.interface.d.ts +38 -0
  75. package/types/http/interfaces/node-outgoing-message.interface.d.ts +29 -0
  76. package/types/http/utils/body-reader.d.ts +41 -0
  77. package/types/http/utils/wrap-exception.d.ts +2 -0
  78. package/types/index.d.ts +24 -26
  79. package/types/platform-adapter.d.ts +20 -48
  80. package/types/type-guards.d.ts +8 -0
  81. package/cjs/augmentation/collection.augmentation.js +0 -2
  82. package/cjs/augmentation/container.augmentation.js +0 -2
  83. package/cjs/augmentation/resource.augmentation.js +0 -26
  84. package/cjs/augmentation/singleton.augmentation.js +0 -2
  85. package/cjs/augmentation/storage.augmentation.js +0 -2
  86. package/cjs/execution-context.host.js +0 -46
  87. package/cjs/http/adapters/express-adapter.host.js +0 -34
  88. package/cjs/http/adapters/express-adapter.js +0 -14
  89. package/cjs/http/adapters/node-http-adapter.host.js +0 -70
  90. package/cjs/http/adapters/node-http-adapter.js +0 -14
  91. package/cjs/http/helpers/json-body-loader.js +0 -29
  92. package/cjs/http/helpers/query-parsers.js +0 -16
  93. package/cjs/http/http-adapter-host.js +0 -715
  94. package/cjs/interfaces/interceptor.interface.js +0 -2
  95. package/cjs/interfaces/request-handler.interface.js +0 -2
  96. package/cjs/platform-adapter.host.js +0 -154
  97. package/cjs/request-context.js +0 -25
  98. package/cjs/request.host.js +0 -24
  99. package/cjs/request.js +0 -2
  100. package/cjs/response.host.js +0 -22
  101. package/cjs/response.js +0 -2
  102. package/esm/augmentation/collection.augmentation.js +0 -1
  103. package/esm/augmentation/container.augmentation.js +0 -1
  104. package/esm/augmentation/resource.augmentation.js +0 -24
  105. package/esm/augmentation/singleton.augmentation.js +0 -1
  106. package/esm/augmentation/storage.augmentation.js +0 -1
  107. package/esm/execution-context.host.js +0 -42
  108. package/esm/http/adapters/express-adapter.host.js +0 -30
  109. package/esm/http/adapters/express-adapter.js +0 -11
  110. package/esm/http/adapters/node-http-adapter.host.js +0 -65
  111. package/esm/http/adapters/node-http-adapter.js +0 -11
  112. package/esm/http/helpers/json-body-loader.js +0 -24
  113. package/esm/http/helpers/query-parsers.js +0 -12
  114. package/esm/http/http-adapter-host.js +0 -710
  115. package/esm/interfaces/interceptor.interface.js +0 -1
  116. package/esm/interfaces/request-handler.interface.js +0 -1
  117. package/esm/platform-adapter.host.js +0 -149
  118. package/esm/request-context.js +0 -22
  119. package/esm/request.host.js +0 -20
  120. package/esm/request.js +0 -1
  121. package/esm/response.host.js +0 -18
  122. package/esm/response.js +0 -1
  123. package/types/augmentation/collection.augmentation.d.ts +0 -146
  124. package/types/augmentation/container.augmentation.d.ts +0 -14
  125. package/types/augmentation/resource.augmentation.d.ts +0 -38
  126. package/types/augmentation/singleton.augmentation.d.ts +0 -83
  127. package/types/augmentation/storage.augmentation.d.ts +0 -50
  128. package/types/execution-context.host.d.ts +0 -25
  129. package/types/http/adapters/express-adapter.d.ts +0 -15
  130. package/types/http/adapters/express-adapter.host.d.ts +0 -12
  131. package/types/http/adapters/node-http-adapter.d.ts +0 -17
  132. package/types/http/adapters/node-http-adapter.host.d.ts +0 -19
  133. package/types/http/helpers/json-body-loader.d.ts +0 -5
  134. package/types/http/helpers/query-parsers.d.ts +0 -1
  135. package/types/http/http-adapter-host.d.ts +0 -34
  136. package/types/interfaces/interceptor.interface.d.ts +0 -2
  137. package/types/interfaces/request-handler.interface.d.ts +0 -4
  138. package/types/platform-adapter.host.d.ts +0 -43
  139. package/types/request-context.d.ts +0 -13
  140. package/types/request.d.ts +0 -14
  141. package/types/request.host.d.ts +0 -27
  142. package/types/response.d.ts +0 -22
  143. package/types/response.host.d.ts +0 -22
  144. package/types/services/api-service.d.ts +0 -10
  145. /package/cjs/http/{helpers → utils}/common.js +0 -0
  146. /package/cjs/http/{helpers → utils}/concat-readable.js +0 -0
  147. /package/cjs/http/{helpers → utils}/convert-to-headers.js +0 -0
  148. /package/esm/http/{helpers → utils}/common.js +0 -0
  149. /package/esm/http/{helpers → utils}/concat-readable.js +0 -0
  150. /package/types/{services → helpers}/logger.d.ts +0 -0
  151. /package/types/http/{helpers → utils}/common.d.ts +0 -0
  152. /package/types/http/{helpers → utils}/concat-readable.d.ts +0 -0
  153. /package/types/http/{helpers → utils}/convert-to-headers.d.ts +0 -0
  154. /package/types/http/{helpers → utils}/convert-to-raw-headers.d.ts +0 -0
  155. /package/types/http/{helpers → utils}/match-known-fields.d.ts +0 -0
@@ -1,710 +0,0 @@
1
- import fs from 'fs/promises';
2
- import os from 'os';
3
- import { ValidationError } from 'valgen';
4
- import { BadRequestError, Collection, Container, HttpHeaderCodes, HttpStatusCode, InternalServerError, isReadable, IssueSeverity, MethodNotAllowedError, OperationResult, OpraException, OpraSchema, OpraURL, ResourceNotAvailableError, Singleton, Storage, translate, uid, wrapException } from '@opra/common';
5
- import { ExecutionContextHost } from '../execution-context.host.js';
6
- import { PlatformAdapterHost } from '../platform-adapter.host.js';
7
- import { RequestHost } from '../request.host.js';
8
- import { RequestContext } from '../request-context.js';
9
- import { ResponseHost } from '../response.host.js';
10
- import { jsonBodyLoader } from './helpers/json-body-loader.js';
11
- import { MultipartIterator } from './helpers/multipart-helper.js';
12
- /**
13
- *
14
- * @class HttpAdapterHost
15
- */
16
- export class HttpAdapterHost extends PlatformAdapterHost {
17
- constructor() {
18
- super(...arguments);
19
- this._protocol = 'http';
20
- this._tempDir = os.tmpdir();
21
- }
22
- /**
23
- * Main http request handler
24
- * @param incoming
25
- * @param outgoing
26
- * @protected
27
- */
28
- async handleHttp(incoming, outgoing) {
29
- const context = new ExecutionContextHost(this.api, this.platform, { http: { incoming, outgoing } });
30
- try {
31
- /* istanbul ignore next */
32
- if (!this._api)
33
- throw new InternalServerError(`${Object.getPrototypeOf(this).constructor.name} has not been initialized yet`);
34
- outgoing.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
35
- // Expose headers if cors enabled
36
- if (outgoing.getHeader(HttpHeaderCodes.Access_Control_Allow_Origin)) {
37
- // Expose X-Opra-* headers
38
- outgoing.appendHeader(HttpHeaderCodes.Access_Control_Expose_Headers, Object.values(HttpHeaderCodes)
39
- .filter(k => k.toLowerCase().startsWith('x-opra-')));
40
- }
41
- const { parsedUrl } = incoming;
42
- if (!parsedUrl.path.length) {
43
- if (incoming.method === 'GET') {
44
- outgoing.setHeader('content-type', 'application/json');
45
- outgoing.end(JSON.stringify(this.api.exportSchema({ webSafe: true })));
46
- return;
47
- }
48
- // Process Batch
49
- if (incoming.method === 'POST' && incoming.headers['content-type'] === 'multipart/mixed') {
50
- // todo Process Batch
51
- }
52
- throw new BadRequestError();
53
- }
54
- let i = 0;
55
- let requestProcessed = false;
56
- const next = async () => {
57
- const interceptor = this._interceptors[i++];
58
- if (interceptor) {
59
- await interceptor(context, next);
60
- await next();
61
- }
62
- else if (!requestProcessed) {
63
- requestProcessed = true;
64
- await this.handleExecution(context);
65
- }
66
- };
67
- await next();
68
- }
69
- catch (error) {
70
- if (!outgoing.writableEnded)
71
- await this.sendErrorResponse(context, [error]);
72
- }
73
- finally {
74
- await context.emitAsync('finish');
75
- }
76
- }
77
- async handleExecution(executionContext) {
78
- // Parse incoming message and create Request object
79
- let request;
80
- try {
81
- request = await this.parseRequest(executionContext);
82
- }
83
- catch (e) {
84
- if (e instanceof OpraException)
85
- throw e;
86
- const errors = e.issues.map(issue => new BadRequestError({
87
- message: issue.message,
88
- code: 'REQUEST_VALIDATION',
89
- details: {
90
- ...issue,
91
- message: undefined
92
- }
93
- }));
94
- return this.sendErrorResponse(executionContext, errors);
95
- }
96
- try {
97
- const { outgoing } = executionContext.switchToHttp();
98
- const response = new ResponseHost({ http: outgoing });
99
- const context = RequestContext.from(executionContext, this.api, request, response);
100
- await this.executeRequest(context);
101
- await this.sendResponse(context);
102
- }
103
- catch (e) {
104
- if (e instanceof OpraException)
105
- throw e;
106
- if (e instanceof ValidationError) {
107
- throw new InternalServerError({
108
- message: translate('error:RESPONSE_VALIDATION,'),
109
- code: 'RESPONSE_VALIDATION',
110
- details: e.issues
111
- }, e);
112
- }
113
- throw new InternalServerError(e);
114
- }
115
- }
116
- async parseRequest(executionContext) {
117
- const { incoming } = executionContext.switchToHttp();
118
- const parsedUrl = new OpraURL(incoming.url);
119
- let i = 0;
120
- let p;
121
- let resource = this.api.root;
122
- let request;
123
- // Walk through container
124
- while (resource instanceof Container && i < parsedUrl.path.length) {
125
- p = parsedUrl.path[i];
126
- const r = resource.resources.get(p.resource);
127
- if (r) {
128
- resource = r;
129
- if (resource instanceof Container) {
130
- i++;
131
- }
132
- else
133
- break;
134
- }
135
- else
136
- break;
137
- }
138
- const urlPath = i > 0 ? parsedUrl.path.slice(i) : parsedUrl.path;
139
- const searchParams = parsedUrl.searchParams;
140
- // If there is one more element in the path it may be an action
141
- if (resource instanceof Container) {
142
- if (urlPath.length === 1 && resource.actions.has(urlPath[0].resource)) {
143
- request = await this._parseRequestAction(executionContext, resource, urlPath, searchParams);
144
- if (request)
145
- return request;
146
- }
147
- }
148
- else if (urlPath.length === 2 && resource.actions.has(urlPath[1].resource)) {
149
- request = await this._parseRequestAction(executionContext, resource, urlPath.slice(1), searchParams);
150
- if (request)
151
- return request;
152
- }
153
- if (resource instanceof Storage)
154
- request = await this._parseRequestStorage(executionContext, resource, urlPath.slice(1), searchParams);
155
- else if (urlPath.length === 1) { // Collection and Singleton resources should be last element in path
156
- if (resource instanceof Collection)
157
- request = await this._parseRequestCollection(executionContext, resource, urlPath, searchParams);
158
- else if (resource instanceof Singleton)
159
- request = await this._parseRequestSingleton(executionContext, resource, urlPath, searchParams);
160
- }
161
- if (request)
162
- return request;
163
- const path = parsedUrl.path.toString();
164
- throw new BadRequestError({
165
- message: 'No resource or endpoint found at ' + path,
166
- details: { path }
167
- });
168
- }
169
- async _parseRequestAction(executionContext, resource, urlPath, searchParams) {
170
- const p = urlPath[0];
171
- const { controller, endpoint, handler } = await this.getActionHandler(resource, p.resource);
172
- const { incoming } = executionContext.switchToHttp();
173
- const contentId = incoming.headers['content-id'];
174
- const params = this.parseParameters(endpoint.parameters, searchParams);
175
- return new RequestHost({
176
- endpoint,
177
- controller,
178
- handler,
179
- http: incoming,
180
- contentId,
181
- params
182
- });
183
- }
184
- async _parseRequestCollection(executionContext, resource, urlPath, searchParams) {
185
- const { incoming } = executionContext.switchToHttp();
186
- if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
187
- throw new BadRequestError({ message: 'Unsupported Content-Type' });
188
- const contentId = incoming.headers['content-id'];
189
- const p = urlPath[0];
190
- switch (incoming.method) {
191
- case 'POST': {
192
- if (p.key == null) {
193
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
194
- const jsonReader = jsonBodyLoader({
195
- limit: endpoint.options.inputMaxContentSize
196
- }, endpoint);
197
- let data = await jsonReader(incoming);
198
- data = endpoint.decodeInput(data, { coerce: true });
199
- const params = this.parseParameters(endpoint.parameters, searchParams);
200
- return new RequestHost({
201
- endpoint,
202
- controller,
203
- handler,
204
- http: incoming,
205
- contentId,
206
- data,
207
- params: {
208
- ...params,
209
- pick: params.pick && resource.normalizeFieldNames(params.pick),
210
- omit: params.omit && resource.normalizeFieldNames(params.omit),
211
- include: params.include && resource.normalizeFieldNames(params.include)
212
- }
213
- });
214
- }
215
- break;
216
- }
217
- case 'DELETE': {
218
- if (p.key != null) {
219
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
220
- const params = this.parseParameters(endpoint.parameters, searchParams);
221
- return new RequestHost({
222
- endpoint,
223
- controller,
224
- handler,
225
- http: incoming,
226
- contentId,
227
- key: resource.parseKeyValue(p.key),
228
- params
229
- });
230
- }
231
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'deleteMany');
232
- const params = this.parseParameters(endpoint.parameters, searchParams);
233
- return new RequestHost({
234
- endpoint,
235
- controller,
236
- handler,
237
- http: incoming,
238
- contentId,
239
- params: {
240
- ...params,
241
- filter: params.filter && resource.normalizeFilter(params.filter)
242
- }
243
- });
244
- }
245
- case 'GET': {
246
- if (p.key != null) {
247
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
248
- const params = this.parseParameters(endpoint.parameters, searchParams);
249
- return new RequestHost({
250
- endpoint,
251
- controller,
252
- handler,
253
- http: incoming,
254
- contentId,
255
- key: resource.parseKeyValue(p.key),
256
- params: {
257
- ...params,
258
- pick: params.pick && resource.normalizeFieldNames(params.pick),
259
- omit: params.omit && resource.normalizeFieldNames(params.omit),
260
- include: params.include && resource.normalizeFieldNames(params.include)
261
- }
262
- });
263
- }
264
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'findMany');
265
- const params = this.parseParameters(endpoint.parameters, searchParams);
266
- return new RequestHost({
267
- endpoint,
268
- controller,
269
- handler,
270
- http: incoming,
271
- contentId,
272
- params: {
273
- ...params,
274
- pick: params.pick && resource.normalizeFieldNames(params.pick),
275
- omit: params.omit && resource.normalizeFieldNames(params.omit),
276
- include: params.include && resource.normalizeFieldNames(params.include),
277
- sort: params.sort && resource.normalizeSortFields(params.sort),
278
- filter: params.filter && resource.normalizeFilter(params.filter)
279
- }
280
- });
281
- }
282
- case 'PATCH': {
283
- if (p.key != null) {
284
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
285
- const jsonReader = jsonBodyLoader({
286
- limit: endpoint.options.inputMaxContentSize
287
- }, endpoint);
288
- let data = await jsonReader(incoming);
289
- data = endpoint.decodeInput(data, { coerce: true, partial: true });
290
- const params = this.parseParameters(endpoint.parameters, searchParams);
291
- return new RequestHost({
292
- endpoint,
293
- controller,
294
- handler,
295
- http: incoming,
296
- contentId,
297
- key: resource.parseKeyValue(p.key),
298
- data,
299
- params: {
300
- ...params,
301
- pick: params.pick && resource.normalizeFieldNames(params.pick),
302
- omit: params.omit && resource.normalizeFieldNames(params.omit),
303
- include: params.include && resource.normalizeFieldNames(params.include),
304
- }
305
- });
306
- }
307
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'updateMany');
308
- const jsonReader = jsonBodyLoader({
309
- limit: endpoint.options.inputMaxContentSize
310
- }, endpoint);
311
- let data = await jsonReader(incoming);
312
- data = endpoint.decodeInput(data, { coerce: true, partial: true });
313
- const params = this.parseParameters(endpoint.parameters, searchParams);
314
- return new RequestHost({
315
- endpoint,
316
- controller,
317
- handler,
318
- http: incoming,
319
- contentId,
320
- data,
321
- params: {
322
- ...params,
323
- filter: params.filter && resource.normalizeFilter(params.filter)
324
- }
325
- });
326
- }
327
- }
328
- throw new MethodNotAllowedError({
329
- message: `Collection resource doesn't accept http "${incoming.method}" method`
330
- });
331
- }
332
- async _parseRequestSingleton(executionContext, resource, urlPath, searchParams) {
333
- const { incoming } = executionContext.switchToHttp();
334
- if ((incoming.method === 'POST' || incoming.method === 'PATCH') && !incoming.is('json'))
335
- throw new BadRequestError({ message: 'Unsupported Content-Type' });
336
- const contentId = incoming.headers['content-id'];
337
- switch (incoming.method) {
338
- case 'POST': {
339
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'create');
340
- const jsonReader = jsonBodyLoader({
341
- limit: endpoint.options.inputMaxContentSize
342
- }, endpoint);
343
- let data = await jsonReader(incoming);
344
- data = endpoint.decodeInput(data, { coerce: true });
345
- const params = this.parseParameters(endpoint.parameters, searchParams);
346
- return new RequestHost({
347
- endpoint,
348
- controller,
349
- handler,
350
- http: incoming,
351
- contentId,
352
- data,
353
- params: {
354
- ...params,
355
- pick: params.pick && resource.normalizeFieldNames(params.pick),
356
- omit: params.omit && resource.normalizeFieldNames(params.omit),
357
- include: params.include && resource.normalizeFieldNames(params.include)
358
- }
359
- });
360
- }
361
- case 'DELETE': {
362
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
363
- const params = this.parseParameters(endpoint.parameters, searchParams);
364
- return new RequestHost({
365
- endpoint,
366
- controller,
367
- handler,
368
- http: incoming,
369
- contentId,
370
- params
371
- });
372
- }
373
- case 'GET': {
374
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
375
- const params = this.parseParameters(endpoint.parameters, searchParams);
376
- return new RequestHost({
377
- endpoint,
378
- controller,
379
- handler,
380
- http: incoming,
381
- contentId,
382
- params: {
383
- ...params,
384
- pick: params.pick && resource.normalizeFieldNames(params.pick),
385
- omit: params.omit && resource.normalizeFieldNames(params.omit),
386
- include: params.include && resource.normalizeFieldNames(params.include)
387
- }
388
- });
389
- }
390
- case 'PATCH': {
391
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'update');
392
- const jsonReader = jsonBodyLoader({
393
- limit: endpoint.options.inputMaxContentSize
394
- }, endpoint);
395
- let data = await jsonReader(incoming);
396
- data = endpoint.decodeInput(data, { coerce: true, partial: true });
397
- const params = this.parseParameters(endpoint.parameters, searchParams);
398
- return new RequestHost({
399
- endpoint,
400
- controller,
401
- handler,
402
- http: incoming,
403
- contentId,
404
- data,
405
- params: {
406
- ...params,
407
- pick: params.pick && resource.normalizeFieldNames(params.pick),
408
- omit: params.omit && resource.normalizeFieldNames(params.omit),
409
- include: params.include && resource.normalizeFieldNames(params.include),
410
- }
411
- });
412
- }
413
- }
414
- throw new MethodNotAllowedError({
415
- message: `Singleton resource doesn't accept http "${incoming.method}" method`
416
- });
417
- }
418
- async _parseRequestStorage(executionContext, resource, urlPath, searchParams) {
419
- const { incoming } = executionContext.switchToHttp();
420
- const contentId = incoming.headers['content-id'];
421
- switch (incoming.method) {
422
- case 'GET': {
423
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'get');
424
- const params = this.parseParameters(endpoint.parameters, searchParams);
425
- return new RequestHost({
426
- endpoint,
427
- controller,
428
- handler,
429
- http: incoming,
430
- contentId,
431
- path: urlPath.toString().substring(1),
432
- params
433
- });
434
- }
435
- case 'DELETE': {
436
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'delete');
437
- const params = this.parseParameters(endpoint.parameters, searchParams);
438
- return new RequestHost({
439
- endpoint,
440
- controller,
441
- handler,
442
- http: incoming,
443
- contentId,
444
- path: urlPath.toString().substring(1),
445
- params
446
- });
447
- }
448
- case 'POST': {
449
- const { controller, endpoint, handler } = await this.getOperationHandler(resource, 'post');
450
- const params = this.parseParameters(endpoint.parameters, searchParams);
451
- await fs.mkdir(this._tempDir, { recursive: true });
452
- const multipartIterator = await MultipartIterator.create(incoming, {
453
- ...endpoint.options,
454
- filename: () => this.serviceName + '_p' + process.pid +
455
- 't' + String(Date.now()).substring(8) + 'r' + uid(12)
456
- });
457
- multipartIterator.pause();
458
- // Add an hook to clean up files after request finished
459
- executionContext.on('finish', async () => {
460
- multipartIterator.cancel();
461
- await multipartIterator.deleteFiles().catch(() => void 0);
462
- });
463
- return new RequestHost({
464
- endpoint,
465
- controller,
466
- handler,
467
- http: incoming,
468
- contentId,
469
- parts: multipartIterator,
470
- path: urlPath.toString().substring(1),
471
- params
472
- });
473
- }
474
- }
475
- throw new MethodNotAllowedError({
476
- message: `Storage resource doesn't accept http "${incoming.method}" method`
477
- });
478
- }
479
- parseParameters(paramDefs, searchParams) {
480
- const out = {};
481
- const onFail = (issue) => {
482
- issue.message = `Parameter parse error. ` + issue.message;
483
- issue.location = '@parameters';
484
- return issue;
485
- };
486
- // Parse known parameters
487
- for (const [k, prm] of paramDefs.entries()) {
488
- const decode = prm.getDecoder();
489
- let v = searchParams?.getAll(k);
490
- try {
491
- if (!v.length && prm.default != null)
492
- v = [prm.default];
493
- if (!prm.isArray) {
494
- v = v[0];
495
- v = decode(v, { coerce: true, label: k, onFail });
496
- }
497
- else {
498
- v = v.map(x => decode(x, { coerce: true, label: k })).flat();
499
- if (!v.length)
500
- v = undefined;
501
- }
502
- if (v !== undefined)
503
- out[k] = v;
504
- }
505
- catch (e) {
506
- e.message = `Error parsing parameter ${k}. ` + e.message;
507
- throw e;
508
- }
509
- }
510
- // Add unknown parameters
511
- if (searchParams) {
512
- for (const k of searchParams.keys()) {
513
- let v = searchParams.getAll(k);
514
- if (v.length < 2)
515
- v = v[0];
516
- if (!paramDefs.has(k))
517
- out[k] = v;
518
- }
519
- }
520
- return out;
521
- }
522
- async executeRequest(context) {
523
- const { request } = context;
524
- const { response } = context;
525
- const { endpoint, resource, handler } = request;
526
- // Call endpoint handler method
527
- let value;
528
- try {
529
- value = await handler.call(request.controller, context);
530
- if (response.value == null)
531
- response.value = value;
532
- // Normalize response value
533
- if (endpoint.kind === 'operation') {
534
- if (resource instanceof Collection || resource instanceof Singleton || resource instanceof Storage) {
535
- const operationName = endpoint.name;
536
- if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
537
- let affected = 0;
538
- if (typeof value === 'number')
539
- affected = value;
540
- else if (typeof value === 'boolean')
541
- affected = value ? 1 : 0;
542
- else if (typeof value === 'object')
543
- affected = value.affected || value.affectedRows ||
544
- (operationName === 'updateMany' ? value.updated : value.deleted);
545
- response.value = affected;
546
- return;
547
- }
548
- if (resource instanceof Collection || resource instanceof Singleton) {
549
- // "get" and "update" endpoints must return the entity instance, otherwise it means resource not found
550
- if (value == null && (operationName === 'get' || operationName === 'update'))
551
- throw new ResourceNotAvailableError(resource.name, request.key);
552
- // "findMany" endpoint should return array of entity instances
553
- if (operationName === 'findMany')
554
- value = (value == null ? [] : Array.isArray(value) ? value : [value]);
555
- else
556
- value = value == null ? {} : Array.isArray(value) ? value[0] : value;
557
- }
558
- value = endpoint.encodeReturning(value, { coerce: true, partial: true });
559
- response.value = value;
560
- return;
561
- }
562
- }
563
- if (response.value)
564
- response.value = endpoint.encodeReturning(response.value, { coerce: true, partial: true });
565
- }
566
- catch (error) {
567
- response.errors.push(error);
568
- }
569
- }
570
- async sendResponse(context) {
571
- const { request, response } = context;
572
- const { endpoint, resource } = request;
573
- const outgoing = response.switchToHttp();
574
- if (response.errors?.length || (outgoing.statusCode >= 400 && outgoing.statusCode <= 599))
575
- return this.sendErrorResponse(context, response.errors || []);
576
- // if response redirected we do not send any response
577
- if (outgoing.statusCode >= 300 && outgoing.statusCode < 400) {
578
- outgoing.end();
579
- return;
580
- }
581
- let contentType = String(outgoing.getHeader('content-type') || '');
582
- let returnType = endpoint.returnType;
583
- if (endpoint.kind === 'action' && !contentType && endpoint.returnMime && response.value) {
584
- contentType = endpoint.returnMime;
585
- outgoing.setHeader('Content-Type', contentType);
586
- }
587
- // OperationResult response
588
- if ((returnType || endpoint.kind === 'operation') &&
589
- !(resource instanceof Storage && endpoint.name === 'get')) {
590
- const incoming = context.switchToHttp().incoming;
591
- const apiUrl = new OpraURL(incoming.baseUrl, incoming.protocol + '://' + incoming.get('host')).toString();
592
- const body = new OperationResult({
593
- context: '',
594
- contextUrl: ''
595
- });
596
- const operationName = endpoint.kind === 'operation' ? endpoint.name : '';
597
- if (operationName === 'delete' || operationName === 'deleteMany' || operationName === 'updateMany') {
598
- body.affected = response.value;
599
- returnType = undefined;
600
- }
601
- else {
602
- outgoing.statusCode = outgoing.statusCode || HttpStatusCode.OK;
603
- if (operationName === 'create')
604
- outgoing.statusCode = 201;
605
- if (operationName === 'update' || operationName === 'create') {
606
- body.affected = response.value ? 1 : 0;
607
- }
608
- if (operationName === 'findMany') {
609
- body.count = response.value.length;
610
- body.totalMatches = response.totalMatches;
611
- }
612
- }
613
- if (returnType) {
614
- if (response.value == null)
615
- throw new InternalServerError(`"${request.endpoint.name}" endpoint should return value`);
616
- if (!returnType.isEmbedded) {
617
- const ns = this.api.getDataTypeNs(returnType);
618
- // const isOpraSpec = returnType.document.url?.startsWith('https://oprajs.com/spec/v1.0')
619
- body.type = (ns ? ns + ':' : '') + returnType.name;
620
- body.typeUrl =
621
- (ns
622
- ? new OpraURL('/#/types/' + returnType.name, returnType.document.url || 'http://tempuri.org').toString()
623
- : apiUrl + '/#/types/' + returnType.name);
624
- }
625
- else
626
- body.typeUrl = body.contextUrl + '/type';
627
- if (response.value instanceof OperationResult) {
628
- Object.assign(body, response.value);
629
- }
630
- else
631
- body.payload = response.value;
632
- body.payload = this.i18n.deep(body.payload);
633
- }
634
- body.context = endpoint.getFullPath(false);
635
- body.contextUrl = apiUrl + '/#' + endpoint.getFullPath(true);
636
- outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
637
- outgoing.send(JSON.stringify(body));
638
- outgoing.end();
639
- return;
640
- }
641
- outgoing.statusCode = outgoing.statusCode || HttpStatusCode.OK;
642
- if (response.value != null) {
643
- if (typeof response.value === 'string') {
644
- if (!contentType)
645
- outgoing.setHeader('content-type', 'text/plain');
646
- outgoing.send(response.value);
647
- }
648
- else if (Buffer.isBuffer(response.value) || isReadable(response.value)) {
649
- if (!contentType)
650
- outgoing.setHeader('content-type', 'application/octet-stream');
651
- outgoing.send(response.value);
652
- }
653
- else {
654
- outgoing.setHeader('content-type', 'application/json; charset=utf-8');
655
- outgoing.send(JSON.stringify(response.value));
656
- }
657
- }
658
- outgoing.end();
659
- }
660
- async sendErrorResponse(context, errors) {
661
- const { outgoing } = context.switchToHttp();
662
- if (outgoing.headersSent) {
663
- outgoing.end();
664
- return;
665
- }
666
- if (!errors.length)
667
- errors.push(wrapException({ status: outgoing.statusCode || 500 }));
668
- errors.forEach(x => {
669
- if (x instanceof OpraException) {
670
- switch (x.severity) {
671
- case "fatal":
672
- this._logger.fatal(x);
673
- break;
674
- case "warning":
675
- this._logger.warn(x);
676
- break;
677
- default:
678
- this._logger.error(x);
679
- }
680
- }
681
- else
682
- this._logger.fatal(x);
683
- });
684
- const wrappedErrors = errors.map(wrapException);
685
- // Sort errors from fatal to info
686
- wrappedErrors.sort((a, b) => {
687
- const i = IssueSeverity.Keys.indexOf(a.severity) - IssueSeverity.Keys.indexOf(b.severity);
688
- if (i === 0)
689
- return b.status - a.status;
690
- return i;
691
- });
692
- let status = outgoing.statusCode || 0;
693
- if (!status || status < Number(HttpStatusCode.BAD_REQUEST)) {
694
- status = wrappedErrors[0].status;
695
- if (status < Number(HttpStatusCode.BAD_REQUEST))
696
- status = HttpStatusCode.INTERNAL_SERVER_ERROR;
697
- }
698
- outgoing.statusCode = status;
699
- const body = new OperationResult({
700
- errors: wrappedErrors.map(x => this._i18n.deep(x.toJSON()))
701
- });
702
- outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/opra+json; charset=utf-8');
703
- outgoing.setHeader(HttpHeaderCodes.Cache_Control, 'no-cache');
704
- outgoing.setHeader(HttpHeaderCodes.Pragma, 'no-cache');
705
- outgoing.setHeader(HttpHeaderCodes.Expires, '-1');
706
- outgoing.setHeader(HttpHeaderCodes.X_Opra_Version, OpraSchema.SpecVersion);
707
- outgoing.send(JSON.stringify(body));
708
- outgoing.end();
709
- }
710
- }