dbgate-rest 7.1.3-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/README.md +7 -0
- package/lib/arrayify.d.ts +3 -0
- package/lib/arrayify.js +81 -0
- package/lib/graphQlDriver.d.ts +2 -0
- package/lib/graphQlDriver.js +55 -0
- package/lib/graphQlQueryParser.d.ts +5 -0
- package/lib/graphQlQueryParser.js +204 -0
- package/lib/graphQlVariables.d.ts +6 -0
- package/lib/graphQlVariables.js +123 -0
- package/lib/graphqlExplorer.d.ts +26 -0
- package/lib/graphqlExplorer.js +125 -0
- package/lib/graphqlIntrospection.d.ts +56 -0
- package/lib/graphqlIntrospection.js +409 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +29 -0
- package/lib/oDataAdapter.d.ts +33 -0
- package/lib/oDataAdapter.js +358 -0
- package/lib/oDataAdapter.test.d.ts +2 -0
- package/lib/oDataAdapter.test.js +61 -0
- package/lib/oDataDriver.d.ts +2 -0
- package/lib/oDataDriver.js +85 -0
- package/lib/oDataMetadataParser.d.ts +2 -0
- package/lib/oDataMetadataParser.js +137 -0
- package/lib/openApiAdapter.d.ts +7 -0
- package/lib/openApiAdapter.js +254 -0
- package/lib/openApiDriver.d.ts +2 -0
- package/lib/openApiDriver.js +90 -0
- package/lib/restApiDef.d.ts +55 -0
- package/lib/restApiDef.js +2 -0
- package/lib/restApiExecutor.d.ts +4 -0
- package/lib/restApiExecutor.js +292 -0
- package/lib/restApiExecutor.test.d.ts +16 -0
- package/lib/restApiExecutor.test.js +99 -0
- package/lib/restAuthTools.d.ts +2 -0
- package/lib/restAuthTools.js +20 -0
- package/lib/restDriverBase.d.ts +61 -0
- package/lib/restDriverBase.js +49 -0
- package/package.json +42 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyseODataDefinition = void 0;
|
|
4
|
+
const oDataMetadataParser_1 = require("./oDataMetadataParser");
|
|
5
|
+
function normalizeServiceRoot(contextUrl, fallbackUrl) {
|
|
6
|
+
const safeFallback = String(fallbackUrl !== null && fallbackUrl !== void 0 ? fallbackUrl : '').trim();
|
|
7
|
+
if (typeof contextUrl === 'string' && contextUrl.trim()) {
|
|
8
|
+
try {
|
|
9
|
+
const resolved = new URL(contextUrl.trim(), safeFallback || undefined);
|
|
10
|
+
resolved.hash = '';
|
|
11
|
+
resolved.search = '';
|
|
12
|
+
resolved.pathname = resolved.pathname.replace(/\/$metadata$/i, '');
|
|
13
|
+
const url = resolved.toString();
|
|
14
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
// ignore, fallback below
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return safeFallback.endsWith('/') ? safeFallback : `${safeFallback}/`;
|
|
21
|
+
}
|
|
22
|
+
function normalizeEndpointPath(valueUrl) {
|
|
23
|
+
const input = String(valueUrl !== null && valueUrl !== void 0 ? valueUrl : '').trim();
|
|
24
|
+
if (!input)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(input, 'http://odata.local');
|
|
28
|
+
const pathWithQuery = `${parsed.pathname}${parsed.search}`;
|
|
29
|
+
return pathWithQuery.startsWith('/') ? pathWithQuery : `/${pathWithQuery}`;
|
|
30
|
+
}
|
|
31
|
+
catch (_a) {
|
|
32
|
+
return input.startsWith('/') ? input : `/${input}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function inferMethods(kind) {
|
|
36
|
+
const normalizedKind = String(kind !== null && kind !== void 0 ? kind : '').toLowerCase();
|
|
37
|
+
if (normalizedKind === 'actionimport')
|
|
38
|
+
return ['POST'];
|
|
39
|
+
if (normalizedKind === 'entityset')
|
|
40
|
+
return ['GET', 'POST'];
|
|
41
|
+
return ['GET'];
|
|
42
|
+
}
|
|
43
|
+
function toLowerCamelCase(value) {
|
|
44
|
+
const text = String(value !== null && value !== void 0 ? value : '').trim();
|
|
45
|
+
if (!text)
|
|
46
|
+
return '';
|
|
47
|
+
return text.charAt(0).toLowerCase() + text.slice(1);
|
|
48
|
+
}
|
|
49
|
+
function normalizeSingularName(value) {
|
|
50
|
+
const text = String(value !== null && value !== void 0 ? value : '').trim();
|
|
51
|
+
if (!text)
|
|
52
|
+
return '';
|
|
53
|
+
if (/ies$/i.test(text))
|
|
54
|
+
return `${text.slice(0, -3)}y`;
|
|
55
|
+
if (/sses$/i.test(text))
|
|
56
|
+
return text;
|
|
57
|
+
if (/s$/i.test(text) && text.length > 1)
|
|
58
|
+
return text.slice(0, -1);
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
function normalizePluralName(value) {
|
|
62
|
+
const text = String(value !== null && value !== void 0 ? value : '').trim();
|
|
63
|
+
if (!text)
|
|
64
|
+
return '';
|
|
65
|
+
if (/y$/i.test(text))
|
|
66
|
+
return `${text.slice(0, -1)}ies`;
|
|
67
|
+
if (/s$/i.test(text))
|
|
68
|
+
return text;
|
|
69
|
+
return `${text}s`;
|
|
70
|
+
}
|
|
71
|
+
function normalizeEntityTypeName(typeName) {
|
|
72
|
+
const text = String(typeName !== null && typeName !== void 0 ? typeName : '').trim();
|
|
73
|
+
if (!text)
|
|
74
|
+
return '';
|
|
75
|
+
const collectionMatch = text.match(/^Collection\((.+)\)$/i);
|
|
76
|
+
const unwrapped = collectionMatch ? collectionMatch[1] : text;
|
|
77
|
+
const slashStripped = unwrapped.includes('/') ? unwrapped.split('/').pop() || unwrapped : unwrapped;
|
|
78
|
+
return slashStripped.trim();
|
|
79
|
+
}
|
|
80
|
+
function buildTypeReferenceKeys(typeReference) {
|
|
81
|
+
const normalizedReference = normalizeEntityTypeName(typeReference);
|
|
82
|
+
if (!normalizedReference)
|
|
83
|
+
return [];
|
|
84
|
+
const keys = new Set();
|
|
85
|
+
const lower = normalizedReference.toLowerCase();
|
|
86
|
+
keys.add(lower);
|
|
87
|
+
const withoutNamespace = normalizedReference.includes('.')
|
|
88
|
+
? normalizedReference.split('.').pop() || normalizedReference
|
|
89
|
+
: normalizedReference;
|
|
90
|
+
keys.add(withoutNamespace.toLowerCase());
|
|
91
|
+
return Array.from(keys);
|
|
92
|
+
}
|
|
93
|
+
function buildEntityTypeLookup(entityTypes) {
|
|
94
|
+
const lookup = new Map();
|
|
95
|
+
for (const [entityTypeKey, entityType] of Object.entries(entityTypes || {})) {
|
|
96
|
+
const keys = new Set([
|
|
97
|
+
...buildTypeReferenceKeys(entityTypeKey),
|
|
98
|
+
...buildTypeReferenceKeys(entityType.fullTypeName),
|
|
99
|
+
...buildTypeReferenceKeys(entityType.typeName),
|
|
100
|
+
]);
|
|
101
|
+
for (const key of keys) {
|
|
102
|
+
if (!lookup.has(key)) {
|
|
103
|
+
lookup.set(key, entityType);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return lookup;
|
|
108
|
+
}
|
|
109
|
+
function resolveEntityType(entityTypeLookup, typeReference) {
|
|
110
|
+
const keys = buildTypeReferenceKeys(typeReference);
|
|
111
|
+
for (const key of keys) {
|
|
112
|
+
const found = entityTypeLookup.get(key);
|
|
113
|
+
if (found)
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function resolveLookupPath(entitySetName, serviceResourceMap) {
|
|
119
|
+
var _a;
|
|
120
|
+
const serviceResource = serviceResourceMap.get(entitySetName);
|
|
121
|
+
const resourceUrl = String((_a = serviceResource === null || serviceResource === void 0 ? void 0 : serviceResource.url) !== null && _a !== void 0 ? _a : '').trim();
|
|
122
|
+
if (!resourceUrl)
|
|
123
|
+
return `/${entitySetName}`;
|
|
124
|
+
return resourceUrl.startsWith('/') ? resourceUrl : `/${resourceUrl}`;
|
|
125
|
+
}
|
|
126
|
+
function buildServiceResourceNameLookup(resources) {
|
|
127
|
+
var _a;
|
|
128
|
+
const lookup = new Map();
|
|
129
|
+
for (const resource of resources || []) {
|
|
130
|
+
const resourceName = String((_a = resource === null || resource === void 0 ? void 0 : resource.name) !== null && _a !== void 0 ? _a : '').trim();
|
|
131
|
+
if (!resourceName)
|
|
132
|
+
continue;
|
|
133
|
+
const lower = resourceName.toLowerCase();
|
|
134
|
+
if (!lookup.has(lower)) {
|
|
135
|
+
lookup.set(lower, resourceName);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return lookup;
|
|
139
|
+
}
|
|
140
|
+
function resolveServiceResourceNameForEntityType(entityType, serviceResourceNameLookup) {
|
|
141
|
+
var _a;
|
|
142
|
+
const baseNames = [
|
|
143
|
+
String((_a = entityType === null || entityType === void 0 ? void 0 : entityType.typeName) !== null && _a !== void 0 ? _a : '').trim(),
|
|
144
|
+
normalizeSingularName(entityType === null || entityType === void 0 ? void 0 : entityType.typeName),
|
|
145
|
+
normalizeEntityTypeName(entityType === null || entityType === void 0 ? void 0 : entityType.fullTypeName),
|
|
146
|
+
normalizeSingularName(normalizeEntityTypeName(entityType === null || entityType === void 0 ? void 0 : entityType.fullTypeName)),
|
|
147
|
+
].filter(Boolean);
|
|
148
|
+
const candidates = new Set();
|
|
149
|
+
for (const baseName of baseNames) {
|
|
150
|
+
candidates.add(baseName);
|
|
151
|
+
candidates.add(normalizeSingularName(baseName));
|
|
152
|
+
candidates.add(normalizePluralName(baseName));
|
|
153
|
+
}
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
const matched = serviceResourceNameLookup.get(String(candidate).toLowerCase());
|
|
156
|
+
if (matched)
|
|
157
|
+
return matched;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
function deduceMandatoryNavigationByTarget(metadataDocument, resources) {
|
|
162
|
+
var _a, _b, _c, _d;
|
|
163
|
+
if (!metadataDocument)
|
|
164
|
+
return {};
|
|
165
|
+
const entityTypeLookup = buildEntityTypeLookup(metadataDocument.entityTypes || {});
|
|
166
|
+
const serviceResourceMap = new Map();
|
|
167
|
+
for (const resource of resources) {
|
|
168
|
+
const resourceName = String((_a = resource === null || resource === void 0 ? void 0 : resource.name) !== null && _a !== void 0 ? _a : '').trim();
|
|
169
|
+
if (resourceName) {
|
|
170
|
+
serviceResourceMap.set(resourceName, resource);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const serviceResourceNameLookup = buildServiceResourceNameLookup(resources);
|
|
174
|
+
const entitySetsByEntityType = new Map();
|
|
175
|
+
for (const [entitySetName, entitySet] of Object.entries(metadataDocument.entitySets || {})) {
|
|
176
|
+
const typeKeys = buildTypeReferenceKeys(entitySet === null || entitySet === void 0 ? void 0 : entitySet.entityType);
|
|
177
|
+
if (typeKeys.length === 0)
|
|
178
|
+
continue;
|
|
179
|
+
for (const typeKey of typeKeys) {
|
|
180
|
+
const list = entitySetsByEntityType.get(typeKey) || [];
|
|
181
|
+
if (!list.includes(entitySetName)) {
|
|
182
|
+
list.push(entitySetName);
|
|
183
|
+
entitySetsByEntityType.set(typeKey, list);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const mandatoryByTarget = {};
|
|
188
|
+
const parentContexts = [];
|
|
189
|
+
const parentTypeKeysCovered = new Set();
|
|
190
|
+
for (const [parentEntitySetName, parentEntitySet] of Object.entries(metadataDocument.entitySets || {})) {
|
|
191
|
+
const parentType = resolveEntityType(entityTypeLookup, parentEntitySet.entityType);
|
|
192
|
+
if (!parentType)
|
|
193
|
+
continue;
|
|
194
|
+
parentContexts.push({
|
|
195
|
+
parentEntitySetName,
|
|
196
|
+
parentType,
|
|
197
|
+
navigationBindings: parentEntitySet.navigationBindings || {},
|
|
198
|
+
});
|
|
199
|
+
for (const typeKey of buildTypeReferenceKeys(parentEntitySet.entityType)) {
|
|
200
|
+
parentTypeKeysCovered.add(typeKey);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const entityType of Object.values(metadataDocument.entityTypes || {})) {
|
|
204
|
+
const typeKeys = [
|
|
205
|
+
...buildTypeReferenceKeys(entityType.fullTypeName),
|
|
206
|
+
...buildTypeReferenceKeys(entityType.typeName),
|
|
207
|
+
];
|
|
208
|
+
const alreadyCovered = typeKeys.some(typeKey => parentTypeKeysCovered.has(typeKey));
|
|
209
|
+
if (alreadyCovered)
|
|
210
|
+
continue;
|
|
211
|
+
if (!Array.isArray(entityType.navigationProperties) || entityType.navigationProperties.length === 0) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const parentEntitySetName = resolveServiceResourceNameForEntityType(entityType, serviceResourceNameLookup);
|
|
215
|
+
if (!parentEntitySetName)
|
|
216
|
+
continue;
|
|
217
|
+
parentContexts.push({
|
|
218
|
+
parentEntitySetName,
|
|
219
|
+
parentType: entityType,
|
|
220
|
+
navigationBindings: {},
|
|
221
|
+
});
|
|
222
|
+
for (const typeKey of typeKeys) {
|
|
223
|
+
parentTypeKeysCovered.add(typeKey);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const { parentEntitySetName, parentType, navigationBindings } of parentContexts) {
|
|
227
|
+
const parentParamName = toLowerCamelCase(parentType.typeName) ||
|
|
228
|
+
toLowerCamelCase(normalizeSingularName(parentEntitySetName)) ||
|
|
229
|
+
toLowerCamelCase(parentEntitySetName);
|
|
230
|
+
if (!parentParamName)
|
|
231
|
+
continue;
|
|
232
|
+
for (const navProperty of parentType.navigationProperties || []) {
|
|
233
|
+
if (!navProperty.containsTarget)
|
|
234
|
+
continue;
|
|
235
|
+
const targetNames = new Set();
|
|
236
|
+
const directBoundTarget = navigationBindings === null || navigationBindings === void 0 ? void 0 : navigationBindings[navProperty.name];
|
|
237
|
+
if (directBoundTarget) {
|
|
238
|
+
targetNames.add(directBoundTarget);
|
|
239
|
+
}
|
|
240
|
+
const navTypeKeys = buildTypeReferenceKeys(navProperty.type);
|
|
241
|
+
if (navTypeKeys.length > 0) {
|
|
242
|
+
const typeTargets = navTypeKeys.flatMap(typeKey => entitySetsByEntityType.get(typeKey) || []);
|
|
243
|
+
for (const targetName of typeTargets) {
|
|
244
|
+
targetNames.add(targetName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const targetEntitySetName of targetNames) {
|
|
248
|
+
const targetList = mandatoryByTarget[targetEntitySetName] || [];
|
|
249
|
+
const exists = targetList.some(item => item.name.toLowerCase() === parentParamName.toLowerCase());
|
|
250
|
+
if (exists)
|
|
251
|
+
continue;
|
|
252
|
+
targetList.push({
|
|
253
|
+
name: parentParamName,
|
|
254
|
+
lookupEntitySet: parentEntitySetName,
|
|
255
|
+
lookupPath: resolveLookupPath(parentEntitySetName, serviceResourceMap),
|
|
256
|
+
lookupValueField: (_b = parentType.keyProperties) === null || _b === void 0 ? void 0 : _b[0],
|
|
257
|
+
lookupLabelField: ((_c = parentType.stringProperties) === null || _c === void 0 ? void 0 : _c.find(prop => /name/i.test(prop))) || ((_d = parentType.stringProperties) === null || _d === void 0 ? void 0 : _d[0]),
|
|
258
|
+
});
|
|
259
|
+
mandatoryByTarget[targetEntitySetName] = targetList;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return mandatoryByTarget;
|
|
264
|
+
}
|
|
265
|
+
function buildMandatoryNavigationParameters(resource, mandatoryByTarget) {
|
|
266
|
+
var _a;
|
|
267
|
+
const resourceName = String((_a = resource === null || resource === void 0 ? void 0 : resource.name) !== null && _a !== void 0 ? _a : '').trim();
|
|
268
|
+
if (!resourceName)
|
|
269
|
+
return [];
|
|
270
|
+
const mandatoryTargets = mandatoryByTarget[resourceName] || [];
|
|
271
|
+
const mandatoryParameters = [];
|
|
272
|
+
const seenNames = new Set();
|
|
273
|
+
for (const mandatoryTarget of mandatoryTargets) {
|
|
274
|
+
const normalizedName = mandatoryTarget.name.toLowerCase();
|
|
275
|
+
if (seenNames.has(normalizedName))
|
|
276
|
+
continue;
|
|
277
|
+
const description = mandatoryTarget.lookupEntitySet
|
|
278
|
+
? `Required navigation parameter deduced from OData metadata (lookup: ${mandatoryTarget.lookupEntitySet})`
|
|
279
|
+
: 'Required navigation parameter deduced from OData metadata';
|
|
280
|
+
mandatoryParameters.push({
|
|
281
|
+
name: mandatoryTarget.name,
|
|
282
|
+
in: 'query',
|
|
283
|
+
dataType: 'string',
|
|
284
|
+
required: true,
|
|
285
|
+
description,
|
|
286
|
+
odataLookupPath: mandatoryTarget.lookupPath,
|
|
287
|
+
odataLookupEntitySet: mandatoryTarget.lookupEntitySet,
|
|
288
|
+
odataLookupValueField: mandatoryTarget.lookupValueField,
|
|
289
|
+
odataLookupLabelField: mandatoryTarget.lookupLabelField,
|
|
290
|
+
});
|
|
291
|
+
seenNames.add(normalizedName);
|
|
292
|
+
}
|
|
293
|
+
return mandatoryParameters;
|
|
294
|
+
}
|
|
295
|
+
function createODataResourceEndpoints(resource, mandatoryByTarget) {
|
|
296
|
+
var _a;
|
|
297
|
+
const path = normalizeEndpointPath(resource.url);
|
|
298
|
+
if (!path)
|
|
299
|
+
return [];
|
|
300
|
+
const summary = resource.name || resource.url || path;
|
|
301
|
+
const descriptionKind = String((_a = resource.kind) !== null && _a !== void 0 ? _a : '').trim();
|
|
302
|
+
const methods = inferMethods(resource.kind);
|
|
303
|
+
const mandatoryNavigationParameters = buildMandatoryNavigationParameters(resource, mandatoryByTarget);
|
|
304
|
+
return methods.map(method => {
|
|
305
|
+
const parameters = [...mandatoryNavigationParameters];
|
|
306
|
+
if (method === 'POST') {
|
|
307
|
+
parameters.push({
|
|
308
|
+
name: 'body',
|
|
309
|
+
in: 'body',
|
|
310
|
+
dataType: 'object',
|
|
311
|
+
contentType: 'application/json',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
method,
|
|
316
|
+
path,
|
|
317
|
+
summary,
|
|
318
|
+
description: descriptionKind ? `OData ${descriptionKind}` : 'OData resource',
|
|
319
|
+
parameters,
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function analyseODataDefinition(doc, endpointUrl, metadataDocumentXml) {
|
|
324
|
+
var _a;
|
|
325
|
+
const resources = Array.isArray(doc === null || doc === void 0 ? void 0 : doc.value) ? doc.value : [];
|
|
326
|
+
const categoriesByName = new Map();
|
|
327
|
+
const metadataDocument = metadataDocumentXml ? (0, oDataMetadataParser_1.parseODataMetadataDocument)(metadataDocumentXml) : null;
|
|
328
|
+
const mandatoryByTarget = deduceMandatoryNavigationByTarget(metadataDocument, resources);
|
|
329
|
+
for (const resource of resources) {
|
|
330
|
+
const endpoints = createODataResourceEndpoints(resource, mandatoryByTarget);
|
|
331
|
+
if (endpoints.length === 0)
|
|
332
|
+
continue;
|
|
333
|
+
const categoryName = String((_a = resource.kind) !== null && _a !== void 0 ? _a : 'Resources').trim() || 'Resources';
|
|
334
|
+
const existingEndpoints = categoriesByName.get(categoryName) || [];
|
|
335
|
+
existingEndpoints.push(...endpoints);
|
|
336
|
+
categoriesByName.set(categoryName, existingEndpoints);
|
|
337
|
+
}
|
|
338
|
+
const metadataEndpoint = {
|
|
339
|
+
method: 'GET',
|
|
340
|
+
path: '/$metadata',
|
|
341
|
+
summary: '$metadata',
|
|
342
|
+
description: 'OData service metadata',
|
|
343
|
+
parameters: [],
|
|
344
|
+
};
|
|
345
|
+
const metadataCategory = categoriesByName.get('Metadata') || [];
|
|
346
|
+
metadataCategory.push(metadataEndpoint);
|
|
347
|
+
categoriesByName.set('Metadata', metadataCategory);
|
|
348
|
+
const serviceRoot = normalizeServiceRoot(doc === null || doc === void 0 ? void 0 : doc['@odata.context'], endpointUrl);
|
|
349
|
+
const servers = serviceRoot ? [{ url: serviceRoot }] : [];
|
|
350
|
+
return {
|
|
351
|
+
categories: Array.from(categoriesByName.entries()).map(([name, endpoints]) => ({
|
|
352
|
+
name,
|
|
353
|
+
endpoints,
|
|
354
|
+
})),
|
|
355
|
+
servers,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
exports.analyseODataDefinition = analyseODataDefinition;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { analyseODataDefinition } = require('./oDataAdapter');
|
|
2
|
+
function findEndpoint(apiInfo, path, method = 'GET') {
|
|
3
|
+
return apiInfo.categories
|
|
4
|
+
.flatMap(category => category.endpoints)
|
|
5
|
+
.find(endpoint => endpoint.path === path && endpoint.method === method);
|
|
6
|
+
}
|
|
7
|
+
test('deduces mandatory company parameter for customers and items from ContainsTarget metadata', () => {
|
|
8
|
+
const serviceDocument = {
|
|
9
|
+
'@odata.context': 'https://example/odata/$metadata',
|
|
10
|
+
value: [
|
|
11
|
+
{ name: 'companies', kind: 'EntitySet', url: 'companies' },
|
|
12
|
+
{ name: 'customers', kind: 'EntitySet', url: 'customers' },
|
|
13
|
+
{ name: 'items', kind: 'EntitySet', url: 'items' },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
const metadataXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
17
|
+
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
|
|
18
|
+
<edmx:DataServices>
|
|
19
|
+
<Schema Namespace="Microsoft.NAV" Alias="NAV" xmlns="http://docs.oasis-open.org/odata/ns/edm">
|
|
20
|
+
<EntityType Name="company">
|
|
21
|
+
<Key><PropertyRef Name="id"/></Key>
|
|
22
|
+
<Property Name="id" Type="Edm.Guid"/>
|
|
23
|
+
<Property Name="displayName" Type="Edm.String"/>
|
|
24
|
+
<NavigationProperty Name="customers" Type="Collection(NAV.customer)" ContainsTarget="true" />
|
|
25
|
+
<NavigationProperty Name="items" Type="Collection(NAV.item)" ContainsTarget="true" />
|
|
26
|
+
</EntityType>
|
|
27
|
+
<EntityType Name="customer">
|
|
28
|
+
<Property Name="id" Type="Edm.Guid"/>
|
|
29
|
+
</EntityType>
|
|
30
|
+
<EntityType Name="item">
|
|
31
|
+
<Property Name="id" Type="Edm.Guid"/>
|
|
32
|
+
</EntityType>
|
|
33
|
+
<EntityContainer Name="default">
|
|
34
|
+
<EntitySet Name="companies" EntityType="NAV.company">
|
|
35
|
+
<NavigationPropertyBinding Path="customers" Target="customers"/>
|
|
36
|
+
<NavigationPropertyBinding Path="items" Target="items"/>
|
|
37
|
+
</EntitySet>
|
|
38
|
+
<EntitySet Name="customers" EntityType="NAV.customer"/>
|
|
39
|
+
<EntitySet Name="items" EntityType="NAV.item"/>
|
|
40
|
+
</EntityContainer>
|
|
41
|
+
</Schema>
|
|
42
|
+
</edmx:DataServices>
|
|
43
|
+
</edmx:Edmx>`;
|
|
44
|
+
const apiInfo = analyseODataDefinition(serviceDocument, 'https://example/odata', metadataXml);
|
|
45
|
+
const customersGet = findEndpoint(apiInfo, '/customers', 'GET');
|
|
46
|
+
const itemsGet = findEndpoint(apiInfo, '/items', 'GET');
|
|
47
|
+
expect(customersGet).toBeDefined();
|
|
48
|
+
expect(itemsGet).toBeDefined();
|
|
49
|
+
const customersCompany = customersGet.parameters.find(param => param.name === 'company');
|
|
50
|
+
const itemsCompany = itemsGet.parameters.find(param => param.name === 'company');
|
|
51
|
+
expect(customersCompany).toBeDefined();
|
|
52
|
+
expect(customersCompany.required).toBe(true);
|
|
53
|
+
expect(customersCompany.in).toBe('query');
|
|
54
|
+
expect(customersCompany.odataLookupEntitySet).toBe('companies');
|
|
55
|
+
expect(customersCompany.odataLookupPath).toBe('/companies');
|
|
56
|
+
expect(itemsCompany).toBeDefined();
|
|
57
|
+
expect(itemsCompany.required).toBe(true);
|
|
58
|
+
expect(itemsCompany.in).toBe('query');
|
|
59
|
+
expect(itemsCompany.odataLookupEntitySet).toBe('companies');
|
|
60
|
+
expect(itemsCompany.odataLookupPath).toBe('/companies');
|
|
61
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.oDataDriver = void 0;
|
|
4
|
+
const restAuthTools_1 = require("./restAuthTools");
|
|
5
|
+
const restDriverBase_1 = require("./restDriverBase");
|
|
6
|
+
function resolveServiceRoot(contextUrl, fallbackUrl) {
|
|
7
|
+
const safeFallback = String(fallbackUrl !== null && fallbackUrl !== void 0 ? fallbackUrl : '').trim();
|
|
8
|
+
if (typeof contextUrl === 'string' && contextUrl.trim()) {
|
|
9
|
+
try {
|
|
10
|
+
const resolved = new URL(contextUrl.trim(), safeFallback || undefined);
|
|
11
|
+
resolved.hash = '';
|
|
12
|
+
resolved.search = '';
|
|
13
|
+
resolved.pathname = resolved.pathname.replace(/\/$metadata$/i, '');
|
|
14
|
+
const url = resolved.toString();
|
|
15
|
+
return url.endsWith('/') ? url : `${url}/`;
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
// ignore, fallback below
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return safeFallback.endsWith('/') ? safeFallback : `${safeFallback}/`;
|
|
22
|
+
}
|
|
23
|
+
async function loadODataServiceDocument(dbhan) {
|
|
24
|
+
var _a;
|
|
25
|
+
if (!((_a = dbhan === null || dbhan === void 0 ? void 0 : dbhan.connection) === null || _a === void 0 ? void 0 : _a.apiServerUrl1)) {
|
|
26
|
+
throw new Error('DBGM-00000 OData endpoint URL is not configured');
|
|
27
|
+
}
|
|
28
|
+
const response = await dbhan.axios.get(dbhan.connection.apiServerUrl1, {
|
|
29
|
+
headers: (0, restAuthTools_1.buildRestAuthHeaders)(dbhan.connection.restAuth),
|
|
30
|
+
});
|
|
31
|
+
const document = response === null || response === void 0 ? void 0 : response.data;
|
|
32
|
+
if (!document || typeof document !== 'object') {
|
|
33
|
+
throw new Error('DBGM-00000 OData service document is empty or invalid');
|
|
34
|
+
}
|
|
35
|
+
if (!document['@odata.context']) {
|
|
36
|
+
throw new Error('DBGM-00000 OData service document does not contain @odata.context');
|
|
37
|
+
}
|
|
38
|
+
return document;
|
|
39
|
+
}
|
|
40
|
+
function getODataVersion(document) {
|
|
41
|
+
var _a;
|
|
42
|
+
const contextUrl = String((_a = document === null || document === void 0 ? void 0 : document['@odata.context']) !== null && _a !== void 0 ? _a : '').trim();
|
|
43
|
+
const versionMatch = contextUrl.match(/\/v(\d+(?:\.\d+)*)\/$metadata$/i);
|
|
44
|
+
if (versionMatch === null || versionMatch === void 0 ? void 0 : versionMatch[1])
|
|
45
|
+
return versionMatch[1];
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
exports.oDataDriver = {
|
|
50
|
+
...restDriverBase_1.apiDriverBase,
|
|
51
|
+
engine: 'odata@rest',
|
|
52
|
+
title: 'OData - REST',
|
|
53
|
+
databaseEngineTypes: ['rest', 'odata'],
|
|
54
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" fill="#f9a000"/><rect x="12" y="12" width="47" height="12" fill="#ffffff"/><rect x="69" y="12" width="47" height="12" fill="#ffffff"/><rect x="12" y="37" width="47" height="12" fill="#ffffff"/><rect x="69" y="37" width="47" height="12" fill="#ffffff"/><rect x="12" y="62" width="47" height="12" fill="#ffffff"/><rect x="69" y="62" width="47" height="12" fill="#ffffff"/><rect x="69" y="87" width="47" height="12" fill="#ffffff"/><circle cx="35" cy="102" r="20" fill="#e6e6e6"/></svg>',
|
|
55
|
+
apiServerUrl1Label: 'OData Service URL',
|
|
56
|
+
showConnectionField: (field, values) => {
|
|
57
|
+
if (restDriverBase_1.apiDriverBase.showAuthConnectionField(field, values))
|
|
58
|
+
return true;
|
|
59
|
+
if (field === 'apiServerUrl1')
|
|
60
|
+
return true;
|
|
61
|
+
return false;
|
|
62
|
+
},
|
|
63
|
+
beforeConnectionSave: connection => ({
|
|
64
|
+
...connection,
|
|
65
|
+
singleDatabase: true,
|
|
66
|
+
defaultDatabase: '_api_database_',
|
|
67
|
+
}),
|
|
68
|
+
async connect(connection) {
|
|
69
|
+
return {
|
|
70
|
+
connection,
|
|
71
|
+
client: null,
|
|
72
|
+
database: '_api_database_',
|
|
73
|
+
axios: connection.axios,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
async getVersion(dbhan) {
|
|
77
|
+
const document = await loadODataServiceDocument(dbhan);
|
|
78
|
+
const resourcesCount = Array.isArray(document === null || document === void 0 ? void 0 : document.value) ? document.value.length : 0;
|
|
79
|
+
const odataVersion = getODataVersion(document);
|
|
80
|
+
return {
|
|
81
|
+
version: odataVersion || 'OData',
|
|
82
|
+
versionText: `OData${odataVersion ? ` ${odataVersion}` : ''}, ${resourcesCount} resources`,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseODataMetadataDocument = void 0;
|
|
4
|
+
function decodeXmlEntities(value) {
|
|
5
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, "'")
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/&/g, '&');
|
|
11
|
+
}
|
|
12
|
+
function parseXmlAttributes(attributesText) {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
const attributes = {};
|
|
15
|
+
const regex = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
16
|
+
let match = regex.exec(attributesText || '');
|
|
17
|
+
while (match) {
|
|
18
|
+
const rawName = match[1];
|
|
19
|
+
const localName = rawName.includes(':') ? rawName.split(':').pop() || rawName : rawName;
|
|
20
|
+
const rawValue = (_b = (_a = match[3]) !== null && _a !== void 0 ? _a : match[4]) !== null && _b !== void 0 ? _b : '';
|
|
21
|
+
const decoded = decodeXmlEntities(rawValue);
|
|
22
|
+
attributes[rawName] = decoded;
|
|
23
|
+
attributes[localName] = decoded;
|
|
24
|
+
match = regex.exec(attributesText || '');
|
|
25
|
+
}
|
|
26
|
+
return attributes;
|
|
27
|
+
}
|
|
28
|
+
function extractXmlElements(xml, elementName) {
|
|
29
|
+
const elements = [];
|
|
30
|
+
const fullTagRegex = new RegExp(`<(?:[A-Za-z_][A-Za-z0-9_.-]*:)?${elementName}\\b([^>]*)>([\\s\\S]*?)<\\/(?:[A-Za-z_][A-Za-z0-9_.-]*:)?${elementName}>`, 'gi');
|
|
31
|
+
const selfClosingRegex = new RegExp(`<(?:[A-Za-z_][A-Za-z0-9_.-]*:)?${elementName}\\b([^>]*)\\/>`, 'gi');
|
|
32
|
+
let fullMatch = fullTagRegex.exec(xml || '');
|
|
33
|
+
while (fullMatch) {
|
|
34
|
+
elements.push({
|
|
35
|
+
attributes: parseXmlAttributes(fullMatch[1] || ''),
|
|
36
|
+
innerXml: fullMatch[2] || '',
|
|
37
|
+
});
|
|
38
|
+
fullMatch = fullTagRegex.exec(xml || '');
|
|
39
|
+
}
|
|
40
|
+
let selfClosingMatch = selfClosingRegex.exec(xml || '');
|
|
41
|
+
while (selfClosingMatch) {
|
|
42
|
+
elements.push({
|
|
43
|
+
attributes: parseXmlAttributes(selfClosingMatch[1] || ''),
|
|
44
|
+
innerXml: '',
|
|
45
|
+
});
|
|
46
|
+
selfClosingMatch = selfClosingRegex.exec(xml || '');
|
|
47
|
+
}
|
|
48
|
+
return elements;
|
|
49
|
+
}
|
|
50
|
+
function toBoolAttribute(value) {
|
|
51
|
+
return String(value !== null && value !== void 0 ? value : '').trim().toLowerCase() === 'true';
|
|
52
|
+
}
|
|
53
|
+
function normalizeEntitySetName(value) {
|
|
54
|
+
const input = String(value !== null && value !== void 0 ? value : '').trim();
|
|
55
|
+
if (!input)
|
|
56
|
+
return '';
|
|
57
|
+
const noContainer = input.includes('/') ? input.split('/').pop() || '' : input;
|
|
58
|
+
return noContainer.includes('.') ? noContainer.split('.').pop() || noContainer : noContainer;
|
|
59
|
+
}
|
|
60
|
+
function parseODataMetadataDocument(metadataXml) {
|
|
61
|
+
const schemas = extractXmlElements(metadataXml || '', 'Schema');
|
|
62
|
+
const entityTypes = {};
|
|
63
|
+
const entitySets = {};
|
|
64
|
+
for (const schema of schemas) {
|
|
65
|
+
const namespace = String(schema.attributes.Namespace || '').trim();
|
|
66
|
+
for (const entityTypeNode of extractXmlElements(schema.innerXml, 'EntityType')) {
|
|
67
|
+
const typeName = String(entityTypeNode.attributes.Name || '').trim();
|
|
68
|
+
if (!typeName)
|
|
69
|
+
continue;
|
|
70
|
+
const fullTypeName = namespace ? `${namespace}.${typeName}` : typeName;
|
|
71
|
+
const keyProperties = [];
|
|
72
|
+
const stringProperties = [];
|
|
73
|
+
const navigationProperties = [];
|
|
74
|
+
for (const keyNode of extractXmlElements(entityTypeNode.innerXml, 'Key')) {
|
|
75
|
+
for (const propRef of extractXmlElements(keyNode.innerXml, 'PropertyRef')) {
|
|
76
|
+
const keyName = String(propRef.attributes.Name || '').trim();
|
|
77
|
+
if (keyName && !keyProperties.includes(keyName)) {
|
|
78
|
+
keyProperties.push(keyName);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const propertyNode of extractXmlElements(entityTypeNode.innerXml, 'Property')) {
|
|
83
|
+
const propName = String(propertyNode.attributes.Name || '').trim();
|
|
84
|
+
const propType = String(propertyNode.attributes.Type || '').trim();
|
|
85
|
+
if (propName && /^Edm\.String$/i.test(propType)) {
|
|
86
|
+
stringProperties.push(propName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const navNode of extractXmlElements(entityTypeNode.innerXml, 'NavigationProperty')) {
|
|
90
|
+
const navName = String(navNode.attributes.Name || '').trim();
|
|
91
|
+
if (!navName)
|
|
92
|
+
continue;
|
|
93
|
+
navigationProperties.push({
|
|
94
|
+
name: navName,
|
|
95
|
+
type: String(navNode.attributes.Type || '').trim(),
|
|
96
|
+
containsTarget: toBoolAttribute(navNode.attributes.ContainsTarget),
|
|
97
|
+
nullable: navNode.attributes.Nullable === undefined ? true : toBoolAttribute(navNode.attributes.Nullable),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
entityTypes[fullTypeName] = {
|
|
101
|
+
typeName,
|
|
102
|
+
fullTypeName,
|
|
103
|
+
keyProperties,
|
|
104
|
+
stringProperties,
|
|
105
|
+
navigationProperties,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
for (const entitySetNode of extractXmlElements(schema.innerXml, 'EntitySet')) {
|
|
109
|
+
const setName = String(entitySetNode.attributes.Name || '').trim();
|
|
110
|
+
const entityType = String(entitySetNode.attributes.EntityType || '').trim();
|
|
111
|
+
if (!setName || !entityType)
|
|
112
|
+
continue;
|
|
113
|
+
const navigationBindings = {};
|
|
114
|
+
for (const bindingNode of extractXmlElements(entitySetNode.innerXml, 'NavigationPropertyBinding')) {
|
|
115
|
+
const path = String(bindingNode.attributes.Path || '').trim();
|
|
116
|
+
const target = normalizeEntitySetName(bindingNode.attributes.Target);
|
|
117
|
+
if (!path || !target)
|
|
118
|
+
continue;
|
|
119
|
+
navigationBindings[path] = target;
|
|
120
|
+
const pathLastSegment = path.split('/').pop();
|
|
121
|
+
if (pathLastSegment && !navigationBindings[pathLastSegment]) {
|
|
122
|
+
navigationBindings[pathLastSegment] = target;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
entitySets[setName] = {
|
|
126
|
+
name: setName,
|
|
127
|
+
entityType,
|
|
128
|
+
navigationBindings,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
entityTypes,
|
|
134
|
+
entitySets,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
exports.parseODataMetadataDocument = parseODataMetadataDocument;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OpenAPIV3_1 } from 'openapi-types';
|
|
2
|
+
import { RestApiDefinition } from './restApiDef';
|
|
3
|
+
/**
|
|
4
|
+
* Converts an OpenAPI v3.1 document into a simplified REST API definition
|
|
5
|
+
* Organizes endpoints by tags into categories
|
|
6
|
+
*/
|
|
7
|
+
export declare function analyseOpenApiDefinition(doc: OpenAPIV3_1.Document): RestApiDefinition;
|