@balena/pinejs 21.0.5-build-renovate-serve-static-2-x-d57cb45f323dd0921480deb4c400b718bb24a1d2-1 → 21.1.0-build-odata-metadata-json-395a55cb54e7b9ce0960ab93aad5f69d6c0e0462-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/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +13 -8
- package/CHANGELOG.md +3 -3
- package/VERSION +1 -1
- package/out/odata-metadata/odata-metadata-generator.d.ts +61 -1
- package/out/odata-metadata/odata-metadata-generator.js +186 -97
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/odata-metadata/open-api-specification-generator.d.ts +2 -0
- package/out/odata-metadata/open-api-specification-generator.js +50 -0
- package/out/odata-metadata/open-api-specification-generator.js.map +1 -0
- package/out/sbvr-api/permissions.d.ts +4 -0
- package/out/sbvr-api/permissions.js +1 -1
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.js +18 -3
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/uri-parser.js +1 -1
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/package.json +5 -3
- package/src/odata-metadata/odata-metadata-generator.ts +344 -126
- package/src/odata-metadata/open-api-specification-generator.ts +98 -0
- package/src/sbvr-api/permissions.ts +2 -2
- package/src/sbvr-api/sbvr-utils.ts +33 -3
- package/src/sbvr-api/uri-parser.ts +1 -1
- package/typings/odata-openapi.d.ts +6 -0
@@ -6,6 +6,199 @@ import type {
|
|
6
6
|
import type { SbvrType } from '@balena/sbvr-types';
|
7
7
|
import { sbvrTypes } from '../sbvr-api/sbvr-utils.js';
|
8
8
|
import { version } from '../config-loader/env.js';
|
9
|
+
import type { PermissionLookup } from '../sbvr-api/permissions.js';
|
10
|
+
|
11
|
+
// OData JSON v4 CSDL Vocabulary constants
|
12
|
+
// http://docs.oasis-open.org/odata/odata-vocabularies/v4.0/odata-vocabularies-v4.0.html
|
13
|
+
const odataVocabularyReferences: ODataCsdlV4References = {
|
14
|
+
'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.json':
|
15
|
+
{
|
16
|
+
$Include: [
|
17
|
+
{
|
18
|
+
$Namespace: 'Org.OData.Core.V1',
|
19
|
+
$Alias: 'Core',
|
20
|
+
'@Core.DefaultNamespace': true,
|
21
|
+
},
|
22
|
+
],
|
23
|
+
},
|
24
|
+
'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.json':
|
25
|
+
{
|
26
|
+
$Include: [
|
27
|
+
{
|
28
|
+
$Namespace: 'Org.OData.Measures.V1',
|
29
|
+
$Alias: 'Measures',
|
30
|
+
},
|
31
|
+
],
|
32
|
+
},
|
33
|
+
'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.json':
|
34
|
+
{
|
35
|
+
$Include: [
|
36
|
+
{
|
37
|
+
$Namespace: 'Org.OData.Aggregation.V1',
|
38
|
+
$Alias: 'Aggregation',
|
39
|
+
},
|
40
|
+
],
|
41
|
+
},
|
42
|
+
'https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.json':
|
43
|
+
{
|
44
|
+
$Include: [
|
45
|
+
{
|
46
|
+
$Namespace: 'Org.OData.Capabilities.V1',
|
47
|
+
$Alias: 'Capabilities',
|
48
|
+
},
|
49
|
+
],
|
50
|
+
},
|
51
|
+
};
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Odata Common Schema Definition Language JSON format
|
55
|
+
* http://docs.oasis-open.org/odata/odata-json-format/v4.0/odata-json-format-v4.0.html
|
56
|
+
*/
|
57
|
+
|
58
|
+
type ODataCsdlV4References = {
|
59
|
+
[URI: string]: {
|
60
|
+
$Include: Array<{
|
61
|
+
$Namespace: string;
|
62
|
+
$Alias: string;
|
63
|
+
[annotation: string]: string | boolean;
|
64
|
+
}>;
|
65
|
+
};
|
66
|
+
};
|
67
|
+
|
68
|
+
type ODataCsdlV4BaseProperty = {
|
69
|
+
[annotation: string]: string | boolean | undefined;
|
70
|
+
$Type?: string;
|
71
|
+
$Nullable?: boolean;
|
72
|
+
};
|
73
|
+
|
74
|
+
type ODataCsdlV4StructuralProperty = ODataCsdlV4BaseProperty & {
|
75
|
+
$Kind?: 'Property'; // This member SHOULD be omitted to reduce document size.
|
76
|
+
};
|
77
|
+
|
78
|
+
type ODataCsdlV4NavigationProperty = ODataCsdlV4BaseProperty & {
|
79
|
+
$Kind: 'NavigationProperty';
|
80
|
+
$Partner?: string;
|
81
|
+
};
|
82
|
+
|
83
|
+
type ODataCsdlV4Property =
|
84
|
+
| ODataCsdlV4BaseProperty
|
85
|
+
| ODataCsdlV4StructuralProperty
|
86
|
+
| ODataCsdlV4NavigationProperty;
|
87
|
+
|
88
|
+
type ODataCsdlV4EntityType = {
|
89
|
+
$Kind: 'EntityType';
|
90
|
+
$Key: string[];
|
91
|
+
[property: string]: true | string[] | string | ODataCsdlV4Property;
|
92
|
+
};
|
93
|
+
|
94
|
+
type ODataCsdlV4EntityContainerEntries = {
|
95
|
+
// $Collection: true;
|
96
|
+
$Type: string;
|
97
|
+
[property: string]: true | string | ODataCapabilitiesUDIRRestrictionsMethod;
|
98
|
+
};
|
99
|
+
|
100
|
+
type ODataCsdlV4Entities = {
|
101
|
+
[resource: string]: ODataCsdlV4EntityType;
|
102
|
+
};
|
103
|
+
|
104
|
+
type ODataCsdlV4EntityContainer = {
|
105
|
+
$Kind: 'EntityContainer';
|
106
|
+
'@Capabilities.BatchSupported'?: boolean;
|
107
|
+
[resourceOrAnnotation: string]:
|
108
|
+
| boolean
|
109
|
+
| string
|
110
|
+
| ODataCsdlV4EntityContainerEntries
|
111
|
+
| undefined;
|
112
|
+
};
|
113
|
+
|
114
|
+
type ODataCsdlV4Schema = {
|
115
|
+
$Alias: string;
|
116
|
+
'@Core.DefaultNamespace': true;
|
117
|
+
[resource: string]:
|
118
|
+
| string
|
119
|
+
| boolean
|
120
|
+
| ODataCsdlV4EntityContainer
|
121
|
+
| ODataCsdlV4EntityType;
|
122
|
+
};
|
123
|
+
|
124
|
+
type OdataCsdlV4 = {
|
125
|
+
$Version: string;
|
126
|
+
$Reference: ODataCsdlV4References;
|
127
|
+
$EntityContainer: string;
|
128
|
+
[schema: string]: string | ODataCsdlV4References | ODataCsdlV4Schema;
|
129
|
+
};
|
130
|
+
|
131
|
+
type PreparedPermissionsLookup = {
|
132
|
+
[vocabulary: string]: {
|
133
|
+
[resource: string]: {
|
134
|
+
read: boolean;
|
135
|
+
create: boolean;
|
136
|
+
update: boolean;
|
137
|
+
delete: boolean;
|
138
|
+
};
|
139
|
+
};
|
140
|
+
};
|
141
|
+
|
142
|
+
type PreparedAbstractModel = {
|
143
|
+
vocabulary: string;
|
144
|
+
abstractSqlModel: AbstractSqlModel;
|
145
|
+
preparedPermissionLookup: PreparedPermissionsLookup;
|
146
|
+
};
|
147
|
+
|
148
|
+
type ODataCapabilitiesUDIRRestrictionsMethod =
|
149
|
+
| { Updatable: boolean }
|
150
|
+
| { Deletable: boolean }
|
151
|
+
| { Insertable: boolean }
|
152
|
+
| { Readable: boolean }
|
153
|
+
| { Filterable: boolean };
|
154
|
+
|
155
|
+
const restrictionsLookup = (
|
156
|
+
method: keyof PreparedPermissionsLookup[string][string] | 'all',
|
157
|
+
value: boolean,
|
158
|
+
) => {
|
159
|
+
const lookup = {
|
160
|
+
update: {
|
161
|
+
'@Capabilities.UpdateRestrictions': {
|
162
|
+
Updatable: value,
|
163
|
+
},
|
164
|
+
'@Capabilities.FilterRestrictions': {
|
165
|
+
Filterable: true,
|
166
|
+
},
|
167
|
+
},
|
168
|
+
delete: {
|
169
|
+
'@Capabilities.DeleteRestrictions': {
|
170
|
+
Deletable: value,
|
171
|
+
},
|
172
|
+
'@Capabilities.FilterRestrictions': {
|
173
|
+
Filterable: true,
|
174
|
+
},
|
175
|
+
},
|
176
|
+
create: {
|
177
|
+
'@Capabilities.InsertRestrictions': {
|
178
|
+
Insertable: value,
|
179
|
+
},
|
180
|
+
},
|
181
|
+
read: {
|
182
|
+
'@Capabilities.ReadRestrictions': {
|
183
|
+
Readable: value,
|
184
|
+
},
|
185
|
+
'@Capabilities.FilterRestrictions': {
|
186
|
+
Filterable: true,
|
187
|
+
},
|
188
|
+
},
|
189
|
+
};
|
190
|
+
|
191
|
+
if (method === 'all') {
|
192
|
+
return {
|
193
|
+
...lookup['update'],
|
194
|
+
...lookup['delete'],
|
195
|
+
...lookup['create'],
|
196
|
+
...lookup['read'],
|
197
|
+
};
|
198
|
+
} else {
|
199
|
+
return lookup[method] ?? {};
|
200
|
+
}
|
201
|
+
};
|
9
202
|
|
10
203
|
const getResourceName = (resourceName: string): string =>
|
11
204
|
resourceName
|
@@ -14,17 +207,25 @@ const getResourceName = (resourceName: string): string =>
|
|
14
207
|
.join('__');
|
15
208
|
|
16
209
|
const forEachUniqueTable = <T>(
|
17
|
-
model:
|
18
|
-
callback: (
|
210
|
+
model: PreparedAbstractModel,
|
211
|
+
callback: (
|
212
|
+
tableName: string,
|
213
|
+
table: AbstractSqlTable & { referenceScheme: string },
|
214
|
+
) => T,
|
19
215
|
): T[] => {
|
20
216
|
const usedTableNames: { [tableName: string]: true } = {};
|
21
217
|
|
22
218
|
const result = [];
|
23
|
-
|
219
|
+
|
220
|
+
for (const key of Object.keys(model.abstractSqlModel.tables).sort()) {
|
221
|
+
const table = model.abstractSqlModel.tables[key] as AbstractSqlTable & {
|
222
|
+
referenceScheme: string;
|
223
|
+
};
|
24
224
|
if (
|
25
225
|
typeof table !== 'string' &&
|
26
226
|
!table.primitive &&
|
27
|
-
!usedTableNames[table.name]
|
227
|
+
!usedTableNames[table.name] &&
|
228
|
+
model.preparedPermissionLookup
|
28
229
|
) {
|
29
230
|
usedTableNames[table.name] = true;
|
30
231
|
result.push(callback(key, table));
|
@@ -33,9 +234,49 @@ const forEachUniqueTable = <T>(
|
|
33
234
|
return result;
|
34
235
|
};
|
35
236
|
|
237
|
+
/**
|
238
|
+
* parsing dictionary of vocabulary.resource.operation permissions string
|
239
|
+
* into dictionary of resource to operation for later lookup
|
240
|
+
*/
|
241
|
+
|
242
|
+
const preparePermissionsLookup = (
|
243
|
+
permissionLookup: PermissionLookup,
|
244
|
+
): PreparedPermissionsLookup => {
|
245
|
+
const resourcesAndOps: PreparedPermissionsLookup = {};
|
246
|
+
|
247
|
+
for (const resourceOpsAuths of Object.keys(permissionLookup)) {
|
248
|
+
const [vocabulary, resource, rule] = resourceOpsAuths.split('.');
|
249
|
+
resourcesAndOps[vocabulary] ??= {};
|
250
|
+
resourcesAndOps[vocabulary][resource] ??= {
|
251
|
+
['read']: false,
|
252
|
+
['create']: false,
|
253
|
+
['update']: false,
|
254
|
+
['delete']: false,
|
255
|
+
};
|
256
|
+
|
257
|
+
if (rule === 'all' || (resource === 'all' && rule === undefined)) {
|
258
|
+
resourcesAndOps[vocabulary][resource] = {
|
259
|
+
['read']: true,
|
260
|
+
['create']: true,
|
261
|
+
['update']: true,
|
262
|
+
['delete']: true,
|
263
|
+
};
|
264
|
+
} else if (
|
265
|
+
rule === 'read' ||
|
266
|
+
rule === 'create' ||
|
267
|
+
rule === 'update' ||
|
268
|
+
rule === 'delete'
|
269
|
+
) {
|
270
|
+
resourcesAndOps[vocabulary][resource][rule] = true;
|
271
|
+
}
|
272
|
+
}
|
273
|
+
return resourcesAndOps;
|
274
|
+
};
|
275
|
+
|
36
276
|
export const generateODataMetadata = (
|
37
277
|
vocabulary: string,
|
38
278
|
abstractSqlModel: AbstractSqlModel,
|
279
|
+
permissionsLookup?: PermissionLookup,
|
39
280
|
) => {
|
40
281
|
const complexTypes: { [fieldType: string]: string } = {};
|
41
282
|
const resolveDataType = (fieldType: keyof typeof sbvrTypes): string => {
|
@@ -50,132 +291,109 @@ export const generateODataMetadata = (
|
|
50
291
|
return sbvrTypes[fieldType].types.odata.name;
|
51
292
|
};
|
52
293
|
|
53
|
-
const
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
294
|
+
const prepPermissionsLookup = permissionsLookup
|
295
|
+
? preparePermissionsLookup(permissionsLookup)
|
296
|
+
: {};
|
297
|
+
|
298
|
+
const model: PreparedAbstractModel = {
|
299
|
+
vocabulary,
|
300
|
+
abstractSqlModel,
|
301
|
+
preparedPermissionLookup: prepPermissionsLookup,
|
302
|
+
};
|
303
|
+
|
304
|
+
const metaBalenaEntries: ODataCsdlV4Entities = {};
|
305
|
+
const entityContainer: ODataCsdlV4EntityContainer = {
|
306
|
+
$Kind: 'EntityContainer',
|
307
|
+
'@Capabilities.KeyAsSegmentSupported': false,
|
308
|
+
};
|
309
|
+
|
310
|
+
forEachUniqueTable(model, (_key, { idField, name: resourceName, fields }) => {
|
62
311
|
resourceName = getResourceName(resourceName);
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
312
|
+
// no path nor entity when permissions not contain resource
|
313
|
+
const permissions: PreparedPermissionsLookup[string][string] =
|
314
|
+
model?.preparedPermissionLookup?.['resource']?.['all'] ??
|
315
|
+
model?.preparedPermissionLookup?.[model.vocabulary]?.['all'] ??
|
316
|
+
model?.preparedPermissionLookup?.[model.vocabulary]?.[resourceName];
|
317
|
+
|
318
|
+
if (!permissions) {
|
319
|
+
return;
|
320
|
+
}
|
321
|
+
|
322
|
+
const uniqueTable: ODataCsdlV4EntityType = {
|
323
|
+
$Kind: 'EntityType',
|
324
|
+
$Key: [idField],
|
325
|
+
};
|
326
|
+
|
327
|
+
fields
|
328
|
+
.filter(({ dataType }) => dataType !== 'ForeignKey')
|
329
|
+
.map(({ dataType, fieldName, required }) => {
|
330
|
+
dataType = resolveDataType(dataType as keyof typeof sbvrTypes);
|
331
|
+
fieldName = getResourceName(fieldName);
|
332
|
+
|
333
|
+
uniqueTable[fieldName] = {
|
334
|
+
$Type: dataType,
|
335
|
+
$Nullable: !required,
|
336
|
+
'@Core.Computed':
|
337
|
+
fieldName === 'created_at' || fieldName === 'modified_at'
|
338
|
+
? true
|
339
|
+
: false,
|
340
|
+
};
|
341
|
+
});
|
342
|
+
|
343
|
+
fields
|
344
|
+
.filter(
|
345
|
+
({ dataType, references }) =>
|
346
|
+
dataType === 'ForeignKey' && references != null,
|
347
|
+
)
|
348
|
+
.map(({ fieldName, references, required }) => {
|
349
|
+
const { resourceName: referencedResource } = references!;
|
350
|
+
const referencedResourceName =
|
351
|
+
model.abstractSqlModel.tables[referencedResource]?.name;
|
352
|
+
const typeReference = referencedResourceName || referencedResource;
|
353
|
+
|
354
|
+
fieldName = getResourceName(fieldName);
|
355
|
+
uniqueTable[fieldName] = {
|
356
|
+
$Kind: 'NavigationProperty',
|
357
|
+
$Partner: resourceName,
|
358
|
+
$Nullable: !required,
|
359
|
+
$Type: vocabulary + '.' + getResourceName(typeReference),
|
360
|
+
};
|
361
|
+
});
|
362
|
+
|
363
|
+
metaBalenaEntries[resourceName] = uniqueTable;
|
364
|
+
|
365
|
+
let entityCon: ODataCsdlV4EntityContainerEntries = {
|
366
|
+
$Collection: true,
|
367
|
+
$Type: vocabulary + '.' + resourceName,
|
368
|
+
};
|
369
|
+
for (const [resKey, resValue] of Object.entries(permissions) as Array<
|
370
|
+
[keyof PreparedPermissionsLookup[string][string], boolean]
|
371
|
+
>) {
|
372
|
+
entityCon = { ...entityCon, ...restrictionsLookup(resKey, resValue) };
|
74
373
|
}
|
374
|
+
|
375
|
+
entityContainer[resourceName] = entityCon;
|
75
376
|
});
|
76
377
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
<PropertyRef Name="${idField}" />
|
97
|
-
</Key>
|
98
|
-
|
99
|
-
` +
|
100
|
-
fields
|
101
|
-
.filter(({ dataType }) => dataType !== 'ForeignKey')
|
102
|
-
.map(({ dataType, fieldName, required }) => {
|
103
|
-
dataType = resolveDataType(dataType as keyof typeof sbvrTypes);
|
104
|
-
fieldName = getResourceName(fieldName);
|
105
|
-
return `<Property Name="${fieldName}" Type="${dataType}" Nullable="${!required}" />`;
|
106
|
-
})
|
107
|
-
.join('\n') +
|
108
|
-
'\n' +
|
109
|
-
fields
|
110
|
-
.filter(
|
111
|
-
({ dataType, references }) =>
|
112
|
-
dataType === 'ForeignKey' && references != null,
|
113
|
-
)
|
114
|
-
.map(({ fieldName, references }) => {
|
115
|
-
const { resourceName: referencedResource } = references!;
|
116
|
-
fieldName = getResourceName(fieldName);
|
117
|
-
return `<NavigationProperty Name="${fieldName}" Relationship="${vocabulary}.${
|
118
|
-
resourceName + referencedResource
|
119
|
-
}" FromRole="${resourceName}" ToRole="${referencedResource}" />`;
|
120
|
-
})
|
121
|
-
.join('\n') +
|
122
|
-
'\n' +
|
123
|
-
`
|
124
|
-
</EntityType>`
|
125
|
-
);
|
126
|
-
},
|
127
|
-
).join('\n\n') +
|
128
|
-
associations
|
129
|
-
.map(({ name, ends }) => {
|
130
|
-
name = getResourceName(name);
|
131
|
-
return (
|
132
|
-
`<Association Name="${name}">` +
|
133
|
-
'\n\t' +
|
134
|
-
ends
|
135
|
-
.map(
|
136
|
-
({ resourceName, cardinality }) =>
|
137
|
-
`<End Role="${resourceName}" Type="${vocabulary}.${resourceName}" Multiplicity="${cardinality}" />`,
|
138
|
-
)
|
139
|
-
.join('\n\t') +
|
140
|
-
'\n' +
|
141
|
-
`</Association>`
|
142
|
-
);
|
143
|
-
})
|
144
|
-
.join('\n') +
|
145
|
-
`
|
146
|
-
<EntityContainer Name="${vocabulary}Service" m:IsDefaultEntityContainer="true">
|
147
|
-
|
148
|
-
` +
|
149
|
-
forEachUniqueTable(model, (_key, { name: resourceName }) => {
|
150
|
-
resourceName = getResourceName(resourceName);
|
151
|
-
return `<EntitySet Name="${resourceName}" EntityType="${vocabulary}.${resourceName}" />`;
|
152
|
-
}).join('\n') +
|
153
|
-
'\n' +
|
154
|
-
associations
|
155
|
-
.map(({ name, ends }) => {
|
156
|
-
name = getResourceName(name);
|
157
|
-
return (
|
158
|
-
`<AssociationSet Name="${name}" Association="${vocabulary}.${name}">` +
|
159
|
-
'\n\t' +
|
160
|
-
ends
|
161
|
-
.map(
|
162
|
-
({ resourceName }) =>
|
163
|
-
`<End Role="${resourceName}" EntitySet="${vocabulary}.${resourceName}" />`,
|
164
|
-
)
|
165
|
-
.join('\n\t') +
|
166
|
-
`
|
167
|
-
</AssociationSet>`
|
168
|
-
);
|
169
|
-
})
|
170
|
-
.join('\n') +
|
171
|
-
`
|
172
|
-
</EntityContainer>` +
|
173
|
-
Object.values(complexTypes).join('\n') +
|
174
|
-
`
|
175
|
-
</Schema>
|
176
|
-
</edmx:DataServices>
|
177
|
-
</edmx:Edmx>`
|
178
|
-
);
|
378
|
+
const odataCsdl: OdataCsdlV4 = {
|
379
|
+
// needs to be === '4.0' as > '4.0' in csdl2openapi will switch to drop the `$` query parameter prefix for eg $top, $skip as it became optional in OData V4.01
|
380
|
+
$Version: '3.0',
|
381
|
+
$EntityContainer: vocabulary + '.ODataApi',
|
382
|
+
$Reference: odataVocabularyReferences,
|
383
|
+
[vocabulary]: {
|
384
|
+
// schema
|
385
|
+
$Alias: vocabulary,
|
386
|
+
'@Core.DefaultNamespace': true,
|
387
|
+
'@Core.Description': `OpenAPI specification for PineJS served SBVR datamodel: ${vocabulary}`,
|
388
|
+
'@Core.LongDescription':
|
389
|
+
'Auto-Genrated OpenAPI specification by utilizing OData CSDL to OpenAPI spec transformer.',
|
390
|
+
'@Core.SchemaVersion': version,
|
391
|
+
...metaBalenaEntries,
|
392
|
+
['ODataApi']: entityContainer,
|
393
|
+
},
|
394
|
+
};
|
395
|
+
|
396
|
+
return odataCsdl;
|
179
397
|
};
|
180
398
|
|
181
399
|
generateODataMetadata.version = version;
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import * as odataMetadata from 'odata-openapi';
|
2
|
+
import type { generateODataMetadata } from './odata-metadata-generator.js';
|
3
|
+
import _ from 'lodash';
|
4
|
+
|
5
|
+
export const generateODataMetadataAsOpenApi = (
|
6
|
+
odataCsdl: ReturnType<typeof generateODataMetadata>,
|
7
|
+
versionBasePathUrl = '',
|
8
|
+
hostname = '',
|
9
|
+
) => {
|
10
|
+
// console.log(`odataCsdl:${JSON.stringify(odataCsdl, null, 2)}`);
|
11
|
+
const openAPIJson: any = odataMetadata.csdl2openapi(odataCsdl, {
|
12
|
+
scheme: 'https',
|
13
|
+
host: hostname,
|
14
|
+
basePath: versionBasePathUrl,
|
15
|
+
diagram: false,
|
16
|
+
maxLevels: 5,
|
17
|
+
});
|
18
|
+
|
19
|
+
/**
|
20
|
+
* Manual rewriting OpenAPI specification to delete OData default functionality
|
21
|
+
* that is not implemented in Pinejs yet or is based on PineJs implements OData V3.
|
22
|
+
*
|
23
|
+
* Rewrite odata body response schema properties from `value: ` to `d: `
|
24
|
+
* Currently pinejs is returning `d: `
|
25
|
+
* https://www.odata.org/documentation/odata-version-2-0/json-format/ (6. Representing Collections of Entries)
|
26
|
+
* https://www.odata.org/documentation/odata-version-3-0/json-verbose-format/ (6.1 Response body)
|
27
|
+
*
|
28
|
+
* New v4 odata specifies the body response with `value: `
|
29
|
+
* http://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#sec_IndividualPropertyorOperationRespons
|
30
|
+
*
|
31
|
+
*
|
32
|
+
* Currently pinejs does not implement a $count=true query parameter as this would return the count of all rows returned as an additional parameter.
|
33
|
+
* This was not part of OData V3 and is new for OData V4. As the odata-openapi converte is opionionated on V4 the parameter is put into the schema.
|
34
|
+
* Until this is in parity with OData V4 pinejs needs to cleanup the `odata.count` key from the response schema put in by `csdl2openapi`
|
35
|
+
*
|
36
|
+
*
|
37
|
+
* Used oasis translator generates openapi according to v4 spec (`value: `)
|
38
|
+
*
|
39
|
+
* Unfortunantely odata-openapi does not export the genericFilter object.
|
40
|
+
* Using hardcoded generic filter description as used in odata-openapi code.
|
41
|
+
* Putting the genericFilter into the #/components/parameters/filter to reference it from paths
|
42
|
+
*
|
43
|
+
* */
|
44
|
+
const parameters = openAPIJson?.components?.parameters;
|
45
|
+
parameters['filter'] = {
|
46
|
+
name: '$filter',
|
47
|
+
description:
|
48
|
+
'Filter items by property values, see [Filtering](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_SystemQueryOptionfilter)',
|
49
|
+
in: 'query',
|
50
|
+
schema: {
|
51
|
+
type: 'string',
|
52
|
+
},
|
53
|
+
};
|
54
|
+
|
55
|
+
for (const idx of Object.keys(openAPIJson.paths)) {
|
56
|
+
// rewrite `value: ` to `d: `
|
57
|
+
const properties =
|
58
|
+
openAPIJson?.paths[idx]?.get?.responses?.['200']?.content?.[
|
59
|
+
'application/json'
|
60
|
+
]?.schema?.properties;
|
61
|
+
if (properties?.value) {
|
62
|
+
properties['d'] = properties.value;
|
63
|
+
delete properties.value;
|
64
|
+
}
|
65
|
+
|
66
|
+
// cleanup the `odata.count` key from the response schema
|
67
|
+
if (properties?.['@odata.count']) {
|
68
|
+
delete properties['@odata.count'];
|
69
|
+
}
|
70
|
+
|
71
|
+
// copy over 'delete' and 'patch' action from single entiy path
|
72
|
+
// odata-openAPI converter does not support collection delete and collection update.
|
73
|
+
// pinejs support collection delete and update with $filter parameter
|
74
|
+
const entityCollectionPath = openAPIJson?.paths[idx];
|
75
|
+
const singleEntityPath = openAPIJson?.paths[idx + '({id})'];
|
76
|
+
if (entityCollectionPath != null && singleEntityPath != null) {
|
77
|
+
const genericFilterParameterRef = {
|
78
|
+
$ref: '#/components/parameters/filter',
|
79
|
+
};
|
80
|
+
for (const action of ['delete', 'patch']) {
|
81
|
+
entityCollectionPath[action] = _.clone(singleEntityPath?.[action]);
|
82
|
+
if (entityCollectionPath[action]) {
|
83
|
+
entityCollectionPath[action]['parameters'] = [
|
84
|
+
genericFilterParameterRef,
|
85
|
+
];
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
// cleanup $batch path as pinejs does not implement it.
|
92
|
+
// http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-protocol.html#sec_BatchRequests
|
93
|
+
if (openAPIJson?.paths['/$batch']) {
|
94
|
+
delete openAPIJson.paths['/$batch'];
|
95
|
+
}
|
96
|
+
|
97
|
+
return openAPIJson;
|
98
|
+
};
|
@@ -328,7 +328,7 @@ const namespaceRelationships = (
|
|
328
328
|
}
|
329
329
|
};
|
330
330
|
|
331
|
-
type PermissionLookup = Dictionary<true | string[]>;
|
331
|
+
export type PermissionLookup = Dictionary<true | string[]>;
|
332
332
|
|
333
333
|
const getPermissionsLookup = env.createCache(
|
334
334
|
'permissionsLookup',
|
@@ -1707,7 +1707,7 @@ const getGuestPermissions = memoize(
|
|
1707
1707
|
{ promise: true },
|
1708
1708
|
);
|
1709
1709
|
|
1710
|
-
const getReqPermissions = async (
|
1710
|
+
export const getReqPermissions = async (
|
1711
1711
|
req: PermissionReq,
|
1712
1712
|
odataBinds: ODataBinds = [] as any as ODataBinds,
|
1713
1713
|
) => {
|
@@ -48,6 +48,7 @@ import * as asyncMigrator from '../migrator/async.js';
|
|
48
48
|
import * as syncMigrator from '../migrator/sync.js';
|
49
49
|
import { generateODataMetadata } from '../odata-metadata/odata-metadata-generator.js';
|
50
50
|
import { importSBVR } from '../server-glue/sbvr-loader.js';
|
51
|
+
import { generateODataMetadataAsOpenApi } from '../odata-metadata/open-api-specification-generator.js';
|
51
52
|
|
52
53
|
import type DevModel from './dev.js';
|
53
54
|
const devModel = await importSBVR('./dev.sbvr', import.meta);
|
@@ -111,6 +112,7 @@ import {
|
|
111
112
|
type MigrationExecutionResult,
|
112
113
|
setExecutedMigrations,
|
113
114
|
} from '../migrator/utils.js';
|
115
|
+
import { metadataEndpoints } from './uri-parser.js';
|
114
116
|
|
115
117
|
const LF2AbstractSQLTranslator = LF2AbstractSQL.createTranslator(sbvrTypes);
|
116
118
|
const LF2AbstractSQLTranslatorVersion = `${LF2AbstractSQLVersion}+${sbvrTypesVersion}`;
|
@@ -1335,7 +1337,10 @@ const runODataRequest = (req: Express.Request, vocabulary: string) => {
|
|
1335
1337
|
})
|
1336
1338
|
: resolveSynonym($request);
|
1337
1339
|
|
1338
|
-
if (
|
1340
|
+
if (
|
1341
|
+
abstractSqlModel.tables[resolvedResourceName] == null &&
|
1342
|
+
!metadataEndpoints.includes(resolvedResourceName)
|
1343
|
+
) {
|
1339
1344
|
throw new UnauthorizedError();
|
1340
1345
|
}
|
1341
1346
|
|
@@ -1838,10 +1843,35 @@ const respondGet = async (
|
|
1838
1843
|
return response;
|
1839
1844
|
} else {
|
1840
1845
|
if (request.resourceName === '$metadata') {
|
1846
|
+
const permLookup = await permissions.getReqPermissions(req);
|
1847
|
+
const spec = generateODataMetadata(
|
1848
|
+
vocab,
|
1849
|
+
models[vocab].abstractSql,
|
1850
|
+
permLookup,
|
1851
|
+
);
|
1852
|
+
return {
|
1853
|
+
statusCode: 200,
|
1854
|
+
body: spec,
|
1855
|
+
headers: { 'content-type': 'application/json' },
|
1856
|
+
};
|
1857
|
+
} else if (request.resourceName === 'openapi.json') {
|
1858
|
+
// https://docs.oasis-open.org/odata/odata-openapi/v1.0/cn01/odata-openapi-v1.0-cn01.html#sec_ProvidingOASDocumentsforanODataServi
|
1859
|
+
// Following the OASIS OData to openapi translation guide the openapi.json is an independent resource
|
1860
|
+
const permLookup = await permissions.getReqPermissions(req);
|
1861
|
+
const spec = generateODataMetadata(
|
1862
|
+
vocab,
|
1863
|
+
models[vocab].abstractSql,
|
1864
|
+
permLookup,
|
1865
|
+
);
|
1866
|
+
const openApispec = generateODataMetadataAsOpenApi(
|
1867
|
+
spec,
|
1868
|
+
req.originalUrl.replace('openapi.json', ''),
|
1869
|
+
req.hostname,
|
1870
|
+
);
|
1841
1871
|
return {
|
1842
1872
|
statusCode: 200,
|
1843
|
-
body:
|
1844
|
-
headers: { 'content-type': '
|
1873
|
+
body: openApispec,
|
1874
|
+
headers: { 'content-type': 'application/json' },
|
1845
1875
|
};
|
1846
1876
|
} else {
|
1847
1877
|
// TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that
|
@@ -267,7 +267,7 @@ const memoizedOdata2AbstractSQL = (() => {
|
|
267
267
|
};
|
268
268
|
})();
|
269
269
|
|
270
|
-
export const metadataEndpoints = ['$metadata', '$serviceroot'];
|
270
|
+
export const metadataEndpoints = ['$metadata', '$serviceroot', 'openapi.json'];
|
271
271
|
|
272
272
|
export function parseOData(
|
273
273
|
b: UnparsedRequest & { _isChangeSet?: false },
|