@opra/core 0.22.0 → 0.23.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 (118) hide show
  1. package/cjs/adapter/execution-context.host.js +48 -0
  2. package/cjs/adapter/http/express-adapter.host.js +24 -0
  3. package/cjs/adapter/http/express-adapter.js +12 -45
  4. package/cjs/adapter/http/helpers/concat-readable.js +20 -0
  5. package/cjs/adapter/http/helpers/multipart-helper.js +96 -0
  6. package/cjs/adapter/http/helpers/query-parsers.js +16 -0
  7. package/cjs/adapter/http/http-adapter-base.js +127 -0
  8. package/cjs/adapter/http/http-adapter.host.js +57 -0
  9. package/cjs/adapter/http/http-adapter.js +11 -133
  10. package/cjs/adapter/http/{impl/http-server-request.js → http-server-request.js} +11 -5
  11. package/cjs/adapter/http/{impl/http-server-response.js → http-server-response.js} +4 -4
  12. package/cjs/adapter/http/impl/http-incoming-message.host.js +148 -0
  13. package/cjs/adapter/http/impl/{http-outgoing-message-host.js → http-outgoing-message.host.js} +26 -38
  14. package/cjs/adapter/http/request-handlers/entity-request-handler.js +405 -0
  15. package/cjs/adapter/http/request-handlers/request-handler-base.js +27 -0
  16. package/cjs/adapter/http/request-handlers/storage-request-handler.js +134 -0
  17. package/cjs/adapter/operation-context.js +16 -0
  18. package/cjs/adapter/platform-adapter.host.js +107 -0
  19. package/cjs/adapter/request.host.js +1 -2
  20. package/cjs/adapter/request.js +2 -0
  21. package/cjs/adapter/response.js +2 -0
  22. package/cjs/adapter/services/logger.js +36 -0
  23. package/cjs/augmentation/collection.augmentation.js +2 -0
  24. package/cjs/augmentation/singleton.augmentation.js +2 -0
  25. package/cjs/augmentation/storage.augmentation.js +2 -0
  26. package/cjs/index.js +15 -9
  27. package/esm/adapter/execution-context.host.js +44 -0
  28. package/esm/adapter/http/express-adapter.host.js +20 -0
  29. package/esm/adapter/http/express-adapter.js +11 -20
  30. package/esm/adapter/http/helpers/concat-readable.js +16 -0
  31. package/esm/adapter/http/helpers/multipart-helper.js +91 -0
  32. package/esm/adapter/http/helpers/query-parsers.js +12 -0
  33. package/esm/adapter/http/http-adapter-base.js +123 -0
  34. package/esm/adapter/http/http-adapter.host.js +52 -0
  35. package/esm/adapter/http/http-adapter.js +11 -132
  36. package/esm/adapter/http/{impl/http-server-request.js → http-server-request.js} +12 -6
  37. package/esm/adapter/http/{impl/http-server-response.js → http-server-response.js} +4 -4
  38. package/esm/adapter/http/impl/http-incoming-message.host.js +144 -0
  39. package/esm/adapter/http/impl/{http-outgoing-message-host.js → http-outgoing-message.host.js} +25 -36
  40. package/esm/adapter/http/request-handlers/entity-request-handler.js +400 -0
  41. package/esm/adapter/http/request-handlers/request-handler-base.js +23 -0
  42. package/esm/adapter/http/request-handlers/storage-request-handler.js +129 -0
  43. package/esm/adapter/operation-context.js +13 -0
  44. package/esm/adapter/platform-adapter.host.js +102 -0
  45. package/esm/adapter/request.host.js +1 -2
  46. package/esm/adapter/request.js +1 -0
  47. package/esm/adapter/response.js +1 -0
  48. package/esm/adapter/services/logger.js +32 -0
  49. package/esm/augmentation/collection.augmentation.js +1 -0
  50. package/esm/augmentation/singleton.augmentation.js +1 -0
  51. package/esm/augmentation/storage.augmentation.js +1 -0
  52. package/esm/index.js +15 -9
  53. package/i18n/en/error.json +7 -2
  54. package/package.json +7 -6
  55. package/types/adapter/execution-context.d.ts +31 -0
  56. package/types/adapter/execution-context.host.d.ts +27 -0
  57. package/types/adapter/http/express-adapter.d.ts +12 -8
  58. package/types/adapter/http/express-adapter.host.d.ts +11 -0
  59. package/types/adapter/http/helpers/concat-readable.d.ts +3 -0
  60. package/types/adapter/http/helpers/multipart-helper.d.ts +25 -0
  61. package/types/adapter/http/helpers/query-parsers.d.ts +1 -0
  62. package/types/adapter/http/http-adapter-base.d.ts +23 -0
  63. package/types/adapter/http/http-adapter.d.ts +13 -29
  64. package/types/adapter/http/http-adapter.host.d.ts +18 -0
  65. package/types/adapter/http/{impl/http-server-request.d.ts → http-server-request.d.ts} +7 -6
  66. package/types/adapter/http/{impl/http-server-response.d.ts → http-server-response.d.ts} +2 -2
  67. package/types/adapter/http/impl/{http-incoming-message-host.d.ts → http-incoming-message.host.d.ts} +16 -12
  68. package/types/adapter/http/impl/{http-outgoing-message-host.d.ts → http-outgoing-message.host.d.ts} +12 -16
  69. package/types/adapter/http/request-handlers/entity-request-handler.d.ts +24 -0
  70. package/types/adapter/http/request-handlers/request-handler-base.d.ts +15 -0
  71. package/types/adapter/http/request-handlers/storage-request-handler.d.ts +23 -0
  72. package/types/adapter/interfaces/logger.interface.d.ts +7 -6
  73. package/types/adapter/interfaces/request-handler.interface.d.ts +4 -0
  74. package/types/adapter/operation-context.d.ts +11 -0
  75. package/types/adapter/{adapter.d.ts → platform-adapter.d.ts} +18 -28
  76. package/types/adapter/platform-adapter.host.d.ts +31 -0
  77. package/types/adapter/request.d.ts +11 -0
  78. package/types/adapter/request.host.d.ts +12 -21
  79. package/types/adapter/{interfaces/response.interface.d.ts → response.d.ts} +2 -2
  80. package/types/adapter/response.host.d.ts +2 -2
  81. package/types/adapter/services/logger.d.ts +14 -0
  82. package/types/augmentation/collection.augmentation.d.ts +112 -0
  83. package/types/augmentation/singleton.augmentation.d.ts +64 -0
  84. package/types/augmentation/storage.augmentation.d.ts +39 -0
  85. package/types/index.d.ts +15 -9
  86. package/cjs/adapter/adapter.js +0 -118
  87. package/cjs/adapter/http/impl/http-incoming-message-host.js +0 -127
  88. package/cjs/adapter/http/request-parsers/parse-collection-request.js +0 -165
  89. package/cjs/adapter/http/request-parsers/parse-request.js +0 -24
  90. package/cjs/adapter/http/request-parsers/parse-singleton-request.js +0 -96
  91. package/cjs/adapter/internal/metadata.resource.js +0 -26
  92. package/cjs/adapter/request-context.host.js +0 -44
  93. package/cjs/shared/collection-resource-base.js +0 -20
  94. package/esm/adapter/adapter.js +0 -113
  95. package/esm/adapter/http/impl/http-incoming-message-host.js +0 -122
  96. package/esm/adapter/http/request-parsers/parse-collection-request.js +0 -161
  97. package/esm/adapter/http/request-parsers/parse-request.js +0 -20
  98. package/esm/adapter/http/request-parsers/parse-singleton-request.js +0 -92
  99. package/esm/adapter/internal/metadata.resource.js +0 -23
  100. package/esm/adapter/request-context.host.js +0 -40
  101. package/esm/shared/collection-resource-base.js +0 -16
  102. package/types/adapter/http/request-parsers/parse-collection-request.d.ts +0 -4
  103. package/types/adapter/http/request-parsers/parse-request.d.ts +0 -4
  104. package/types/adapter/http/request-parsers/parse-singleton-request.d.ts +0 -4
  105. package/types/adapter/interfaces/request-context.interface.d.ts +0 -31
  106. package/types/adapter/interfaces/request.interface.d.ts +0 -15
  107. package/types/adapter/internal/metadata.resource.d.ts +0 -7
  108. package/types/adapter/request-context.host.d.ts +0 -22
  109. package/types/shared/collection-resource-base.d.ts +0 -11
  110. /package/cjs/adapter/{interfaces/request-context.interface.js → execution-context.js} +0 -0
  111. /package/cjs/adapter/http/{request-parsers/batch-request-parser.js → request-handlers/parse-batch-request.js} +0 -0
  112. /package/cjs/adapter/interfaces/{request.interface.js → request-handler.interface.js} +0 -0
  113. /package/cjs/adapter/{interfaces/response.interface.js → platform-adapter.js} +0 -0
  114. /package/esm/adapter/{interfaces/request-context.interface.js → execution-context.js} +0 -0
  115. /package/esm/adapter/http/{request-parsers/batch-request-parser.js → request-handlers/parse-batch-request.js} +0 -0
  116. /package/esm/adapter/interfaces/{request.interface.js → request-handler.interface.js} +0 -0
  117. /package/esm/adapter/{interfaces/response.interface.js → platform-adapter.js} +0 -0
  118. /package/types/adapter/http/{request-parsers/batch-request-parser.d.ts → request-handlers/parse-batch-request.d.ts} +0 -0
@@ -0,0 +1,400 @@
1
+ import bodyParser from 'body-parser';
2
+ import { toBoolean, toInt } from 'putil-varhelpers';
3
+ import * as valgen from 'valgen';
4
+ import { BadRequestError, Collection, HttpHeaderCodes, HttpStatusCodes, InternalServerError, MethodNotAllowedError, OpraException, ResourceNotFoundError, Singleton, translate, } from '@opra/common';
5
+ import { OperationContext } from '../../operation-context.js';
6
+ import { RequestHost } from '../../request.host.js';
7
+ import { ResponseHost } from '../../response.host.js';
8
+ import { parseArrayParam } from '../helpers/query-parsers.js';
9
+ import { RequestHandlerBase } from './request-handler-base.js';
10
+ /**
11
+ * @class EntityRequestHandler
12
+ */
13
+ export class EntityRequestHandler extends RequestHandlerBase {
14
+ constructor(adapter) {
15
+ super(adapter);
16
+ this.adapter = adapter;
17
+ this.bodyLoaders = new WeakMap();
18
+ }
19
+ async processRequest(executionContext) {
20
+ const { incoming, outgoing } = executionContext.switchToHttp();
21
+ // Parse incoming message and create Request object
22
+ const request = await this.parseRequest(incoming);
23
+ if (!request)
24
+ return;
25
+ const response = new ResponseHost({ http: outgoing });
26
+ const context = OperationContext.from(executionContext, request, response);
27
+ // Execute operation
28
+ await this.executeOperation(context);
29
+ if (response.errors.length) {
30
+ context.errors.push(...response.errors);
31
+ return;
32
+ }
33
+ try {
34
+ await this.sendResponse(context);
35
+ }
36
+ catch (e) {
37
+ if (e instanceof OpraException)
38
+ throw e;
39
+ if (e instanceof valgen.ValidationError) {
40
+ throw new InternalServerError({
41
+ message: translate('error:RESPONSE_VALIDATION,', 'Response validation failed'),
42
+ code: 'RESPONSE_VALIDATION',
43
+ details: e.issues
44
+ }, e);
45
+ }
46
+ throw new InternalServerError(e);
47
+ }
48
+ }
49
+ async parseRequest(incoming) {
50
+ const p = incoming.parsedUrl.path[0];
51
+ const resource = this.adapter.api.getResource(p.resource);
52
+ try {
53
+ if (resource instanceof Collection)
54
+ return await this.parseCollectionRequest(resource, incoming);
55
+ if (resource instanceof Singleton)
56
+ return await this.parseSingletonRequest(resource, incoming);
57
+ }
58
+ catch (e) {
59
+ if (e instanceof OpraException)
60
+ throw e;
61
+ if (e instanceof valgen.ValidationError) {
62
+ throw new BadRequestError({
63
+ message: translate('error:REQUEST_VALIDATION,', 'Request validation failed'),
64
+ code: 'REQUEST_VALIDATION',
65
+ details: e.issues
66
+ }, e);
67
+ }
68
+ throw new BadRequestError(e);
69
+ }
70
+ }
71
+ async executeOperation(context) {
72
+ const request = context.request;
73
+ const { response } = context;
74
+ const resource = request.resource;
75
+ // Call operation handler method
76
+ let value;
77
+ try {
78
+ value = await request.controller[request.operation].call(request.controller, context);
79
+ if (value == null)
80
+ value = response.value;
81
+ const { operation } = request;
82
+ if (operation === 'delete' || operation === 'deleteMany' || operation === 'updateMany') {
83
+ let affected = 0;
84
+ if (typeof value === 'number')
85
+ affected = value;
86
+ if (typeof value === 'boolean')
87
+ affected = value ? 1 : 0;
88
+ if (typeof value === 'object')
89
+ affected = value.affected || value.affectedRows ||
90
+ (operation === 'updateMany' ? value.updated : value.deleted);
91
+ response.value = affected;
92
+ }
93
+ else {
94
+ // "get" and "update" operations must return the entity instance, otherwise it means resource not found
95
+ if (value == null && (request.operation === 'get' || request.operation === 'update'))
96
+ throw new ResourceNotFoundError(resource.name, request.key);
97
+ // "findMany" operation should return array of entity instances
98
+ if (request.operation === 'findMany')
99
+ value = value == null ? [] : Array.isArray(value) ? value : [value];
100
+ else
101
+ value = value == null ? {} : Array.isArray(value) ? value[0] : value;
102
+ response.value = value;
103
+ }
104
+ }
105
+ catch (error) {
106
+ response.errors.push(error);
107
+ }
108
+ }
109
+ async sendResponse(context) {
110
+ const { request, response } = context;
111
+ const resource = request.resource;
112
+ const outgoing = response.switchToHttp();
113
+ let responseObject;
114
+ if (request.operation === 'delete' || request.operation === 'deleteMany' || request.operation === 'updateMany') {
115
+ responseObject = {
116
+ resource: resource.name,
117
+ operation: request.operation,
118
+ affected: response.value
119
+ };
120
+ }
121
+ else {
122
+ if (!response.value)
123
+ throw new InternalServerError(`"${request.operation}" operation should return value`);
124
+ const encode = resource.getEncoder(request.operation);
125
+ const data = encode(response.value, { coerce: true });
126
+ if (request.operation === 'create')
127
+ outgoing.statusCode = 201;
128
+ responseObject = {
129
+ resource: resource.name,
130
+ operation: request.operation,
131
+ data
132
+ };
133
+ if (request.operation === 'create' || request.operation === 'update')
134
+ responseObject.affected = 1;
135
+ if (request.operation === 'findMany' && response.count != null && response.count >= 0)
136
+ responseObject.totalCount = response.count;
137
+ }
138
+ outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
139
+ const body = this.adapter._i18n.deep(responseObject);
140
+ outgoing.setHeader(HttpHeaderCodes.Content_Type, 'application/json; charset=utf-8');
141
+ outgoing.send(JSON.stringify(body));
142
+ outgoing.end();
143
+ }
144
+ async parseCollectionRequest(resource, incoming) {
145
+ if ((incoming.method === 'POST' || incoming.method === 'PATCH') &&
146
+ incoming.headers['content-type'] !== 'application/json')
147
+ throw new BadRequestError({ message: 'Unsupported Content-Type' });
148
+ const contentId = incoming.headers['content-id'];
149
+ const p = incoming.parsedUrl.path[0];
150
+ const params = incoming.parsedUrl.searchParams;
151
+ switch (incoming.method) {
152
+ case 'POST': {
153
+ if (p.key == null) {
154
+ const operationMeta = await this.assertOperation(resource, 'create');
155
+ const jsonReader = this.getBodyLoader(operationMeta);
156
+ const decode = resource.getDecoder('create');
157
+ let data = await jsonReader(incoming);
158
+ data = decode(data, { coerce: true });
159
+ const pick = parseArrayParam(params.get('$pick'));
160
+ const omit = parseArrayParam(params.get('$omit'));
161
+ const include = parseArrayParam(params.get('$include'));
162
+ return new RequestHost({
163
+ controller: operationMeta.controller,
164
+ http: incoming,
165
+ contentId,
166
+ resource,
167
+ operation: 'create',
168
+ data,
169
+ params: {
170
+ pick: pick && resource.normalizeFieldPath(pick),
171
+ omit: omit && resource.normalizeFieldPath(omit),
172
+ include: include && resource.normalizeFieldPath(include)
173
+ }
174
+ });
175
+ }
176
+ break;
177
+ }
178
+ case 'DELETE': {
179
+ if (p.key != null) {
180
+ const operationMeta = await this.assertOperation(resource, 'delete');
181
+ return new RequestHost({
182
+ controller: operationMeta.controller,
183
+ http: incoming,
184
+ contentId,
185
+ resource,
186
+ operation: 'delete',
187
+ key: resource.parseKeyValue(p.key)
188
+ });
189
+ }
190
+ const operationMeta = await this.assertOperation(resource, 'deleteMany');
191
+ const filter = resource.normalizeFilter(params.get('$filter'));
192
+ return new RequestHost({
193
+ controller: operationMeta.controller,
194
+ http: incoming,
195
+ contentId,
196
+ resource,
197
+ operation: 'deleteMany',
198
+ params: {
199
+ filter
200
+ }
201
+ });
202
+ }
203
+ case 'GET': {
204
+ const pick = parseArrayParam(params.get('$pick'));
205
+ const omit = parseArrayParam(params.get('$omit'));
206
+ const include = parseArrayParam(params.get('$include'));
207
+ if (p.key != null) {
208
+ const operationMeta = await this.assertOperation(resource, 'get');
209
+ return new RequestHost({
210
+ controller: operationMeta.controller,
211
+ http: incoming,
212
+ contentId,
213
+ resource,
214
+ operation: 'get',
215
+ key: resource.parseKeyValue(p.key),
216
+ params: {
217
+ pick: pick && resource.normalizeFieldPath(pick),
218
+ omit: omit && resource.normalizeFieldPath(omit),
219
+ include: include && resource.normalizeFieldPath(include)
220
+ }
221
+ });
222
+ }
223
+ const operationMeta = await this.assertOperation(resource, 'findMany');
224
+ const filter = resource.normalizeFilter(params.get('$filter'));
225
+ const sort = parseArrayParam(params.get('$sort'));
226
+ return new RequestHost({
227
+ controller: operationMeta.controller,
228
+ http: incoming,
229
+ contentId,
230
+ resource,
231
+ operation: 'findMany',
232
+ params: {
233
+ pick: pick && resource.normalizeFieldPath(pick),
234
+ omit: omit && resource.normalizeFieldPath(omit),
235
+ include: include && resource.normalizeFieldPath(include),
236
+ sort: sort && resource.normalizeSortFields(sort),
237
+ filter,
238
+ limit: toInt(params.get('$limit')),
239
+ skip: toInt(params.get('$skip')),
240
+ distinct: toBoolean(params.get('$distinct')),
241
+ count: toBoolean(params.get('$count')),
242
+ }
243
+ });
244
+ }
245
+ case 'PATCH': {
246
+ if (p.key != null) {
247
+ const operationMeta = await this.assertOperation(resource, 'update');
248
+ const jsonReader = this.getBodyLoader(operationMeta);
249
+ const decode = resource.getDecoder('update');
250
+ let data = await jsonReader(incoming);
251
+ data = decode(data, { coerce: true });
252
+ const pick = parseArrayParam(params.get('$pick'));
253
+ const omit = parseArrayParam(params.get('$omit'));
254
+ const include = parseArrayParam(params.get('$include'));
255
+ return new RequestHost({
256
+ controller: operationMeta.controller,
257
+ http: incoming,
258
+ contentId,
259
+ resource,
260
+ operation: 'update',
261
+ key: resource.parseKeyValue(p.key),
262
+ data,
263
+ params: {
264
+ pick: pick && resource.normalizeFieldPath(pick),
265
+ omit: omit && resource.normalizeFieldPath(omit),
266
+ include: include && resource.normalizeFieldPath(include),
267
+ }
268
+ });
269
+ }
270
+ const operationMeta = await this.assertOperation(resource, 'updateMany');
271
+ const jsonReader = this.getBodyLoader(operationMeta);
272
+ const decode = resource.getDecoder('updateMany');
273
+ let data = await jsonReader(incoming);
274
+ data = decode(data, { coerce: true });
275
+ const filter = resource.normalizeFilter(params.get('$filter'));
276
+ return new RequestHost({
277
+ controller: operationMeta.controller,
278
+ http: incoming,
279
+ contentId,
280
+ resource,
281
+ operation: 'updateMany',
282
+ data,
283
+ params: {
284
+ filter,
285
+ }
286
+ });
287
+ }
288
+ }
289
+ throw new MethodNotAllowedError({
290
+ message: `Collection resources do not accept http "${incoming.method}" method`
291
+ });
292
+ }
293
+ async parseSingletonRequest(resource, incoming) {
294
+ if ((incoming.method === 'POST' || incoming.method === 'PATCH') &&
295
+ incoming.headers['content-type'] !== 'application/json')
296
+ throw new BadRequestError({ message: 'Unsupported Content-Type' });
297
+ const contentId = incoming.headers['content-id'];
298
+ const params = incoming.parsedUrl.searchParams;
299
+ switch (incoming.method) {
300
+ case 'POST': {
301
+ const operationMeta = await this.assertOperation(resource, 'create');
302
+ const jsonReader = this.getBodyLoader(operationMeta);
303
+ const decode = resource.getDecoder('create');
304
+ let data = await jsonReader(incoming);
305
+ data = decode(data, { coerce: true });
306
+ const pick = parseArrayParam(params.get('$pick'));
307
+ const omit = parseArrayParam(params.get('$omit'));
308
+ const include = parseArrayParam(params.get('$include'));
309
+ return new RequestHost({
310
+ controller: operationMeta.controller,
311
+ http: incoming,
312
+ contentId,
313
+ resource,
314
+ operation: 'create',
315
+ data,
316
+ params: {
317
+ pick: pick && resource.normalizeFieldPath(pick),
318
+ omit: omit && resource.normalizeFieldPath(omit),
319
+ include: include && resource.normalizeFieldPath(include)
320
+ }
321
+ });
322
+ }
323
+ case 'DELETE': {
324
+ const operationMeta = await this.assertOperation(resource, 'delete');
325
+ return new RequestHost({
326
+ controller: operationMeta.controller,
327
+ http: incoming,
328
+ contentId,
329
+ resource,
330
+ operation: 'delete',
331
+ });
332
+ }
333
+ case 'GET': {
334
+ const operationMeta = await this.assertOperation(resource, 'get');
335
+ const pick = parseArrayParam(params.get('$pick'));
336
+ const omit = parseArrayParam(params.get('$omit'));
337
+ const include = parseArrayParam(params.get('$include'));
338
+ return new RequestHost({
339
+ controller: operationMeta.controller,
340
+ http: incoming,
341
+ contentId,
342
+ resource,
343
+ operation: 'get',
344
+ params: {
345
+ pick: pick && resource.normalizeFieldPath(pick),
346
+ omit: omit && resource.normalizeFieldPath(omit),
347
+ include: include && resource.normalizeFieldPath(include)
348
+ }
349
+ });
350
+ }
351
+ case 'PATCH': {
352
+ const operationMeta = await this.assertOperation(resource, 'update');
353
+ const jsonReader = this.getBodyLoader(operationMeta);
354
+ const decode = resource.getDecoder('update');
355
+ let data = await jsonReader(incoming);
356
+ data = decode(data, { coerce: true });
357
+ const pick = parseArrayParam(params.get('$pick'));
358
+ const omit = parseArrayParam(params.get('$omit'));
359
+ const include = parseArrayParam(params.get('$include'));
360
+ return new RequestHost({
361
+ controller: operationMeta.controller,
362
+ http: incoming,
363
+ contentId,
364
+ resource,
365
+ operation: 'update',
366
+ data,
367
+ params: {
368
+ pick: pick && resource.normalizeFieldPath(pick),
369
+ omit: omit && resource.normalizeFieldPath(omit),
370
+ include: include && resource.normalizeFieldPath(include),
371
+ }
372
+ });
373
+ }
374
+ }
375
+ throw new MethodNotAllowedError({
376
+ message: `Singleton resources do not accept http "${incoming.method}" method`
377
+ });
378
+ }
379
+ getBodyLoader(operation) {
380
+ let bodyLoader = this.bodyLoaders.get(operation);
381
+ if (!bodyLoader) {
382
+ const parser = bodyParser.json({
383
+ limit: operation.input?.maxContentSize,
384
+ type: 'json'
385
+ });
386
+ bodyLoader = (incoming) => {
387
+ return new Promise((resolve, reject) => {
388
+ const next = (error) => {
389
+ if (error)
390
+ return reject(error);
391
+ resolve(incoming.body);
392
+ };
393
+ parser(incoming, {}, next);
394
+ });
395
+ };
396
+ this.bodyLoaders.set(operation, bodyLoader);
397
+ }
398
+ return bodyLoader;
399
+ }
400
+ }
@@ -0,0 +1,23 @@
1
+ import { ForbiddenError, translate } from '@opra/common';
2
+ /**
3
+ * @class RequestHandlerBase
4
+ */
5
+ export class RequestHandlerBase {
6
+ constructor(adapter) {
7
+ this.adapter = adapter;
8
+ }
9
+ async assertOperation(resource, operation) {
10
+ const controller = await this.adapter.getController(resource);
11
+ const operationMeta = (typeof controller?.[operation] === 'function') && resource.operations[operation];
12
+ if (operationMeta)
13
+ return {
14
+ ...operationMeta,
15
+ controller
16
+ };
17
+ throw new ForbiddenError({
18
+ message: translate('RESOLVER_FORBIDDEN', { resource: resource.name, operation }, `'{{resource}}' endpoint does not accept '{{operation}}' operations`),
19
+ severity: 'error',
20
+ code: 'RESOLVER_FORBIDDEN'
21
+ });
22
+ }
23
+ }
@@ -0,0 +1,129 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import { BadRequestError, HttpStatusCodes, isReadable, OpraException, Storage, uid } from '@opra/common';
4
+ import { OperationContext } from '../../operation-context.js';
5
+ import { RequestHost } from '../../request.host.js';
6
+ import { ResponseHost } from '../../response.host.js';
7
+ import { MultipartIterator } from '../helpers/multipart-helper.js';
8
+ import { RequestHandlerBase } from './request-handler-base.js';
9
+ /**
10
+ * @class StorageRequestHandler
11
+ */
12
+ export class StorageRequestHandler extends RequestHandlerBase {
13
+ constructor(adapter, options) {
14
+ super(adapter);
15
+ this.adapter = adapter;
16
+ this._uploadDir = options?.uploadDir || os.tmpdir();
17
+ }
18
+ async processRequest(executionContext) {
19
+ const { incoming, outgoing } = executionContext.switchToHttp();
20
+ // Parse incoming message and create Request object
21
+ const request = await this.parseRequest(executionContext, incoming);
22
+ if (!request)
23
+ return;
24
+ const response = new ResponseHost({ http: outgoing });
25
+ const context = OperationContext.from(executionContext, request, response);
26
+ // Execute operation
27
+ await this.executeOperation(context);
28
+ if (response.errors.length) {
29
+ context.errors.push(...response.errors);
30
+ return;
31
+ }
32
+ await this.sendResponse(context);
33
+ }
34
+ async parseRequest(executionContext, incoming) {
35
+ const contentId = incoming.headers['content-id'];
36
+ const p = incoming.parsedUrl.path[0];
37
+ const resource = this.adapter.api.getResource(p.resource);
38
+ try {
39
+ if (!(resource instanceof Storage))
40
+ return;
41
+ switch (incoming.method) {
42
+ case 'GET': {
43
+ const operationMeta = await this.assertOperation(resource, 'get');
44
+ return new RequestHost({
45
+ controller: operationMeta.controller,
46
+ http: incoming,
47
+ resource,
48
+ operation: 'get',
49
+ contentId
50
+ });
51
+ }
52
+ case 'DELETE': {
53
+ const operationMeta = await this.assertOperation(resource, 'delete');
54
+ return new RequestHost({
55
+ controller: operationMeta.controller,
56
+ http: incoming,
57
+ resource,
58
+ operation: 'delete',
59
+ contentId
60
+ });
61
+ }
62
+ case 'POST': {
63
+ const operationMeta = await this.assertOperation(resource, 'post');
64
+ await fs.mkdir(this._uploadDir, { recursive: true });
65
+ const multipartIterator = new MultipartIterator(incoming, {
66
+ ...operationMeta,
67
+ filename: () => this.adapter.serviceName + '_p' + process.pid +
68
+ 't' + String(Date.now()).substring(8) + 'r' + uid(12)
69
+ });
70
+ multipartIterator.pause();
71
+ // Add an hook to clean up files after request finished
72
+ executionContext.on('finish', async () => {
73
+ multipartIterator.cancel();
74
+ await multipartIterator.deleteFiles().catch(() => void 0);
75
+ });
76
+ return new RequestHost({
77
+ controller: operationMeta.controller,
78
+ http: incoming,
79
+ resource,
80
+ operation: 'post',
81
+ contentId,
82
+ parts: multipartIterator
83
+ });
84
+ }
85
+ }
86
+ }
87
+ catch (e) {
88
+ if (e instanceof OpraException)
89
+ throw e;
90
+ throw new BadRequestError(e);
91
+ }
92
+ }
93
+ async executeOperation(context) {
94
+ const request = context.request;
95
+ const { response } = context;
96
+ // Call operation handler method
97
+ let value;
98
+ try {
99
+ value = await request.controller[request.operation].call(request.controller, context);
100
+ if (response.value == null)
101
+ response.value = value;
102
+ }
103
+ catch (error) {
104
+ response.errors.push(error);
105
+ }
106
+ }
107
+ async sendResponse(context) {
108
+ const { response } = context;
109
+ const outgoing = response.switchToHttp();
110
+ outgoing.statusCode = outgoing.statusCode || HttpStatusCodes.OK;
111
+ if (response.value != null) {
112
+ if (typeof response.value === 'string') {
113
+ if (!outgoing.hasHeader('content-type'))
114
+ outgoing.setHeader('content-type', 'text/plain');
115
+ outgoing.send(response.value);
116
+ }
117
+ else if (Buffer.isBuffer(response.value) || isReadable(response.value)) {
118
+ if (!outgoing.hasHeader('content-type'))
119
+ outgoing.setHeader('content-type', 'application/octet-stream');
120
+ outgoing.send(response.value);
121
+ }
122
+ else {
123
+ outgoing.setHeader('content-type', 'application/json; charset=utf-8');
124
+ outgoing.send(JSON.stringify(response.value));
125
+ }
126
+ }
127
+ outgoing.end();
128
+ }
129
+ }
@@ -0,0 +1,13 @@
1
+ export var OperationContext;
2
+ (function (OperationContext) {
3
+ function from(executionContext, request, response) {
4
+ const out = {
5
+ request,
6
+ response,
7
+ requestScope: {}
8
+ };
9
+ Object.setPrototypeOf(out, executionContext);
10
+ return out;
11
+ }
12
+ OperationContext.from = from;
13
+ })(OperationContext || (OperationContext = {}));
@@ -0,0 +1,102 @@
1
+ import path from 'path';
2
+ import { pascalCase } from 'putil-varhelpers';
3
+ import { AsyncEventEmitter } from 'strict-typed-events';
4
+ import { getStackFileName, I18n, Resource } from '@opra/common';
5
+ import { Logger } from './services/logger.js';
6
+ /**
7
+ * @class PlatformAdapterHost
8
+ */
9
+ export class PlatformAdapterHost extends AsyncEventEmitter {
10
+ constructor(api, options) {
11
+ super();
12
+ this.api = api;
13
+ this._controllers = new WeakMap();
14
+ this._initialized = false;
15
+ this._options = options || {};
16
+ this._logger = options?.logger && options.logger instanceof Logger
17
+ ? options.logger
18
+ : new Logger({ instance: options?.logger });
19
+ // Assign events
20
+ if (options?.on) {
21
+ for (const [event, fn] of Object.entries(options.on)) {
22
+ /* istanbul ignore next */
23
+ if (typeof fn === 'function')
24
+ this.on(event, fn);
25
+ }
26
+ }
27
+ // Make a safe service name
28
+ this._serviceName = pascalCase((api.info.title || '').replace(/[^a-z0-9_ ]/ig, '')) || 'OpraService';
29
+ if (!/^[a-z]/i.test(this._serviceName))
30
+ this._serviceName = 'X' + this._serviceName;
31
+ }
32
+ get platform() {
33
+ return this._platform;
34
+ }
35
+ get protocol() {
36
+ return this._protocol;
37
+ }
38
+ get serviceName() {
39
+ return this.api.info.title;
40
+ }
41
+ async close() {
42
+ const promises = [];
43
+ for (const r of this.api.resources.values()) {
44
+ const onShutdown = r.onShutdown;
45
+ if (onShutdown)
46
+ promises.push((async () => onShutdown.call(r.controller, r))());
47
+ }
48
+ await Promise.allSettled(promises);
49
+ }
50
+ /**
51
+ * Initializes the adapter
52
+ */
53
+ async init() {
54
+ if (this._initialized)
55
+ return;
56
+ // Init I18n
57
+ if (this._options?.i18n instanceof I18n)
58
+ this._i18n = this._options.i18n;
59
+ else if (typeof this._options?.i18n === 'function')
60
+ this._i18n = await this._options.i18n();
61
+ else
62
+ this._i18n = await this._createI18n(this._options?.i18n);
63
+ this._i18n = this._i18n || I18n.defaultInstance;
64
+ if (!this._i18n.isInitialized)
65
+ await this._i18n.init();
66
+ // Initialize all controllers
67
+ for (const resource of this.api.resources.values()) {
68
+ await this.getController(resource);
69
+ }
70
+ this._initialized = true;
71
+ }
72
+ async getController(resource) {
73
+ resource = typeof resource === 'object' && resource instanceof Resource
74
+ ? resource : this.api.getResource(resource);
75
+ let controller = this._controllers.get(resource);
76
+ if (!controller) {
77
+ if (resource.controller) {
78
+ controller = typeof resource.controller === 'function' ?
79
+ new resource.controller()
80
+ : resource.controller;
81
+ // Initialize controller
82
+ if (typeof controller.onInit === 'function')
83
+ await controller.onInit.call(controller);
84
+ this._controllers.set(resource, controller);
85
+ }
86
+ }
87
+ return controller;
88
+ }
89
+ async _createI18n(options) {
90
+ const opts = {
91
+ ...options,
92
+ };
93
+ delete opts.resourceDirs;
94
+ const instance = I18n.createInstance(opts);
95
+ await instance.init();
96
+ await instance.loadResourceDir(path.resolve(getStackFileName(), '../../../i18n'));
97
+ if (options?.resourceDirs)
98
+ for (const dir of options.resourceDirs)
99
+ await instance.loadResourceDir(dir);
100
+ return instance;
101
+ }
102
+ }
@@ -1,8 +1,7 @@
1
1
  export class RequestHost {
2
2
  constructor(init) {
3
- this.contentId = '';
4
3
  Object.assign(this, init);
5
- this.resourceKind = this.resource.kind;
4
+ this.params = this.params || {};
6
5
  }
7
6
  switchToHttp() {
8
7
  if (this.http)
@@ -0,0 +1 @@
1
+ export {};