@opra/core 0.33.13 → 1.0.0-alpha.2
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/cjs/augmentation/18n.augmentation.js +17 -4
- package/cjs/augmentation/http-controller.augmentation.js +25 -0
- package/cjs/constants.js +5 -0
- package/cjs/execution-context.js +25 -12
- package/cjs/{services → helpers}/logger.js +1 -2
- package/cjs/{services/api-service.js → helpers/service-base.js} +8 -8
- package/cjs/http/express-adapter.js +164 -0
- package/cjs/http/http-adapter.js +27 -0
- package/cjs/http/http-context.js +116 -0
- package/cjs/http/impl/asset-cache.js +21 -0
- package/cjs/http/impl/http-handler.js +575 -0
- package/cjs/http/{http-server-request.js → impl/http-incoming.host.js} +21 -46
- package/cjs/http/{http-server-response.js → impl/http-outgoing.host.js} +7 -26
- package/cjs/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +24 -22
- package/cjs/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +13 -54
- package/cjs/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +11 -14
- package/cjs/http/interfaces/http-incoming.interface.js +25 -0
- package/cjs/http/interfaces/http-outgoing.interface.js +22 -0
- package/cjs/http/interfaces/node-incoming-message.interface.js +63 -0
- package/cjs/http/interfaces/node-outgoing-message.interface.js +15 -0
- package/cjs/http/utils/body-reader.js +215 -0
- package/cjs/http/{helpers → utils}/convert-to-raw-headers.js +1 -2
- package/cjs/http/{helpers → utils}/match-known-fields.js +11 -9
- package/cjs/http/utils/wrap-exception.js +34 -0
- package/cjs/index.js +25 -25
- package/cjs/platform-adapter.js +21 -0
- package/cjs/type-guards.js +23 -0
- package/esm/augmentation/18n.augmentation.js +20 -7
- package/esm/augmentation/http-controller.augmentation.js +23 -0
- package/esm/constants.js +2 -0
- package/esm/execution-context.js +25 -13
- package/esm/{services → helpers}/logger.js +1 -2
- package/esm/{services/api-service.js → helpers/service-base.js} +6 -6
- package/esm/http/express-adapter.js +159 -0
- package/esm/http/http-adapter.js +23 -0
- package/esm/http/http-context.js +111 -0
- package/esm/http/impl/asset-cache.js +17 -0
- package/esm/http/impl/http-handler.js +570 -0
- package/esm/http/{http-server-request.js → impl/http-incoming.host.js} +17 -43
- package/esm/http/{http-server-response.js → impl/http-outgoing.host.js} +6 -26
- package/esm/http/{helpers/multipart-helper.js → impl/multipart-reader.js} +22 -20
- package/esm/http/impl/{http-incoming-message.host.js → node-incoming-message.host.js} +11 -52
- package/esm/http/impl/{http-outgoing-message.host.js → node-outgoing-message.host.js} +9 -12
- package/esm/http/interfaces/http-incoming.interface.js +22 -0
- package/esm/http/interfaces/http-outgoing.interface.js +19 -0
- package/esm/http/interfaces/node-incoming-message.interface.js +60 -0
- package/esm/http/interfaces/node-outgoing-message.interface.js +12 -0
- package/esm/http/utils/body-reader.js +210 -0
- package/esm/http/{helpers → utils}/convert-to-headers.js +1 -1
- package/esm/http/{helpers → utils}/convert-to-raw-headers.js +2 -3
- package/esm/http/{helpers → utils}/match-known-fields.js +11 -9
- package/esm/http/utils/wrap-exception.js +30 -0
- package/esm/index.js +25 -26
- package/esm/platform-adapter.js +19 -1
- package/esm/type-guards.js +16 -0
- package/package.json +22 -10
- package/types/augmentation/18n.augmentation.d.ts +32 -2
- package/types/augmentation/http-controller.augmentation.d.ts +21 -0
- package/types/constants.d.ts +2 -0
- package/types/execution-context.d.ts +24 -26
- package/types/helpers/service-base.d.ts +10 -0
- package/types/http/express-adapter.d.ts +13 -0
- package/types/http/http-adapter.d.ts +27 -0
- package/types/http/http-context.d.ts +44 -0
- package/types/http/impl/asset-cache.d.ts +5 -0
- package/types/http/impl/http-handler.d.ts +73 -0
- package/types/http/impl/http-incoming.host.d.ts +23 -0
- package/types/http/impl/http-outgoing.host.d.ts +17 -0
- package/types/http/{helpers/multipart-helper.d.ts → impl/multipart-reader.d.ts} +8 -6
- package/types/http/impl/{http-incoming-message.host.d.ts → node-incoming-message.host.d.ts} +9 -22
- package/types/http/impl/{http-outgoing-message.host.d.ts → node-outgoing-message.host.d.ts} +11 -27
- package/types/http/{http-server-request.d.ts → interfaces/http-incoming.interface.d.ts} +28 -17
- package/types/http/{http-server-response.d.ts → interfaces/http-outgoing.interface.d.ts} +17 -10
- package/types/http/interfaces/node-incoming-message.interface.d.ts +38 -0
- package/types/http/interfaces/node-outgoing-message.interface.d.ts +29 -0
- package/types/http/utils/body-reader.d.ts +41 -0
- package/types/http/utils/wrap-exception.d.ts +2 -0
- package/types/index.d.ts +24 -26
- package/types/platform-adapter.d.ts +20 -48
- package/types/type-guards.d.ts +8 -0
- package/cjs/augmentation/collection.augmentation.js +0 -2
- package/cjs/augmentation/container.augmentation.js +0 -2
- package/cjs/augmentation/resource.augmentation.js +0 -26
- package/cjs/augmentation/singleton.augmentation.js +0 -2
- package/cjs/augmentation/storage.augmentation.js +0 -2
- package/cjs/execution-context.host.js +0 -46
- package/cjs/http/adapters/express-adapter.host.js +0 -34
- package/cjs/http/adapters/express-adapter.js +0 -14
- package/cjs/http/adapters/node-http-adapter.host.js +0 -70
- package/cjs/http/adapters/node-http-adapter.js +0 -14
- package/cjs/http/helpers/json-body-loader.js +0 -29
- package/cjs/http/helpers/query-parsers.js +0 -16
- package/cjs/http/http-adapter-host.js +0 -715
- package/cjs/interfaces/interceptor.interface.js +0 -2
- package/cjs/interfaces/request-handler.interface.js +0 -2
- package/cjs/platform-adapter.host.js +0 -154
- package/cjs/request-context.js +0 -25
- package/cjs/request.host.js +0 -24
- package/cjs/request.js +0 -2
- package/cjs/response.host.js +0 -22
- package/cjs/response.js +0 -2
- package/esm/augmentation/collection.augmentation.js +0 -1
- package/esm/augmentation/container.augmentation.js +0 -1
- package/esm/augmentation/resource.augmentation.js +0 -24
- package/esm/augmentation/singleton.augmentation.js +0 -1
- package/esm/augmentation/storage.augmentation.js +0 -1
- package/esm/execution-context.host.js +0 -42
- package/esm/http/adapters/express-adapter.host.js +0 -30
- package/esm/http/adapters/express-adapter.js +0 -11
- package/esm/http/adapters/node-http-adapter.host.js +0 -65
- package/esm/http/adapters/node-http-adapter.js +0 -11
- package/esm/http/helpers/json-body-loader.js +0 -24
- package/esm/http/helpers/query-parsers.js +0 -12
- package/esm/http/http-adapter-host.js +0 -710
- package/esm/interfaces/interceptor.interface.js +0 -1
- package/esm/interfaces/request-handler.interface.js +0 -1
- package/esm/platform-adapter.host.js +0 -149
- package/esm/request-context.js +0 -22
- package/esm/request.host.js +0 -20
- package/esm/request.js +0 -1
- package/esm/response.host.js +0 -18
- package/esm/response.js +0 -1
- package/i18n/i18n/en/error.json +0 -21
- package/types/augmentation/collection.augmentation.d.ts +0 -146
- package/types/augmentation/container.augmentation.d.ts +0 -14
- package/types/augmentation/resource.augmentation.d.ts +0 -38
- package/types/augmentation/singleton.augmentation.d.ts +0 -83
- package/types/augmentation/storage.augmentation.d.ts +0 -50
- package/types/execution-context.host.d.ts +0 -25
- package/types/http/adapters/express-adapter.d.ts +0 -15
- package/types/http/adapters/express-adapter.host.d.ts +0 -12
- package/types/http/adapters/node-http-adapter.d.ts +0 -17
- package/types/http/adapters/node-http-adapter.host.d.ts +0 -19
- package/types/http/helpers/json-body-loader.d.ts +0 -5
- package/types/http/helpers/query-parsers.d.ts +0 -1
- package/types/http/http-adapter-host.d.ts +0 -34
- package/types/interfaces/interceptor.interface.d.ts +0 -2
- package/types/interfaces/request-handler.interface.d.ts +0 -4
- package/types/platform-adapter.host.d.ts +0 -43
- package/types/request-context.d.ts +0 -13
- package/types/request.d.ts +0 -14
- package/types/request.host.d.ts +0 -27
- package/types/response.d.ts +0 -22
- package/types/response.host.d.ts +0 -22
- package/types/services/api-service.d.ts +0 -10
- /package/cjs/http/{helpers → utils}/common.js +0 -0
- /package/cjs/http/{helpers → utils}/concat-readable.js +0 -0
- /package/cjs/http/{helpers → utils}/convert-to-headers.js +0 -0
- /package/esm/http/{helpers → utils}/common.js +0 -0
- /package/esm/http/{helpers → utils}/concat-readable.js +0 -0
- /package/types/{services → helpers}/logger.d.ts +0 -0
- /package/types/http/{helpers → utils}/common.d.ts +0 -0
- /package/types/http/{helpers → utils}/concat-readable.d.ts +0 -0
- /package/types/http/{helpers → utils}/convert-to-headers.d.ts +0 -0
- /package/types/http/{helpers → utils}/convert-to-raw-headers.d.ts +0 -0
- /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 });
|
|
559
|
-
response.value = value;
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (response.value)
|
|
564
|
-
response.value = endpoint.encodeReturning(response.value, { coerce: 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
|
-
}
|