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