@opra/core 0.2.0 → 0.4.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/adapter.js +319 -0
- package/cjs/{implementation → adapter}/express-adapter.js +3 -6
- package/cjs/adapter/http-adapter.js +242 -0
- package/cjs/adapter/metadata-resource.js +23 -0
- package/cjs/{implementation → adapter}/query-context.js +1 -1
- package/cjs/enums/http-headers.enum.js +1 -1
- package/cjs/{implementation → helpers}/headers-map.js +2 -2
- package/cjs/index.js +7 -6
- package/cjs/interfaces/resource.interface.js +2 -0
- package/cjs/services/{json-data-service.js → json-collection-service.js} +62 -45
- package/cjs/services/json-singleton-service.js +97 -0
- package/esm/{implementation → adapter}/adapter.d.ts +17 -9
- package/esm/adapter/adapter.js +315 -0
- package/esm/{implementation → adapter}/express-adapter.d.ts +2 -2
- package/esm/{implementation → adapter}/express-adapter.js +3 -6
- package/esm/{implementation → adapter}/http-adapter.d.ts +2 -3
- package/esm/adapter/http-adapter.js +238 -0
- package/esm/adapter/metadata-resource.d.ts +8 -0
- package/esm/adapter/metadata-resource.js +20 -0
- package/esm/{implementation → adapter}/query-context.d.ts +6 -6
- package/esm/{implementation → adapter}/query-context.js +1 -1
- package/esm/enums/http-headers.enum.d.ts +1 -1
- package/esm/enums/http-headers.enum.js +1 -1
- package/esm/{implementation → helpers}/headers-map.d.ts +1 -1
- package/esm/{implementation → helpers}/headers-map.js +1 -1
- package/esm/index.d.ts +7 -6
- package/esm/index.js +7 -6
- package/esm/interfaces/resource.interface.d.ts +22 -0
- package/esm/interfaces/resource.interface.js +1 -0
- package/esm/services/{json-data-service.d.ts → json-collection-service.d.ts} +18 -19
- package/esm/services/{json-data-service.js → json-collection-service.js} +61 -44
- package/esm/services/json-singleton-service.d.ts +39 -0
- package/esm/services/json-singleton-service.js +92 -0
- package/esm/types.d.ts +2 -8
- package/esm/utils/create-i18n.d.ts +1 -1
- package/package.json +16 -14
- package/cjs/implementation/adapter-utils/entity-resource-execute.util.js +0 -86
- package/cjs/implementation/adapter-utils/resource-execute.util.js +0 -11
- package/cjs/implementation/adapter-utils/resource-prepare.util.js +0 -11
- package/cjs/implementation/adapter.js +0 -130
- package/cjs/implementation/http-adapter.js +0 -253
- package/cjs/interfaces/entity-service.interface.js +0 -30
- package/esm/implementation/adapter-utils/entity-resource-execute.util.d.ts +0 -3
- package/esm/implementation/adapter-utils/entity-resource-execute.util.js +0 -82
- package/esm/implementation/adapter-utils/resource-execute.util.d.ts +0 -3
- package/esm/implementation/adapter-utils/resource-execute.util.js +0 -7
- package/esm/implementation/adapter-utils/resource-prepare.util.d.ts +0 -3
- package/esm/implementation/adapter-utils/resource-prepare.util.js +0 -7
- package/esm/implementation/adapter.js +0 -126
- package/esm/implementation/http-adapter.js +0 -249
- package/esm/interfaces/entity-service.interface.d.ts +0 -19
- package/esm/interfaces/entity-service.interface.js +0 -26
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { AsyncEventEmitter } from 'strict-typed-events';
|
|
2
|
+
import { ResponsiveMap } from '@opra/common';
|
|
3
|
+
import { FailedDependencyError, ForbiddenError, ResourceNotFoundError, wrapException } from '@opra/exception';
|
|
4
|
+
import { I18n, translate } from '@opra/i18n';
|
|
5
|
+
import { CollectionCountQuery, CollectionResourceInfo, ComplexType, SingletonResourceInfo } from '@opra/schema';
|
|
6
|
+
import { HttpHeaders } from '../enums/index.js';
|
|
7
|
+
import { createI18n } from '../utils/create-i18n.js';
|
|
8
|
+
import { MetadataResource } from './metadata-resource.js';
|
|
9
|
+
export class OpraAdapter {
|
|
10
|
+
document;
|
|
11
|
+
i18n;
|
|
12
|
+
userContextResolver;
|
|
13
|
+
// protected _metadataResource: SingletonResourceInfo;
|
|
14
|
+
_internalResources = new ResponsiveMap();
|
|
15
|
+
constructor(document) {
|
|
16
|
+
this.document = document;
|
|
17
|
+
}
|
|
18
|
+
async handler(executionContext) {
|
|
19
|
+
let queryContexts;
|
|
20
|
+
let userContext;
|
|
21
|
+
let failed = false;
|
|
22
|
+
try {
|
|
23
|
+
queryContexts = this.prepareRequests(executionContext);
|
|
24
|
+
let stop = false;
|
|
25
|
+
// Read requests can be executed simultaneously, write request should be executed one by one
|
|
26
|
+
let promises;
|
|
27
|
+
let exclusive = false;
|
|
28
|
+
for (const context of queryContexts) {
|
|
29
|
+
exclusive = exclusive || context.query.operation !== 'read';
|
|
30
|
+
// Wait previous read requests before executing update request
|
|
31
|
+
if (exclusive && promises) {
|
|
32
|
+
await Promise.allSettled(promises);
|
|
33
|
+
promises = undefined;
|
|
34
|
+
}
|
|
35
|
+
// If previous request in bucket had an error and executed an update
|
|
36
|
+
// we do not execute next requests
|
|
37
|
+
if (stop) {
|
|
38
|
+
context.errors.push(new FailedDependencyError());
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const promise = (async () => {
|
|
43
|
+
// if (context.query.method === 'metadata') {
|
|
44
|
+
// await this._getSchemaExecute(context); //todo
|
|
45
|
+
// return;
|
|
46
|
+
// }
|
|
47
|
+
const resource = context.query.resource;
|
|
48
|
+
await this._resourcePrepare(resource, context);
|
|
49
|
+
if (this.userContextResolver && !userContext)
|
|
50
|
+
userContext = this.userContextResolver({
|
|
51
|
+
executionContext,
|
|
52
|
+
isBatch: this.isBatch(executionContext)
|
|
53
|
+
});
|
|
54
|
+
context.userContext = userContext;
|
|
55
|
+
await this._resourceExecute(this.document, resource, context);
|
|
56
|
+
})().catch(e => {
|
|
57
|
+
context.errors.push(e);
|
|
58
|
+
});
|
|
59
|
+
if (exclusive)
|
|
60
|
+
await promise;
|
|
61
|
+
else {
|
|
62
|
+
promises = promises || [];
|
|
63
|
+
promises.push(promise);
|
|
64
|
+
}
|
|
65
|
+
// todo execute sub property queries
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
context.errors.unshift(e);
|
|
69
|
+
}
|
|
70
|
+
if (context.errors.length) {
|
|
71
|
+
// noinspection SuspiciousTypeOfGuard
|
|
72
|
+
context.errors = context.errors.map(e => wrapException(e));
|
|
73
|
+
if (exclusive)
|
|
74
|
+
stop = stop || !!context.errors.find(e => !(e.issue.severity === 'warning' || e.issue.severity === 'info'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (promises)
|
|
78
|
+
await Promise.allSettled(promises);
|
|
79
|
+
await this.sendResponse(executionContext, queryContexts);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
failed = true;
|
|
83
|
+
const error = wrapException(e);
|
|
84
|
+
await this.sendError(executionContext, error);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (executionContext instanceof AsyncEventEmitter) {
|
|
88
|
+
await executionContext
|
|
89
|
+
.emitAsyncSerial('finish', {
|
|
90
|
+
userContext,
|
|
91
|
+
failed
|
|
92
|
+
}).catch();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async _resourcePrepare(resource, context) {
|
|
97
|
+
const { query } = context;
|
|
98
|
+
const fn = resource.metadata['pre_' + query.method];
|
|
99
|
+
if (fn && typeof fn === 'function') {
|
|
100
|
+
await fn(context);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async _resourceExecute(document, resource, context) {
|
|
104
|
+
if (resource instanceof CollectionResourceInfo) {
|
|
105
|
+
const { query } = context;
|
|
106
|
+
if (query.kind === 'SearchCollectionQuery') {
|
|
107
|
+
const promises = [];
|
|
108
|
+
let search;
|
|
109
|
+
promises.push(this._collectionResourceExecute(document, resource, context)
|
|
110
|
+
.then(v => search = v));
|
|
111
|
+
if (query.count && resource.metadata.count) {
|
|
112
|
+
const ctx = {
|
|
113
|
+
query: new CollectionCountQuery(query.resource, { filter: query.filter }),
|
|
114
|
+
resultPath: ''
|
|
115
|
+
};
|
|
116
|
+
Object.setPrototypeOf(ctx, context);
|
|
117
|
+
promises.push(this._collectionResourceExecute(document, resource, ctx));
|
|
118
|
+
}
|
|
119
|
+
await Promise.all(promises);
|
|
120
|
+
context.response = search;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
context.response = await this._collectionResourceExecute(document, resource, context);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
else if (resource instanceof SingletonResourceInfo) {
|
|
127
|
+
context.response = await this._singletonResourceExecute(document, resource, context);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`Executing "${resource.kind}" has not been implemented yet`);
|
|
131
|
+
}
|
|
132
|
+
async _init(options) {
|
|
133
|
+
if (options?.i18n instanceof I18n)
|
|
134
|
+
this.i18n = options.i18n;
|
|
135
|
+
else if (typeof options?.i18n === 'function')
|
|
136
|
+
this.i18n = await options.i18n();
|
|
137
|
+
else
|
|
138
|
+
this.i18n = await createI18n(options?.i18n);
|
|
139
|
+
this.i18n = this.i18n || I18n.defaultInstance;
|
|
140
|
+
if (!this.i18n.isInitialized)
|
|
141
|
+
await this.i18n.init();
|
|
142
|
+
this.userContextResolver = options?.userContext;
|
|
143
|
+
const metadataResource = new MetadataResource();
|
|
144
|
+
const metadataResourceInfo = new SingletonResourceInfo(this.document, '$metadata', this.document.getComplexDataType('object'), {
|
|
145
|
+
kind: 'SingletonResource',
|
|
146
|
+
type: 'object',
|
|
147
|
+
instance: metadataResource,
|
|
148
|
+
get: {
|
|
149
|
+
handler: metadataResource.get.bind(metadataResource)
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
this._internalResources.set(metadataResourceInfo.name, metadataResourceInfo);
|
|
153
|
+
metadataResource.init(metadataResourceInfo);
|
|
154
|
+
for (const r of this.document.resources.values()) {
|
|
155
|
+
if (r.instance) {
|
|
156
|
+
const init = r.instance.init;
|
|
157
|
+
if (init)
|
|
158
|
+
await init.call(r.instance, r);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async _collectionResourceExecute(document, resource, context) {
|
|
163
|
+
const method = context.query.method;
|
|
164
|
+
const resolverInfo = resource.metadata[method];
|
|
165
|
+
if (!(resolverInfo && resolverInfo.handler))
|
|
166
|
+
throw new ForbiddenError({
|
|
167
|
+
message: translate('RESOLVER_FORBIDDEN', { method }, `The resource endpoint does not accept '{{method}}' operations`),
|
|
168
|
+
severity: 'error',
|
|
169
|
+
code: 'RESOLVER_FORBIDDEN'
|
|
170
|
+
});
|
|
171
|
+
let result;
|
|
172
|
+
switch (method) {
|
|
173
|
+
case 'create': {
|
|
174
|
+
const query = context.query;
|
|
175
|
+
result = await resolverInfo.handler(context, query.data, query);
|
|
176
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
177
|
+
if (result)
|
|
178
|
+
context.status = 201;
|
|
179
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, resource.dataType.name);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
case 'count': {
|
|
183
|
+
const query = context.query;
|
|
184
|
+
result = await resolverInfo.handler(context, query);
|
|
185
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_Count, result);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
case 'get': {
|
|
189
|
+
const query = context.query;
|
|
190
|
+
result = await resolverInfo.handler(context, query.keyValue, query);
|
|
191
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
192
|
+
if (!result)
|
|
193
|
+
throw new ResourceNotFoundError(resource.name, query.keyValue);
|
|
194
|
+
const v = await this._pathWalkThrough(query, query.dataType, result, resource.name);
|
|
195
|
+
if (v.value === undefined)
|
|
196
|
+
throw new ResourceNotFoundError(v.path);
|
|
197
|
+
if (v.dataType)
|
|
198
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, v.dataType.name);
|
|
199
|
+
return v.value;
|
|
200
|
+
}
|
|
201
|
+
case 'search': {
|
|
202
|
+
const query = context.query;
|
|
203
|
+
result = await resolverInfo.handler(context, query);
|
|
204
|
+
const items = Array.isArray(result) ? result : (context.response ? [result] : []);
|
|
205
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, resource.dataType.name);
|
|
206
|
+
return items;
|
|
207
|
+
}
|
|
208
|
+
case 'update': {
|
|
209
|
+
const query = context.query;
|
|
210
|
+
result = await resolverInfo.handler(context, query.keyValue, query.data, query);
|
|
211
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
212
|
+
if (!result)
|
|
213
|
+
throw new ResourceNotFoundError(resource.name, query.keyValue);
|
|
214
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, resource.dataType.name);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
case 'delete':
|
|
218
|
+
case 'deleteMany':
|
|
219
|
+
case 'updateMany': {
|
|
220
|
+
switch (method) {
|
|
221
|
+
case 'delete': {
|
|
222
|
+
const query = context.query;
|
|
223
|
+
result = await resolverInfo.handler(context, query.keyValue, query);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case 'deleteMany': {
|
|
227
|
+
const query = context.query;
|
|
228
|
+
result = await resolverInfo.handler(context, query);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case 'updateMany': {
|
|
232
|
+
const query = context.query;
|
|
233
|
+
result = await resolverInfo.handler(context, query.data, query);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
let affected;
|
|
238
|
+
if (typeof result === 'number')
|
|
239
|
+
affected = result;
|
|
240
|
+
if (typeof result === 'boolean')
|
|
241
|
+
affected = result ? 1 : 0;
|
|
242
|
+
if (typeof result === 'object')
|
|
243
|
+
affected = result.affectedRows || result.affected;
|
|
244
|
+
return {
|
|
245
|
+
operation: context.query.method,
|
|
246
|
+
affected
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async _singletonResourceExecute(document, resource, context) {
|
|
252
|
+
const method = context.query.method;
|
|
253
|
+
const resolverInfo = resource.metadata[method];
|
|
254
|
+
if (!(resolverInfo && resolverInfo.handler))
|
|
255
|
+
throw new ForbiddenError({
|
|
256
|
+
message: translate('RESOLVER_FORBIDDEN', { method }, `The resource endpoint does not accept '{{method}}' operations`),
|
|
257
|
+
severity: 'error',
|
|
258
|
+
code: 'RESOLVER_FORBIDDEN'
|
|
259
|
+
});
|
|
260
|
+
let result = await resolverInfo.handler(context);
|
|
261
|
+
switch (method) {
|
|
262
|
+
case 'get': {
|
|
263
|
+
const query = context.query;
|
|
264
|
+
result = await resolverInfo.handler(context, query);
|
|
265
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
266
|
+
if (!result)
|
|
267
|
+
throw new ResourceNotFoundError(resource.name);
|
|
268
|
+
const v = await this._pathWalkThrough(query, query.dataType, result, resource.name);
|
|
269
|
+
if (v.value === undefined)
|
|
270
|
+
throw new ResourceNotFoundError(v.path);
|
|
271
|
+
if (v.dataType)
|
|
272
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, v.dataType.name);
|
|
273
|
+
return v.value;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!result)
|
|
277
|
+
return;
|
|
278
|
+
result = Array.isArray(result) ? result[0] : result;
|
|
279
|
+
let dataType = resource.dataType;
|
|
280
|
+
if (context.resultPath) {
|
|
281
|
+
const pathArray = context.resultPath.split('.');
|
|
282
|
+
for (const field of pathArray) {
|
|
283
|
+
const prop = dataType instanceof ComplexType ? dataType.fields.get(field) : undefined;
|
|
284
|
+
dataType = prop && prop.type ? this.document.types.get(prop.type) : undefined;
|
|
285
|
+
result = result && typeof result === 'object' && result[field];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (method === 'create')
|
|
289
|
+
context.status = 201;
|
|
290
|
+
context.responseHeaders.set(HttpHeaders.X_Opra_DataType, resource.dataType.name);
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
async _pathWalkThrough(query, dataType, value, parentPath) {
|
|
294
|
+
const { child } = query;
|
|
295
|
+
if (!child)
|
|
296
|
+
return { value, dataType, path: parentPath };
|
|
297
|
+
// Make a case in sensitive lookup
|
|
298
|
+
const fieldNameLower = child.fieldName.toLowerCase();
|
|
299
|
+
const path = parentPath + (parentPath ? '.' : '') + child.fieldName;
|
|
300
|
+
for (const key of Object.keys(value)) {
|
|
301
|
+
if (key.toLowerCase() === fieldNameLower) {
|
|
302
|
+
let v = value[key];
|
|
303
|
+
if (v == null)
|
|
304
|
+
return { path };
|
|
305
|
+
if (child.child && child.dataType instanceof ComplexType) {
|
|
306
|
+
if (Array.isArray(v))
|
|
307
|
+
v = v[0];
|
|
308
|
+
return this._pathWalkThrough(child, child.dataType, v, path);
|
|
309
|
+
}
|
|
310
|
+
return { value: v, dataType: child.dataType, path };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { path };
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Application } from 'express';
|
|
2
|
-
import {
|
|
2
|
+
import { OpraDocument } from '@opra/schema';
|
|
3
3
|
import type { IHttpExecutionContext } from '../interfaces/execution-context.interface';
|
|
4
4
|
import { OpraHttpAdapter } from './http-adapter.js';
|
|
5
5
|
export declare namespace OpraExpressAdapter {
|
|
@@ -7,5 +7,5 @@ export declare namespace OpraExpressAdapter {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
export declare class OpraExpressAdapter extends OpraHttpAdapter<IHttpExecutionContext> {
|
|
10
|
-
static init(app: Application,
|
|
10
|
+
static init(app: Application, document: OpraDocument, options?: OpraExpressAdapter.Options): Promise<OpraExpressAdapter>;
|
|
11
11
|
}
|
|
@@ -3,12 +3,9 @@ import { AsyncEventEmitter } from 'strict-typed-events';
|
|
|
3
3
|
import { normalizePath } from '@opra/url';
|
|
4
4
|
import { OpraHttpAdapter } from './http-adapter.js';
|
|
5
5
|
export class OpraExpressAdapter extends OpraHttpAdapter {
|
|
6
|
-
static async init(app,
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
...options,
|
|
10
|
-
i18n
|
|
11
|
-
});
|
|
6
|
+
static async init(app, document, options) {
|
|
7
|
+
const adapter = new OpraExpressAdapter(document);
|
|
8
|
+
await adapter._init(options);
|
|
12
9
|
const prefix = '/' + normalizePath(options?.prefix, true);
|
|
13
10
|
app.use(prefix, bodyParser.json());
|
|
14
11
|
app.use(prefix, (request, response, next) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OpraException } from '@opra/exception';
|
|
2
|
-
import {
|
|
2
|
+
import { OpraQuery } from '@opra/schema';
|
|
3
3
|
import { OpraURL } from '@opra/url';
|
|
4
4
|
import { IHttpExecutionContext } from '../interfaces/execution-context.interface.js';
|
|
5
5
|
import { OpraAdapter } from './adapter.js';
|
|
@@ -17,8 +17,7 @@ interface PreparedOutput {
|
|
|
17
17
|
export declare class OpraHttpAdapter<TExecutionContext extends IHttpExecutionContext> extends OpraAdapter<IHttpExecutionContext> {
|
|
18
18
|
protected prepareRequests(executionContext: TExecutionContext): QueryContext[];
|
|
19
19
|
prepareRequest(executionContext: IHttpExecutionContext, url: OpraURL, method: string, headers: Map<string, string>, body?: any): QueryContext;
|
|
20
|
-
|
|
21
|
-
buildQuery(url: OpraURL, method: string, body?: any): OpraAnyQuery | undefined;
|
|
20
|
+
buildQuery(url: OpraURL, method: string, body?: any): OpraQuery | undefined;
|
|
22
21
|
protected sendResponse(executionContext: TExecutionContext, queryContexts: QueryContext[]): Promise<void>;
|
|
23
22
|
protected isBatch(executionContext: TExecutionContext): boolean;
|
|
24
23
|
protected createOutput(ctx: QueryContext): PreparedOutput;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { BadRequestError, InternalServerError, IssueSeverity, MethodNotAllowedError, NotFoundError, OpraException, wrapException } from '@opra/exception';
|
|
2
|
+
import { CollectionCreateQuery, CollectionDeleteManyQuery, CollectionDeleteQuery, CollectionGetQuery, CollectionResourceInfo, CollectionSearchQuery, CollectionUpdateManyQuery, CollectionUpdateQuery, ComplexType, ContainerResourceInfo, FieldGetQuery, OpraSchema, SingletonGetQuery, SingletonResourceInfo, UnionType, } from '@opra/schema';
|
|
3
|
+
import { OpraURL } from '@opra/url';
|
|
4
|
+
import { HttpHeaders, HttpStatus } from '../enums/index.js';
|
|
5
|
+
import { HeadersMap } from '../helpers/headers-map.js';
|
|
6
|
+
import { OpraAdapter } from './adapter.js';
|
|
7
|
+
import { QueryContext } from './query-context.js';
|
|
8
|
+
export class OpraHttpAdapter extends OpraAdapter {
|
|
9
|
+
prepareRequests(executionContext) {
|
|
10
|
+
const req = executionContext.getRequestWrapper();
|
|
11
|
+
// todo implement batch requests
|
|
12
|
+
if (this.isBatch(executionContext)) {
|
|
13
|
+
throw new Error('not implemented yet');
|
|
14
|
+
}
|
|
15
|
+
const url = new OpraURL(req.getUrl());
|
|
16
|
+
return [
|
|
17
|
+
this.prepareRequest(executionContext, url, req.getMethod(), new HeadersMap(req.getHeaders()), req.getBody())
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
prepareRequest(executionContext, url, method, headers, body) {
|
|
21
|
+
if (!url.path.size)
|
|
22
|
+
throw new BadRequestError();
|
|
23
|
+
if (method !== 'GET' && url.path.size > 1)
|
|
24
|
+
throw new BadRequestError();
|
|
25
|
+
const query = this.buildQuery(url, method, body);
|
|
26
|
+
if (!query)
|
|
27
|
+
throw new MethodNotAllowedError({
|
|
28
|
+
message: `Method "${method}" is not allowed by target endpoint`
|
|
29
|
+
});
|
|
30
|
+
return new QueryContext({
|
|
31
|
+
service: this.document,
|
|
32
|
+
executionContext,
|
|
33
|
+
query,
|
|
34
|
+
headers: new HeadersMap(),
|
|
35
|
+
params: url.searchParams,
|
|
36
|
+
continueOnError: query.operation === 'read'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
buildQuery(url, method, body) {
|
|
40
|
+
const pathLen = url.path.size;
|
|
41
|
+
let p = url.path.get(0);
|
|
42
|
+
let resource = this._internalResources.get(p.resource) || this.document.getResource(p.resource);
|
|
43
|
+
let container;
|
|
44
|
+
let pathIndex = 0;
|
|
45
|
+
while (resource && resource instanceof ContainerResourceInfo) {
|
|
46
|
+
container = resource;
|
|
47
|
+
p = url.path.get(++pathIndex);
|
|
48
|
+
resource = container.getResource(p.resource);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
method = method.toUpperCase();
|
|
52
|
+
let query;
|
|
53
|
+
if (resource instanceof SingletonResourceInfo && !p.key) {
|
|
54
|
+
switch (method) {
|
|
55
|
+
case 'GET': {
|
|
56
|
+
query = new SingletonGetQuery(resource);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (resource instanceof CollectionResourceInfo) {
|
|
61
|
+
switch (method) {
|
|
62
|
+
case 'GET': {
|
|
63
|
+
if (p.key) {
|
|
64
|
+
const searchParams = url.searchParams;
|
|
65
|
+
query = new CollectionGetQuery(resource, p.key, {
|
|
66
|
+
pick: searchParams.get('$pick'),
|
|
67
|
+
omit: searchParams.get('$omit'),
|
|
68
|
+
include: searchParams.get('$include')
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const searchParams = url.searchParams;
|
|
73
|
+
query = new CollectionSearchQuery(resource, {
|
|
74
|
+
filter: searchParams.get('$filter'),
|
|
75
|
+
limit: searchParams.get('$limit'),
|
|
76
|
+
skip: searchParams.get('$skip'),
|
|
77
|
+
distinct: searchParams.get('$distinct'),
|
|
78
|
+
count: searchParams.get('$count'),
|
|
79
|
+
sort: searchParams.get('$sort'),
|
|
80
|
+
pick: searchParams.get('$pick'),
|
|
81
|
+
omit: searchParams.get('$omit'),
|
|
82
|
+
include: searchParams.get('$include')
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'DELETE': {
|
|
88
|
+
const searchParams = url.searchParams;
|
|
89
|
+
query = p.key
|
|
90
|
+
? new CollectionDeleteQuery(resource, p.key)
|
|
91
|
+
: new CollectionDeleteManyQuery(resource, {
|
|
92
|
+
filter: searchParams.get('$filter'),
|
|
93
|
+
});
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'POST': {
|
|
97
|
+
if (!p.key) {
|
|
98
|
+
const searchParams = url.searchParams;
|
|
99
|
+
query = new CollectionCreateQuery(resource, body, {
|
|
100
|
+
pick: searchParams.get('$pick'),
|
|
101
|
+
omit: searchParams.get('$omit'),
|
|
102
|
+
include: searchParams.get('$include')
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'PATCH': {
|
|
108
|
+
if (p.key) {
|
|
109
|
+
const searchParams = url.searchParams;
|
|
110
|
+
query = new CollectionUpdateQuery(resource, p.key, body, {
|
|
111
|
+
pick: searchParams.get('$pick'),
|
|
112
|
+
omit: searchParams.get('$omit'),
|
|
113
|
+
include: searchParams.get('$include')
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const searchParams = url.searchParams;
|
|
118
|
+
query = new CollectionUpdateManyQuery(resource, body, {
|
|
119
|
+
filter: searchParams.get('$filter')
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
throw new InternalServerError();
|
|
128
|
+
if (query instanceof SingletonGetQuery || query instanceof CollectionGetQuery || query instanceof FieldGetQuery) {
|
|
129
|
+
// Move through properties
|
|
130
|
+
let parentType;
|
|
131
|
+
const curPath = [];
|
|
132
|
+
let parent = query;
|
|
133
|
+
while (++pathIndex < pathLen) {
|
|
134
|
+
p = url.path.get(pathIndex);
|
|
135
|
+
parentType = parent.dataType;
|
|
136
|
+
if (parent.dataType instanceof UnionType) {
|
|
137
|
+
if (parent.dataType.name === 'any')
|
|
138
|
+
parentType = this.document.getComplexDataType('object');
|
|
139
|
+
else
|
|
140
|
+
throw new TypeError(`"${resource.name}.${curPath.join()}" is a UnionType and needs type casting.`);
|
|
141
|
+
}
|
|
142
|
+
if (!(parentType instanceof ComplexType))
|
|
143
|
+
throw new TypeError(`"${resource.name}.${curPath.join()}" is not a ComplexType and has no fields.`);
|
|
144
|
+
curPath.push(p.resource);
|
|
145
|
+
parent.child = new FieldGetQuery(parent, p.resource, { castingType: parentType });
|
|
146
|
+
parent = parent.child;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return query;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
if (e instanceof OpraException)
|
|
153
|
+
throw e;
|
|
154
|
+
throw new BadRequestError(e);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async sendResponse(executionContext, queryContexts) {
|
|
158
|
+
const outputPackets = [];
|
|
159
|
+
for (const ctx of queryContexts) {
|
|
160
|
+
const v = this.createOutput(ctx);
|
|
161
|
+
outputPackets.push(v);
|
|
162
|
+
}
|
|
163
|
+
if (this.isBatch(executionContext)) {
|
|
164
|
+
// this.writeError([], new InternalServerError({message: 'Not implemented yet'}));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!outputPackets.length)
|
|
168
|
+
return this.sendError(executionContext, new NotFoundError());
|
|
169
|
+
const out = outputPackets[0];
|
|
170
|
+
const resp = executionContext.getResponseWrapper();
|
|
171
|
+
resp.setStatus(out.status);
|
|
172
|
+
resp.setHeader(HttpHeaders.Content_Type, 'application/json');
|
|
173
|
+
resp.setHeader(HttpHeaders.Cache_Control, 'no-cache');
|
|
174
|
+
resp.setHeader(HttpHeaders.Pragma, 'no-cache');
|
|
175
|
+
resp.setHeader(HttpHeaders.Expires, '-1');
|
|
176
|
+
resp.setHeader(HttpHeaders.X_Opra_Version, OpraSchema.Version);
|
|
177
|
+
if (out.headers) {
|
|
178
|
+
for (const [k, v] of Object.entries(out.headers)) {
|
|
179
|
+
resp.setHeader(k, v);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
resp.send(JSON.stringify(out.body));
|
|
183
|
+
resp.end();
|
|
184
|
+
}
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
186
|
+
isBatch(executionContext) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
createOutput(ctx) {
|
|
190
|
+
const { query } = ctx;
|
|
191
|
+
let body;
|
|
192
|
+
let status = ctx.status || 0;
|
|
193
|
+
const errors = ctx.errors.map(e => wrapException(e));
|
|
194
|
+
if (errors && errors.length) {
|
|
195
|
+
// Sort errors from fatal to info
|
|
196
|
+
errors.sort((a, b) => {
|
|
197
|
+
const i = IssueSeverity.Keys.indexOf(a.issue.severity) - IssueSeverity.Keys.indexOf(b.issue.severity);
|
|
198
|
+
if (i === 0)
|
|
199
|
+
return b.status - a.status;
|
|
200
|
+
return i;
|
|
201
|
+
});
|
|
202
|
+
if (!status || status < HttpStatus.BAD_REQUEST) {
|
|
203
|
+
status = errors[0].status;
|
|
204
|
+
if (status < HttpStatus.BAD_REQUEST)
|
|
205
|
+
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
206
|
+
}
|
|
207
|
+
body = {
|
|
208
|
+
operation: ctx.query.method,
|
|
209
|
+
errors: errors.map(e => e.issue)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
body = ctx.response;
|
|
214
|
+
status = status || (query.operation === 'create' ? HttpStatus.CREATED : HttpStatus.OK);
|
|
215
|
+
}
|
|
216
|
+
body = this.i18n.deep(body);
|
|
217
|
+
return {
|
|
218
|
+
status,
|
|
219
|
+
headers: ctx.responseHeaders.toObject(),
|
|
220
|
+
body
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async sendError(executionContext, error) {
|
|
224
|
+
const resp = executionContext.getResponseWrapper();
|
|
225
|
+
resp.setStatus(error.status || 500);
|
|
226
|
+
resp.setHeader(HttpHeaders.Content_Type, 'application/json');
|
|
227
|
+
resp.setHeader(HttpHeaders.Cache_Control, 'no-cache');
|
|
228
|
+
resp.setHeader(HttpHeaders.Pragma, 'no-cache');
|
|
229
|
+
resp.setHeader(HttpHeaders.Expires, '-1');
|
|
230
|
+
resp.setHeader(HttpHeaders.X_Opra_Version, OpraSchema.Version);
|
|
231
|
+
const issue = this.i18n.deep(error.issue);
|
|
232
|
+
const body = {
|
|
233
|
+
operation: 'unknown',
|
|
234
|
+
errors: [issue]
|
|
235
|
+
};
|
|
236
|
+
resp.send(JSON.stringify(body));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SingletonResourceInfo } from '@opra/schema';
|
|
2
|
+
import { ISingletonResource } from '../interfaces/resource.interface.js';
|
|
3
|
+
import { JsonSingletonService } from '../services/json-singleton-service.js';
|
|
4
|
+
export declare class MetadataResource implements ISingletonResource<any> {
|
|
5
|
+
service: JsonSingletonService<any>;
|
|
6
|
+
init(resource: SingletonResourceInfo): void;
|
|
7
|
+
get(): any;
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { OprSingletonResource } from '@opra/schema';
|
|
3
|
+
import { JsonSingletonService } from '../services/json-singleton-service.js';
|
|
4
|
+
let MetadataResource = class MetadataResource {
|
|
5
|
+
service;
|
|
6
|
+
init(resource) {
|
|
7
|
+
this.service = new JsonSingletonService(resource.dataType, {
|
|
8
|
+
data: resource.document.getMetadata(true)
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.service.get();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
MetadataResource = __decorate([
|
|
16
|
+
OprSingletonResource(Object, {
|
|
17
|
+
name: '$metadata'
|
|
18
|
+
})
|
|
19
|
+
], MetadataResource);
|
|
20
|
+
export { MetadataResource };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { OpraException } from '@opra/exception';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { OpraDocument, OpraQuery } from '@opra/schema';
|
|
3
|
+
import { OpraURLSearchParams } from '@opra/url';
|
|
4
4
|
import { HttpStatus } from '../enums/index.js';
|
|
5
|
+
import { HeadersMap } from '../helpers/headers-map.js';
|
|
5
6
|
import { ContextType, IExecutionContext, IHttpExecutionContext } from '../interfaces/execution-context.interface.js';
|
|
6
|
-
import { HeadersMap } from './headers-map.js';
|
|
7
7
|
export declare type QueryContextArgs = Pick<QueryContext, 'service' | 'executionContext' | 'query' | 'params' | 'headers' | 'userContext' | 'parentValue' | 'continueOnError'>;
|
|
8
8
|
export declare class QueryContext {
|
|
9
|
-
readonly service:
|
|
9
|
+
readonly service: OpraDocument;
|
|
10
10
|
readonly executionContext: IExecutionContext;
|
|
11
|
-
readonly query:
|
|
12
|
-
readonly params:
|
|
11
|
+
readonly query: OpraQuery;
|
|
12
|
+
readonly params: OpraURLSearchParams;
|
|
13
13
|
readonly headers: HeadersMap;
|
|
14
14
|
readonly parentValue?: any;
|
|
15
15
|
readonly resultPath: string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare enum HttpHeaders {
|
|
5
5
|
X_Opra_Version = "X-Opra-Version",
|
|
6
|
-
|
|
6
|
+
X_Opra_DataType = "X-Opra-DataType",
|
|
7
7
|
X_Opra_Count = "X-Opra-Count",
|
|
8
8
|
/**
|
|
9
9
|
* Defines the authentication method that should be used to access a resource.
|