Merge remote-tracking branch 'upstream/main' into statusCode
This commit is contained in:
Коммит
88597944f3
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/packages/odata/src/cli/odata-gen.js"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
{
|
||||
"editor.renderWhitespace": "all",
|
||||
"editor.tabSize": 2,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false,
|
||||
|
|
|
@ -1,9 +1,53 @@
|
|||
{
|
||||
"name": "@microsoft/overreact-root",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@algolia/autocomplete-core": {
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@microsoft/overreact-root",
|
||||
"workspaces": [
|
||||
"packages"
|
||||
],
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-beta.0",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.0",
|
||||
"@docusaurus/theme-live-codeblock": "^2.0.0-beta.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"clsx": "^1.1.1",
|
||||
"prism-react-renderer": "^1.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^2.0.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/preset-env": "^7.14.4",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.1",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"lerna": "^4.0.0",
|
||||
"prettier": "^2.3.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"regenerator-runtime": "^0.13.2",
|
||||
"underscore": "^1.10.2",
|
||||
"uuid": "^3.3.3",
|
||||
"webpack": "^5.38.1",
|
||||
"webpack-cli": "^4.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/autocomplete-core": {
|
||||
"version": "1.0.0-alpha.44",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.0.0-alpha.44.tgz",
|
||||
"integrity": "sha512-2iMXthldMIDXtlbg9omRKLgg1bLo2ZzINAEqwhNjUeyj1ceEyL1ck6FY0VnJpf2LsjmNthHCz2BuFk+nYUeDNA==",
|
||||
|
@ -27030,6 +27074,26 @@
|
|||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
@ -27089,3 +27153,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# `overreact-odata`
|
||||
|
||||
A typical usage of overreact within our team is to deal with [OData](https://www.odata.org/) from various service endpoints. Usually these endpoints already have pre-built schema packages available (such as `@bingads-webui/mca-odata-schemas`, and `@bingads-webui/campaign-odata-schemas`), from which we can extract useful information and generate overreact specs without having our developers write from scratch. Given each entity (e.g., `Activity`, `Ad`) have CRUD operations, as well as OData actions/functions attached, this could save a tremendous amount of manual effort.
|
||||
|
||||
## The Idea
|
||||
|
||||
To generate a spec for each OData model, we'll need to solve these problems:
|
||||
|
||||
1. How to create `dataPath` from OData model hierarchy.
|
||||
2. How to assign proper "Key" values to each level on hierarchy.
|
||||
3. How to identify from response which property is the "Key".
|
||||
|
||||
In overreact, the internal data structure ("store") is constructed from a schema tree, where each node has an associated `dataPath` to describe its location from root. Similarly, OData also organizes data using a tree-like structure, and provides navigation properties to locate specific data in the tree.
|
||||
|
||||
Consider an OData GET request to fetch an `Activity`. The URL would look like this:
|
||||
|
||||
> GET https://contoso.com/Customers(123)/Accounts(456)/Activities('789')
|
||||
|
||||
We can directly map `dataPath` from EDM hierarchy to `customer:account:activity`.
|
||||
|
||||
For OData actions/functions, a call to `https://contoso.com/Customers(123)/Accounts(456)/Default.FooBar()` will map to `customer:account:foo_bar`. Note that we converted the Pascal naming to Snake convention, and discarded the namespace "Default" in this case.
|
||||
|
||||
The "Key" values are used to identify which entity to use on each level in the hierarchy. Currently in overreact we have 2 options to select keys:
|
||||
|
||||
1. Using `locator.order` in `variables`. For example:
|
||||
```javascript
|
||||
const variables = {
|
||||
locator: {
|
||||
order: ['cid', 'aid', 'activityId'],
|
||||
descriptor: { cid: 123, aid: 456, activityId: '789' },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. Using `parentKeySelector` in request contract.
|
||||
|
||||
Unfortunately `parentKeySelector` only provides "Key" info for current value, as well as its "parent" key values. We'll loose info for levels that are higher than 2, so in our case we'll resort to using `order`.
|
||||
|
||||
Finally, we need to identify the "Key" property value from OData responses, as they are used to look up cached items in overreact store. Luckily it is usually specified in `$$ODataExtension.Key` from the OData schemas. It is an array value but for now we'll only leverage the first one:
|
||||
|
||||
```javascript
|
||||
const { $$ODataExtension } = entitySchema;
|
||||
|
||||
createResponseContract({
|
||||
// ...
|
||||
keySelector: r => r[$$ODataExtension.Key[0]],
|
||||
})
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Due to length limit, please visit [Working with OData](https://microsoft.github.io/overreact-core/blog/odata) for details on usage.
|
|
@ -21,7 +21,10 @@
|
|||
},
|
||||
"homepage": "https://github.com/microsoft/overreact-core#readme",
|
||||
"dependencies": {
|
||||
"@microsoft/overreact": "^0.1.0-alpha.8"
|
||||
"@microsoft/overreact": "^0.1.0-alpha.8",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.6.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"query-string": "^7.0.1",
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
const _ = require('lodash');
|
||||
const { generateODataSchema } = require('../schema/generate-odata-schema');
|
||||
const { exportSchemaModel } = require('../schema/export-schema-model');
|
||||
const { EDM } = require('../edm/core');
|
||||
const { resIdsPlugin } = require('../edm/resource-identifiers');
|
||||
const { defineConstProperty } = require('../edm/reflection');
|
||||
|
||||
(async () => {
|
||||
const namespaces = await generateODataSchema('http://ui.ads-int.microsoft.com/ODataApi/Mca/V1', {
|
||||
isByDefaultNullable(ref) {
|
||||
if (ref === 'Edm/String') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !ref.startsWith('Edm') && !ref.startsWith('Enum');
|
||||
},
|
||||
withEnumValue: false,
|
||||
});
|
||||
|
||||
const model = exportSchemaModel(namespaces);
|
||||
|
||||
const rootPropertyName = 'Customers';
|
||||
const rootPropertyModelName = 'Model/McaCustomer';
|
||||
|
||||
Object.keys(model || {}).forEach(key => {
|
||||
const schema = model[key];
|
||||
model[key] = {
|
||||
...schema,
|
||||
$$ref: key,
|
||||
};
|
||||
});
|
||||
|
||||
const edm = new EDM({
|
||||
schemas: {
|
||||
$ROOT: {
|
||||
$$ref: '$ROOT',
|
||||
type: 'object',
|
||||
properties: {
|
||||
[rootPropertyName]: {
|
||||
type: 'array',
|
||||
items: model[rootPropertyModelName],
|
||||
},
|
||||
},
|
||||
$$ODataExtension: {
|
||||
Name: '$ROOT',
|
||||
NavigationProperty: [
|
||||
rootPropertyName,
|
||||
],
|
||||
},
|
||||
},
|
||||
...model,
|
||||
},
|
||||
});
|
||||
|
||||
resIdsPlugin(edm);
|
||||
|
||||
const root = edm.types.resolve('$ROOT');
|
||||
|
||||
const rootResourceIdentifier = new root.ResourceIdentifier();
|
||||
defineConstProperty(edm, 'root', rootResourceIdentifier);
|
||||
defineConstProperty(edm, rootPropertyName, rootResourceIdentifier[rootPropertyName]);
|
||||
|
||||
console.log(edm);
|
||||
})();
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* the Entity Data Model module
|
||||
*/
|
||||
const typesPlugin = require('./types-plugin');
|
||||
const schemaPlugin = require('./schema-plugin');
|
||||
|
||||
class EDM {
|
||||
constructor(options) {
|
||||
typesPlugin(this);
|
||||
if (options) {
|
||||
schemaPlugin(this, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EDM,
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* the reflection module for property defintions
|
||||
*/
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* Define a const property
|
||||
* @param {Object} obj - the host object of the proprty
|
||||
* @param {String} name - the name of the property
|
||||
* @param {Object} value - the value of the property
|
||||
* @return {Object} the host object
|
||||
*/
|
||||
function defineConstProperty(obj, name, value) {
|
||||
return Object.defineProperty(obj, name, {
|
||||
value,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a produced property with a factory
|
||||
* @param {Object} obj - the host object of the proprty
|
||||
* @param {String} name - the name of the property
|
||||
* @param {Function} factory - the factory to produce the property value
|
||||
* @return {Object} the host object
|
||||
*/
|
||||
function defineProducedProperty(obj, name, factory) {
|
||||
return Object.defineProperty(obj, name, {
|
||||
get() {
|
||||
const value = factory.apply(obj);
|
||||
|
||||
defineConstProperty(obj, name, value);
|
||||
return value;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a produced property on a class
|
||||
* @param {Class} Class - the host class of the proprty
|
||||
* @param {String} name - the name of the property
|
||||
* @param {Function} factory - the factory to produce the property value
|
||||
* @return {Class} the host class
|
||||
*/
|
||||
function defineProducedPropertyOnClass(Class, name, factory) {
|
||||
const RAND_MAX = 65535;
|
||||
const className = Class.name || `Anony${_.random(0, RAND_MAX)}`;
|
||||
const symbol = `__${className}_${name}`;
|
||||
|
||||
Object.defineProperty(Class.prototype, name, {
|
||||
get() {
|
||||
if (!Object.prototype.hasOwnProperty.call(this, symbol)) {
|
||||
defineConstProperty(this, symbol, factory.apply(this));
|
||||
}
|
||||
return this[symbol];
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
return Class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a computed property whose value is computed each time the getter being called
|
||||
* @param {Object} obj - the host object of the proprty
|
||||
* @param {String} name - the name of the property
|
||||
* @param {Function} getter - the getter to compute the property value
|
||||
* @return {Object} the host object
|
||||
*/
|
||||
function defineComputedProperty(obj, name, getter) {
|
||||
return Object.defineProperty(obj, name, {
|
||||
get: getter,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Make the functions chainable with underscorejs
|
||||
_.mixin({
|
||||
defineConstProperty,
|
||||
defineProducedProperty,
|
||||
defineProducedPropertyOnClass,
|
||||
defineComputedProperty,
|
||||
});
|
||||
|
||||
/**
|
||||
* Detect whether or not a object has certain property without evaluation
|
||||
* @param {Object} obj - the host class of the proprty
|
||||
* @param {String} name - the name of the property
|
||||
* @return {Boolean} whether the property is defined
|
||||
*/
|
||||
function hasOwnProperty(obj, name) {
|
||||
return !!Object.getOwnPropertyDescriptor(obj, name);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
defineConstProperty,
|
||||
defineProducedProperty,
|
||||
defineProducedPropertyOnClass,
|
||||
defineComputedProperty,
|
||||
hasOwnProperty,
|
||||
};
|
|
@ -0,0 +1,163 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable no-mixed-operators */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
const _ = require('lodash');
|
||||
|
||||
/** Class representing a namespace */
|
||||
class Namespace {
|
||||
/**
|
||||
* Create a namespace
|
||||
* @param {Namespace|null} parent - The parent namespace
|
||||
*/
|
||||
constructor(parent) {
|
||||
this.map = {};
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an object by name
|
||||
* @param {String[]} segments - The name segments
|
||||
* @return {Object} The object registered with the given name
|
||||
*/
|
||||
resolve(segments) {
|
||||
const iterator = (ns, seg) => {
|
||||
if (ns && ns instanceof Namespace) {
|
||||
return ns.map[seg];
|
||||
}
|
||||
return ns;
|
||||
};
|
||||
|
||||
return _.reduce(segments, iterator, this)
|
||||
|| this.parent && this.parent.resolve(segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an object
|
||||
* @param {Object} obj - The object to be registered
|
||||
* @param {String[]} segments - The name segments for the object
|
||||
* @return {Void} Nothing to return
|
||||
*/
|
||||
register(obj, segments) {
|
||||
const key = segments.pop();
|
||||
const iterator = (ns, seg) => {
|
||||
if (ns instanceof Namespace) {
|
||||
if (_.isUndefined(ns.map[seg])) {
|
||||
ns.map[seg] = new Namespace(ns);
|
||||
}
|
||||
return ns.map[seg];
|
||||
}
|
||||
throw new Error(`There are conflicts when defining registry for ${segments.join('.')}`);
|
||||
};
|
||||
|
||||
const nsTarget = _.reduce(segments, iterator, this);
|
||||
|
||||
nsTarget.map[key] = obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all objects in this namespace
|
||||
* @param {Function} worker - The callback for each of the objects
|
||||
* @return {void} Nothing to return
|
||||
*/
|
||||
each(worker) {
|
||||
_.each(this.map, obj => {
|
||||
if (obj instanceof Namespace) {
|
||||
obj.each(worker);
|
||||
} else {
|
||||
worker(obj);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectWithName(obj) {
|
||||
return typeof obj.name === 'string';
|
||||
}
|
||||
|
||||
/** Class representing a namespaced registry */
|
||||
class Registry {
|
||||
/**
|
||||
* Create a registry
|
||||
*/
|
||||
constructor() {
|
||||
this.rootNamespace = new Namespace(null);
|
||||
this.qualifiedNames = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an object by name
|
||||
* @param {String} name - The name of the object
|
||||
* @param {String} [namespace=this.rootNamespace] - The base namespace to resolve against
|
||||
* @return {Object|null} The object registered with the name
|
||||
*/
|
||||
resolve(name, namespace) {
|
||||
const ns = namespace
|
||||
? this.rootNamespace.resolve(namespace.split('.'))
|
||||
: this.rootNamespace;
|
||||
const obj = ns instanceof Namespace ? ns.resolve(name.split('.')) : null;
|
||||
|
||||
return obj || null;
|
||||
}
|
||||
|
||||
resolveQualifiedName(name, namespace) {
|
||||
const obj = this.resolve(name, namespace);
|
||||
if (obj instanceof Namespace || !obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.qualifiedNames.get(obj) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an object
|
||||
* @param {Object} obj - The object to be registered
|
||||
* @param {String} [name=obj.name] - The qualified name for the object
|
||||
* @return {void} Nothing to return
|
||||
*/
|
||||
register(obj, name) {
|
||||
const realName = !name && isObjectWithName(obj) ? obj.name : name;
|
||||
if (typeof realName === 'undefined') {
|
||||
throw new Error(`name not passed in and obj ${JSON.stringify(obj)} doesn't have name property`);
|
||||
}
|
||||
this.rootNamespace.register(obj, realName.split('.'));
|
||||
this.qualifiedNames.set(obj, realName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all registered objects
|
||||
* @param {Function} worker - The callback for each of the objects
|
||||
* @return {void} Nothing to return
|
||||
*/
|
||||
each(worker) {
|
||||
this.rootNamespace.each(worker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get namespace from a qualified name
|
||||
* @param {String} name - The qualified name
|
||||
* @return {String} The namespace
|
||||
*/
|
||||
static getNamespace(name) {
|
||||
const segments = name.split('.');
|
||||
|
||||
segments.pop();
|
||||
return segments.join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short name from a qualified name
|
||||
* @param {String} name - The qualified name
|
||||
* @return {String} The short name
|
||||
*/
|
||||
static getShortName(name) {
|
||||
return _.last(name.split('.'));
|
||||
}
|
||||
|
||||
static getQualifiedName(name, namespace) {
|
||||
return namespace ? `${namespace}.${name}` : name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Registry,
|
||||
};
|
|
@ -0,0 +1,543 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const { Registry } = require('./registry');
|
||||
const {
|
||||
defineConstProperty,
|
||||
defineProducedPropertyOnClass,
|
||||
} = require('./reflection');
|
||||
|
||||
const url = require('./url-util');
|
||||
|
||||
/**
|
||||
* @param {EDM} edm - The EDM object to apply this plugin to
|
||||
* @return {Void} nothing to return
|
||||
*/
|
||||
function resIdsPlugin(edm) {
|
||||
if (edm.resourceIdentifiers) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name edm
|
||||
* @property {Registry} resourceIdentifiers - A registry of all resource-identifiers
|
||||
* @property {Class} resourceIdentifiers.Navigation - The base type of all navigations
|
||||
* @property {Class} resourceIdentifiers.PropertyNavigation - The navigation following a property
|
||||
* @property {Class} resourceIdentifiers.CastNavigation - The navigation following a type casting
|
||||
* @property {Class} resourceIdentifiers.WithKeyNavigation
|
||||
* - The navigation of selecting a single instance from an entity set
|
||||
* @property {Class} resourceIdentifiers.CallNavigation - The navigation of calling a callable
|
||||
* @property {Class} resourceIdentifiers.ResourceIdentifier
|
||||
* - The base type of all resource-identifiers
|
||||
*/
|
||||
defineConstProperty(edm, 'resourceIdentifiers', (() => {
|
||||
const resourceIdentifiers = new Registry();
|
||||
|
||||
/**
|
||||
* @class Navigation
|
||||
* @property {ResourceIdentifier} source - The source accessor navigated from
|
||||
*/
|
||||
class Navigation {
|
||||
constructor({
|
||||
source,
|
||||
}) {
|
||||
defineConstProperty(this, 'source', source);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return [...this.source.toJSON(), this.toSelfJSON()];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
toSelfJSON() {
|
||||
throw new Error('I\'m abstract');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PropertyNavigation
|
||||
* @extends Navigation
|
||||
* @property {Property} property - The property navigated with
|
||||
*/
|
||||
class PropertyNavigation extends Navigation {
|
||||
constructor({
|
||||
source,
|
||||
property,
|
||||
name,
|
||||
}) {
|
||||
super({
|
||||
source,
|
||||
});
|
||||
defineConstProperty(this, 'property', property);
|
||||
defineConstProperty(this, 'name', name);
|
||||
defineConstProperty(this, 'path', url.join(source.path, name));
|
||||
}
|
||||
|
||||
toSelfJSON() {
|
||||
return {
|
||||
type: 'property',
|
||||
name: this.name,
|
||||
};
|
||||
}
|
||||
|
||||
static defineOn(TypeResID, navigationProperties) {
|
||||
_.each(navigationProperties, (property, name) => {
|
||||
/**
|
||||
* Navigation property factory for the defined property
|
||||
* @memberof TypeResID#
|
||||
* @this TypeResID
|
||||
* @returns {ResourceIdentifier} The ResourceIdentifier to access the property
|
||||
*/
|
||||
function navPropFactory() {
|
||||
return new property.type.ResourceIdentifier({
|
||||
navigation: new PropertyNavigation({ source: this, property, name }),
|
||||
});
|
||||
}
|
||||
|
||||
defineProducedPropertyOnClass(TypeResID, name, navPropFactory);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CallableNavigation extends Navigation {
|
||||
constructor({
|
||||
source,
|
||||
name,
|
||||
}) {
|
||||
super({
|
||||
source,
|
||||
});
|
||||
defineConstProperty(this, 'name', name);
|
||||
defineConstProperty(this, 'path', url.join(source.path, name));
|
||||
}
|
||||
|
||||
static defineOn(TypeResID, callable) {
|
||||
function defineCallableOn(resID, callableTypes) {
|
||||
_.each(callableTypes, type => {
|
||||
/**
|
||||
* factory for the callables
|
||||
* @memberof resID#
|
||||
* @this resID
|
||||
* @returns {ResourceIdentifier} The ResourceIdentifier to access the property
|
||||
*/
|
||||
function factory() {
|
||||
return new type.ResourceIdentifier({
|
||||
navigation: new CallableNavigation({ source: this, name: type.callableName }),
|
||||
});
|
||||
}
|
||||
|
||||
defineProducedPropertyOnClass(resID, type.callableName, factory);
|
||||
});
|
||||
}
|
||||
defineCallableOn(TypeResID, callable.actions);
|
||||
defineCallableOn(TypeResID, callable.functions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class CastNavigation
|
||||
* @extends Navigation
|
||||
* @property {Type} type - The type casting to
|
||||
* @property {String} name - The type name used for casting
|
||||
*/
|
||||
class CastNavigation extends Navigation {
|
||||
constructor({
|
||||
source,
|
||||
type,
|
||||
name = type.name,
|
||||
}) {
|
||||
super({
|
||||
source,
|
||||
});
|
||||
|
||||
_.chain(this)
|
||||
.defineConstProperty('type', type)
|
||||
.defineConstProperty('name', name)
|
||||
.value();
|
||||
|
||||
defineConstProperty(this, 'path', url.join(source.path, name));
|
||||
}
|
||||
|
||||
toSelfJSON() {
|
||||
return {
|
||||
type: 'function',
|
||||
name: CastNavigation.navigationName,
|
||||
parameters: [this.name],
|
||||
};
|
||||
}
|
||||
|
||||
static get navigationName() {
|
||||
return '$cast';
|
||||
}
|
||||
|
||||
static defineOn(TypeResID) {
|
||||
/**
|
||||
* $cast navigation method of the TypeResID
|
||||
* @memberof TypeResID#
|
||||
* @this TypeResID
|
||||
* @param {String} name - The name of the subclass
|
||||
* @returns {TypeResID} The ResourceIdentifier to access the subclass object
|
||||
*/
|
||||
function navCastMethod(name) {
|
||||
const entityType = edm.types.resolve(name, this.type.namespace);
|
||||
const type = TypeResID.prototype.type instanceof edm.types.CollectionType
|
||||
? entityType.collectionType
|
||||
: entityType;
|
||||
const navigation = new CastNavigation({ source: this, type: entityType, name });
|
||||
|
||||
return new type.ResourceIdentifier({ navigation });
|
||||
}
|
||||
|
||||
defineConstProperty(TypeResID.prototype, CastNavigation.navigationName, navCastMethod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class WithKeyNavigation
|
||||
* @extends Navigation
|
||||
* @property {String|Number} key - The key of the selected entity
|
||||
*/
|
||||
class WithKeyNavigation extends Navigation {
|
||||
constructor({
|
||||
source,
|
||||
key,
|
||||
}) {
|
||||
super({
|
||||
source,
|
||||
});
|
||||
|
||||
defineConstProperty(this, 'key', key);
|
||||
defineConstProperty(this, 'path', (() => {
|
||||
// In case people pass a decimal string for a integer key
|
||||
// We cannot use parseInt directly, it would get a wrong number if the
|
||||
// key is beyond Number.MAX_SAFE_INTEGER
|
||||
if (_.isString(key)) {
|
||||
const keyType = _.chain(source)
|
||||
.result('type')
|
||||
.result('elementType')
|
||||
.result('keyProperty')
|
||||
.result('typeName')
|
||||
.value();
|
||||
|
||||
if (keyType === 'integer') {
|
||||
if (key.match(/^[+-]?(0|[1-9][0-9]*)$/)) {
|
||||
return `${source.path}(${key})`;
|
||||
}
|
||||
}
|
||||
if (keyType === 'string') {
|
||||
return `${source.path}('${key}')`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${source.path}(${JSON.stringify(key)})`;
|
||||
})());
|
||||
}
|
||||
|
||||
toSelfJSON() {
|
||||
return {
|
||||
type: 'function',
|
||||
name: WithKeyNavigation.navigationName,
|
||||
parameters: [this.key],
|
||||
};
|
||||
}
|
||||
|
||||
static get navigationName() {
|
||||
return '$withKey';
|
||||
}
|
||||
|
||||
static defineOn(TypeResID) {
|
||||
/**
|
||||
* $withKey navigation method of the TypeResID
|
||||
* @memberof TypeResID#
|
||||
* @this TypeResID
|
||||
* @param {String} key - The key of the element
|
||||
* @returns {ResourceIdentifier}
|
||||
* The ResourceIdentifier to access the element with the given key
|
||||
*/
|
||||
function navWithKeyMethod(key) {
|
||||
const navigation = new WithKeyNavigation({ source: this, key });
|
||||
|
||||
return new this.type.elementType.ResourceIdentifier({ navigation });
|
||||
}
|
||||
|
||||
defineConstProperty(
|
||||
TypeResID.prototype,
|
||||
WithKeyNavigation.navigationName,
|
||||
navWithKeyMethod,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class CallNavigation
|
||||
* @extends Navigation
|
||||
* @property {Object} parameters - The parameters for the function call
|
||||
*/
|
||||
class CallNavigation extends Navigation {
|
||||
constructor({
|
||||
source,
|
||||
parameters = {},
|
||||
}) {
|
||||
super({
|
||||
source,
|
||||
});
|
||||
defineConstProperty(this, 'parameters', parameters);
|
||||
|
||||
const path = source.type instanceof edm.types.ActionType
|
||||
? source.path : `${source.path}(${_.map(parameters, (value, name) => `${name}=${value}`).join(',')})`;
|
||||
|
||||
defineConstProperty(this, 'path', path);
|
||||
}
|
||||
|
||||
toSelfJSON() {
|
||||
return {
|
||||
type: 'function',
|
||||
name: CallNavigation.navigationName,
|
||||
// for callable, parameters are named
|
||||
parameters: [this.parameters],
|
||||
};
|
||||
}
|
||||
|
||||
static get navigationName() {
|
||||
return '$call';
|
||||
}
|
||||
|
||||
static defineOn(TypeResID) {
|
||||
/**
|
||||
* $call navigation method of the TypeResID
|
||||
* @memberof TypeResID#
|
||||
* @this TypeResID
|
||||
* @param {object} parameters - The parameters to call the callable
|
||||
* @return {ResourceIdentifier}
|
||||
* The ResourceIdentifier accessing the return value of the callable
|
||||
*/
|
||||
function navCallMethod(parameters) {
|
||||
const navigation = new CallNavigation({ source: this, parameters });
|
||||
|
||||
return new this.type.returnType.ResourceIdentifier({ navigation });
|
||||
}
|
||||
|
||||
defineConstProperty(TypeResID.prototype, CallNavigation.navigationName, navCallMethod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ResourceIdentifier
|
||||
* @property {Navigation} navigation
|
||||
* - The navigation back tracking the parent ResourceIdentifier
|
||||
*/
|
||||
class ResourceIdentifier {
|
||||
/**
|
||||
* Create a ResourceIdentifier
|
||||
* @param {Object} [options] - The constructor options
|
||||
* @param {Navigation} [options.navigation]
|
||||
* - The navigation back tracking the parent ResourceIdentifier
|
||||
* @return {Void} Nothing to return
|
||||
*/
|
||||
constructor({
|
||||
navigation,
|
||||
} = {}) {
|
||||
if (navigation) {
|
||||
defineConstProperty(this, 'navigation', navigation);
|
||||
defineConstProperty(this, 'path', navigation.path);
|
||||
} else {
|
||||
defineConstProperty(this, 'path', '');
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
if (this.navigation) {
|
||||
return this.navigation.toJSON();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
identifyEntitySet(json) {
|
||||
// eslint-disable-next-line
|
||||
let entitySet = this;
|
||||
|
||||
/* eslint guard-for-in: 0 */
|
||||
/* eslint no-restricted-syntax: 0 */
|
||||
for (const i in json) {
|
||||
const item = json[i];
|
||||
|
||||
if (item.type === 'property') {
|
||||
entitySet = entitySet[item.name];
|
||||
} else if (item.type === 'function') {
|
||||
entitySet = entitySet[item.name](...item.parameters);
|
||||
}
|
||||
|
||||
if (!entitySet) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return entitySet;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @class CollectionResourceIdentifier
|
||||
* @property {Navigation} navigation
|
||||
* - The navigation back tracking the parent ResourceIdentifier
|
||||
*/
|
||||
class CollectionResourceIdentifier extends ResourceIdentifier {
|
||||
/**
|
||||
* Create a ResourceIdentifier
|
||||
* @param {Object} [options] - The constructor options
|
||||
* @param {Navigation} [options.navigation]
|
||||
* - The navigation back tracking the parent ResourceIdentifier
|
||||
* @return {Void} Nothing to return
|
||||
*/
|
||||
constructor({
|
||||
navigation,
|
||||
} = {}) {
|
||||
super({ navigation });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* define ResourceIdentifier type for the given type
|
||||
* @param {Type} type - An instance of one of the meta types
|
||||
* @return {Class} the ResourceIdentifier class
|
||||
*/
|
||||
function resourceIdentifierForType(type) {
|
||||
// duck type, if the type has a "baseType", use its ResourceIdentifier as the super class
|
||||
const DefaultBase = type instanceof edm.types.CollectionType
|
||||
? CollectionResourceIdentifier
|
||||
: ResourceIdentifier;
|
||||
const Base = type.baseType ? type.baseType.ResourceIdentifier : DefaultBase;
|
||||
|
||||
const AccessorType = class extends Base {
|
||||
};
|
||||
|
||||
/**
|
||||
* @name resId
|
||||
* @type ResourceIdentifier
|
||||
* @property {Type} type - The type associated with the ResourceIdentifier
|
||||
*/
|
||||
defineConstProperty(AccessorType.prototype, 'type', type);
|
||||
|
||||
resourceIdentifiers.register(AccessorType, type.name);
|
||||
|
||||
return AccessorType;
|
||||
}
|
||||
|
||||
// For each of the meta types defined by the type mixin, define the
|
||||
// "ResourceIdentifier" property
|
||||
_.each({
|
||||
|
||||
// the root class 'Type' don't have an ResourceIdentifier property
|
||||
// Type: { factory() { } },
|
||||
|
||||
PrimitiveType: {
|
||||
|
||||
/**
|
||||
* PrimitiveType ResourceIdentifier type factory
|
||||
* @memberof PrimitiveType#
|
||||
* @this PrimitiveType
|
||||
* @returns {Class} The ResourceIdentifier type of the PrimitiveType
|
||||
*/
|
||||
factory() {
|
||||
const PrimitiveTypeResID = resourceIdentifierForType(this);
|
||||
|
||||
return PrimitiveTypeResID;
|
||||
},
|
||||
},
|
||||
|
||||
ObjectType: {
|
||||
|
||||
/**
|
||||
* ObjectType ResourceIdentifier type factory
|
||||
* @memberof ObjectType#
|
||||
* @this ObjectType
|
||||
* @returns {Class} The ResourceIdentifier type of the ObjectType
|
||||
*/
|
||||
factory() {
|
||||
const ObjectTypeResID = resourceIdentifierForType(this);
|
||||
|
||||
CastNavigation.defineOn(ObjectTypeResID);
|
||||
PropertyNavigation.defineOn(ObjectTypeResID, this.navigationProperties);
|
||||
if (this.callable) {
|
||||
CallableNavigation.defineOn(ObjectTypeResID, this.callable);
|
||||
}
|
||||
return ObjectTypeResID;
|
||||
},
|
||||
},
|
||||
|
||||
// fallback to ObjectType.prototype.ResourceIdentifier
|
||||
// EntityType: { factory () { } },
|
||||
|
||||
// fallback to ObjectType.prototype.ResourceIdentifier
|
||||
// ComplexType: { factory () { } },
|
||||
|
||||
CollectionType: {
|
||||
|
||||
/**
|
||||
* CollectionType ResourceIdentifier type factory
|
||||
* @memberof CollectionType#
|
||||
* @this CollectionType
|
||||
* @returns {Class} the ResourceIdentifier type of the CollectionType
|
||||
*/
|
||||
factory() {
|
||||
const CollectionTypeResID = resourceIdentifierForType(this);
|
||||
|
||||
CastNavigation.defineOn(CollectionTypeResID);
|
||||
// Only the entity sets can be navigated with keys
|
||||
if (this.elementType instanceof edm.types.EntityType) {
|
||||
WithKeyNavigation.defineOn(CollectionTypeResID);
|
||||
}
|
||||
PropertyNavigation.defineOn(CollectionTypeResID, this.navigationProperties);
|
||||
if (this.callable) {
|
||||
CallableNavigation.defineOn(CollectionTypeResID, this.callable);
|
||||
}
|
||||
|
||||
return CollectionTypeResID;
|
||||
},
|
||||
},
|
||||
|
||||
CallableType: {
|
||||
|
||||
/**
|
||||
* CallableType ResourceIdentifier type factory
|
||||
* @memberof CallableType#
|
||||
* @this CallableType
|
||||
* @returns {Class} The ResourceIdentifier type of the CallableType
|
||||
*/
|
||||
factory() {
|
||||
const CallableTypeResID = resourceIdentifierForType(this);
|
||||
|
||||
CallNavigation.defineOn(CallableTypeResID);
|
||||
|
||||
return CallableTypeResID;
|
||||
},
|
||||
},
|
||||
|
||||
}, (def, typeName) => {
|
||||
/**
|
||||
* @name type
|
||||
* @type Type
|
||||
* @property {Class} ResourceIdentifier - The ResourceIdentifier type of the type
|
||||
*/
|
||||
defineProducedPropertyOnClass(edm.types[typeName], 'ResourceIdentifier', def.factory);
|
||||
});
|
||||
|
||||
_.chain(resourceIdentifiers)
|
||||
.defineConstProperty('ResourceIdentifier', ResourceIdentifier)
|
||||
.defineConstProperty('CollectionResourceIdentifier', CollectionResourceIdentifier)
|
||||
.defineConstProperty('Navigation', Navigation)
|
||||
.defineConstProperty('PropertyNavigation', PropertyNavigation)
|
||||
.defineConstProperty('CastNavigation', CastNavigation)
|
||||
.defineConstProperty('WithKeyNavigation', WithKeyNavigation)
|
||||
.defineConstProperty('CallNavigation', CallNavigation)
|
||||
.defineConstProperty('CallableNavigation', CallableNavigation)
|
||||
.value();
|
||||
|
||||
return resourceIdentifiers;
|
||||
})());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resIdsPlugin,
|
||||
};
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* the schema plugin module
|
||||
*/
|
||||
|
||||
const _ = require('lodash');
|
||||
const { Registry } = require('./registry');
|
||||
const {
|
||||
defineConstProperty,
|
||||
hasOwnProperty,
|
||||
} = require('./reflection');
|
||||
|
||||
const typesPlugin = require('./types-plugin');
|
||||
|
||||
function getSchemaName(schema) {
|
||||
const { $$ODataExtension, name } = schema;
|
||||
if ($$ODataExtension) {
|
||||
return $$ODataExtension.Name;
|
||||
}
|
||||
|
||||
// deprecated
|
||||
// There is no such keyword as "name" in JSON Schema, it's specific to OData
|
||||
// See:
|
||||
// http://json-schema.org/latest/json-schema-core.html
|
||||
// http://json-schema.org/latest/json-schema-validation.html
|
||||
return name;
|
||||
}
|
||||
|
||||
function getSchemaBaseTypeName(schema) {
|
||||
const { $$ODataExtension, clrTypeBase } = schema;
|
||||
if ($$ODataExtension) {
|
||||
return $$ODataExtension.BaseType && $$ODataExtension.BaseType.$ref;
|
||||
}
|
||||
|
||||
// deprecated
|
||||
// There is no such keyword as "clrTypeBase" in JSON Schema, it's specific to OData
|
||||
// See:
|
||||
// http://json-schema.org/latest/json-schema-core.html
|
||||
// http://json-schema.org/latest/json-schema-validation.html
|
||||
return clrTypeBase;
|
||||
}
|
||||
|
||||
function getSchemaKey(schema) {
|
||||
const { $$ODataExtension, key } = schema;
|
||||
if ($$ODataExtension) {
|
||||
return $$ODataExtension.Key;
|
||||
}
|
||||
|
||||
// deprecated
|
||||
// There is no such keyword as "key" in JSON Schema, it's specific to OData
|
||||
// See:
|
||||
// http://json-schema.org/latest/json-schema-core.html
|
||||
// http://json-schema.org/latest/json-schema-validation.html
|
||||
return key;
|
||||
}
|
||||
|
||||
function getSchemaNavigationPropertyNames(schema) {
|
||||
const { $$ODataExtension } = schema;
|
||||
if ($$ODataExtension) {
|
||||
return $$ODataExtension.NavigationProperty;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
module.exports = (edm, {
|
||||
schemas = {},
|
||||
// TODO: deprecated, every schemas should be able to have different namespaces
|
||||
// we are still using it with legacy schemas
|
||||
namespace = null,
|
||||
} = {}) => {
|
||||
if (hasOwnProperty(edm, 'schema')) {
|
||||
return;
|
||||
}
|
||||
|
||||
typesPlugin(edm);
|
||||
|
||||
const {
|
||||
CollectionType,
|
||||
EntityType,
|
||||
PrimitiveType,
|
||||
ComplexType,
|
||||
OneOfType,
|
||||
ActionType,
|
||||
FunctionType,
|
||||
} = edm.types;
|
||||
|
||||
defineConstProperty(edm, 'schema', (() => {
|
||||
function getSchemaFullName(schema, path) {
|
||||
// $$ref exists in resolved schema
|
||||
const possiblePath = (_.isString(path) && path) || schema.$$ref || schema.$ref;
|
||||
return possiblePath
|
||||
? possiblePath.replace(/\//g, '.')
|
||||
: Registry.getQualifiedName(getSchemaName(schema), namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type name object with its information
|
||||
* @param {Schema[]} dependencies - An output paramter, a queue of dependent schemas
|
||||
* @param {Object} typeInfo - The schema information of the property
|
||||
* @return {String} The type name
|
||||
*/
|
||||
function getTypeName(dependencies, typeInfo) {
|
||||
let schema = null;
|
||||
let typeName = null;
|
||||
|
||||
if (!typeInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeInfo.type === 'array') {
|
||||
return CollectionType.collectionTypeName(getTypeName(dependencies, typeInfo.items));
|
||||
} if (typeInfo.oneOf) {
|
||||
return OneOfType.oneOfTypeName(typeInfo.oneOf.map(item => getTypeName(dependencies, item)));
|
||||
} if (typeInfo.type === 'object') {
|
||||
({ schema } = typeInfo);
|
||||
// it's possible for the schema to be undefined, use 'object' type by default
|
||||
typeName = getSchemaFullName(schema || typeInfo);
|
||||
} else if (typeInfo.type === 'string' && typeInfo.enum) {
|
||||
// enum type is string, but we need to get a meanful name for different enum type
|
||||
// this is to help us disable cache accurately, as enum can be odata call return type now
|
||||
typeName = (typeInfo.$$ref || 'string').split('/').join('.');
|
||||
} else {
|
||||
// primitive types
|
||||
typeName = typeInfo.type;
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
dependencies.push(schema);
|
||||
}
|
||||
|
||||
return typeName;
|
||||
}
|
||||
|
||||
function defineCallableOnType(acts, funcs, type, qualifiedName, forEntityType = true) {
|
||||
const actions = [];
|
||||
const functions = [];
|
||||
|
||||
function getCallableTypeName(key) {
|
||||
return `${key}##${qualifiedName}${forEntityType ? '' : '@@COLL'}`;
|
||||
}
|
||||
|
||||
_.each(_.keys(acts), key => {
|
||||
const typeName = getCallableTypeName(key);
|
||||
const parameters = {};
|
||||
|
||||
Object.keys(acts[key].Parameter || {}).forEach(k => {
|
||||
const parameter = acts[key].Parameter[k];
|
||||
parameters[k] = getTypeName([], parameter);
|
||||
});
|
||||
|
||||
actions.push(new ActionType({
|
||||
name: typeName,
|
||||
callableName: key,
|
||||
parameters,
|
||||
returnTypeName: getTypeName([], acts[key].ReturnType),
|
||||
}));
|
||||
});
|
||||
|
||||
_.each(_.keys(funcs), key => {
|
||||
const typeName = getCallableTypeName(key);
|
||||
const parameters = {};
|
||||
|
||||
Object.keys(funcs[key].Parameter || {}).forEach(k => {
|
||||
const param = funcs[key].Parameter[k];
|
||||
parameters[k] = getTypeName([], param);
|
||||
});
|
||||
|
||||
functions.push(new FunctionType({
|
||||
name: typeName,
|
||||
callableName: key,
|
||||
parameters,
|
||||
returnTypeName: getTypeName([], funcs[key].ReturnType),
|
||||
}));
|
||||
});
|
||||
|
||||
defineConstProperty(type, 'callable', {
|
||||
actions,
|
||||
functions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Define an entity type, and its dependencies
|
||||
*
|
||||
* @param {Schema} schema - The schema for the entity type
|
||||
* @param {string} [path] - Possible full path of the schema
|
||||
* @return {Void} Nothing to return
|
||||
*/
|
||||
function defineSchemaType(schema, path) {
|
||||
const qualifiedName = getSchemaFullName(schema, path);
|
||||
|
||||
if (!edm.types.resolve(qualifiedName)) {
|
||||
const dependencies = [];
|
||||
const properties = {};
|
||||
|
||||
Object.keys(schema.properties || {}).forEach(key => {
|
||||
const typeInfo = schema.properties[key];
|
||||
properties[key] = {
|
||||
typeName: getTypeName(dependencies, typeInfo),
|
||||
};
|
||||
});
|
||||
|
||||
const type = new EntityType({
|
||||
name: qualifiedName,
|
||||
baseTypeName: getSchemaBaseTypeName(schema) || 'object',
|
||||
key: getSchemaKey(schema),
|
||||
properties,
|
||||
navigationPropertyNames: getSchemaNavigationPropertyNames(schema),
|
||||
});
|
||||
|
||||
defineConstProperty(type, 'schema', schema);
|
||||
|
||||
_.each(dependencies, defineSchemaType);
|
||||
|
||||
const entityActions = _.get(schema, '$$ODataExtension.Action', null);
|
||||
const entityFunctions = _.get(schema, '$$ODataExtension.Function', null);
|
||||
|
||||
if (entityActions || entityFunctions) {
|
||||
defineCallableOnType(entityActions, entityFunctions, type, qualifiedName);
|
||||
}
|
||||
|
||||
const collCallable = _.get(schema, '$$ODataExtension.Collection', null);
|
||||
|
||||
if (collCallable) {
|
||||
defineCallableOnType(
|
||||
collCallable.Action,
|
||||
collCallable.Function,
|
||||
type.collectionType,
|
||||
qualifiedName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Define all the primitive types
|
||||
_.each({
|
||||
string: String,
|
||||
integer: Number,
|
||||
number: Number,
|
||||
datetime: Date,
|
||||
boolean: Boolean,
|
||||
null: null,
|
||||
}, (jsType, name) => new PrimitiveType({ name, jsType }));
|
||||
|
||||
// Define the base object type
|
||||
/* eslint no-new: 0 */
|
||||
new ComplexType({
|
||||
name: 'object',
|
||||
properties: {},
|
||||
});
|
||||
|
||||
_.each(schemas, (schema, index) => {
|
||||
if (_.isString(index)) {
|
||||
defineSchemaType(schema, index);
|
||||
} else {
|
||||
defineSchemaType(schema);
|
||||
}
|
||||
});
|
||||
|
||||
return _.chain({})
|
||||
.defineConstProperty('schemas', schemas)
|
||||
.defineConstProperty('namespace', namespace)
|
||||
.value();
|
||||
})());
|
||||
};
|
|
@ -0,0 +1,516 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
const _ = require('lodash');
|
||||
const { Registry } = require('./registry');
|
||||
const {
|
||||
defineConstProperty,
|
||||
defineProducedProperty,
|
||||
defineProducedPropertyOnClass,
|
||||
} = require('./reflection');
|
||||
|
||||
const ONEOF_TYPE_PREFIX = '@ONEOF';
|
||||
const COLLECTION_TYPE_POSTFIX = '@COLL';
|
||||
|
||||
function isOneOfType(typeName) {
|
||||
return typeName.indexOf(ONEOF_TYPE_PREFIX) === 0;
|
||||
}
|
||||
|
||||
function hasPostfix(str, postfix) {
|
||||
return str.slice(-postfix.length) === postfix;
|
||||
}
|
||||
|
||||
function removePostfix(str, postfix) {
|
||||
return str.slice(0, -postfix.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EDM} edm - The EDM object to apply this plugin to
|
||||
* @return {void}
|
||||
*/
|
||||
module.exports = edm => {
|
||||
if (edm.types) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const oneOfTypes = new Map();
|
||||
|
||||
/**
|
||||
* @name edm
|
||||
* @type {EDM}
|
||||
* @property {Registry} types - A registry of all types
|
||||
* @property {Class} types.Type - The base type of all meta Types
|
||||
* @property {Class} types.PrimitiveType - The meta type of primitive types
|
||||
* @property {Class} types.ObjectType - The meta type of the key/value types
|
||||
* @property {Class} types.ComplexType - The meta type of the complex types
|
||||
* @property {Class} types.EntityType - The meta type of the entity types
|
||||
* @property {Class} types.CollectionType - The meta type of the collection types
|
||||
* @property {Class} types.CallableType - The meta type of the callable types
|
||||
*/
|
||||
defineConstProperty(edm, 'types', (() => {
|
||||
const types = new Registry();
|
||||
|
||||
function resolveType(name, namespace) {
|
||||
if (hasPostfix(name, COLLECTION_TYPE_POSTFIX)) {
|
||||
return resolveType(removePostfix(name, COLLECTION_TYPE_POSTFIX), namespace).collectionType;
|
||||
}
|
||||
|
||||
if (isOneOfType(name)) {
|
||||
if (!oneOfTypes.has(name)) {
|
||||
const prefixLength = ONEOF_TYPE_PREFIX.length + 1;
|
||||
const typeNames = name.substr(prefixLength, name.length - prefixLength - 1).split(',');
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
oneOfTypes.set(name, new OneOfType({ typeNames }));
|
||||
}
|
||||
|
||||
return oneOfTypes.get(name);
|
||||
}
|
||||
|
||||
return types.resolve(name, namespace);
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Property
|
||||
* @property {String} name - The name of the property
|
||||
* @property {String} typeName - The name of the property's type
|
||||
* @property {Type} type - The type of the property
|
||||
*/
|
||||
class Property {
|
||||
/**
|
||||
* Create a property
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the property
|
||||
* @param {String} options.typeName - The name of the propertie's type
|
||||
* @param {String} options.namespace - The namespace of the propertie's type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
typeName,
|
||||
namespace,
|
||||
}) {
|
||||
defineConstProperty(this, 'name', name);
|
||||
defineConstProperty(this, 'typeName', typeName);
|
||||
defineProducedProperty(this, 'type', () => resolveType(typeName, namespace));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Parameter
|
||||
* @property {String} name - The name of the parameter
|
||||
* @property {String} typeName - The name of the paramter's type
|
||||
* @property {type} type - The type of the parameter
|
||||
*/
|
||||
class Parameter {
|
||||
/**
|
||||
* Create a parameter
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the parameter
|
||||
* @param {String} options.typeName - The name of the parameter's type
|
||||
* @param {String} options.namespace - The namespace of the parameter's type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
typeName,
|
||||
namespace,
|
||||
}) {
|
||||
defineConstProperty(this, 'name', name);
|
||||
defineConstProperty(this, 'typeName', typeName);
|
||||
defineProducedProperty(this, 'type', () => resolveType(typeName, namespace));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Type
|
||||
* @property {String} name - The qualified name of the type
|
||||
* @property {String} namespace - The namespace of the type
|
||||
* @property {String} shortName - The short name of the type
|
||||
*/
|
||||
class Type {
|
||||
/**
|
||||
* Create and register a type
|
||||
* A side effect of Type creation is registering itself to the type registry
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
}) {
|
||||
defineConstProperty(this, 'name', name);
|
||||
defineConstProperty(this, 'namespace', Registry.getNamespace(name));
|
||||
defineConstProperty(this, 'shortName', Registry.getShortName(name));
|
||||
types.register(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class PrimitiveType
|
||||
* @extends Type
|
||||
* @property {Class} jsType - The corresponding JavaScript type
|
||||
*/
|
||||
class PrimitiveType extends Type {
|
||||
/**
|
||||
* Create a PrimitiveType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Class} options.jsType - The JavaScript type of the primitive type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
jsType,
|
||||
}) {
|
||||
super({ name });
|
||||
defineConstProperty(this, 'jsType', jsType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class OneOfType
|
||||
* @extends Type
|
||||
* @property {Class} types - Data could be one of these types
|
||||
* @see {@link http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.7.3}
|
||||
*/
|
||||
class OneOfType extends Type {
|
||||
/**
|
||||
* Create a PrimitiveType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Type[]} options.types - The JavaScript type of the primitive type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
typeNames,
|
||||
}) {
|
||||
super({
|
||||
name: OneOfType.oneOfTypeName(typeNames),
|
||||
});
|
||||
|
||||
defineConstProperty(this, 'typeNames', typeNames);
|
||||
defineConstProperty(this, 'types', typeNames.map(typeName => resolveType(typeName, this.namespace)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OneOfType's name from it's elements type name
|
||||
* @param {String[]} typeNames - The name of the elementTypes
|
||||
* @return {String} The name of the OneOfType
|
||||
*/
|
||||
static oneOfTypeName(typeNames) {
|
||||
// We don't support nested one for now
|
||||
const sortedNames = typeNames
|
||||
.map(name => types.resolveQualifiedName(name, this.namespace))
|
||||
.sort();
|
||||
|
||||
return `${ONEOF_TYPE_PREFIX}(${sortedNames.join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PropertyInfo
|
||||
* @property {String} typeName - The name of the propertie's type
|
||||
*
|
||||
* @memberof ObjectType#
|
||||
* @this ObjectType
|
||||
* @param {Object.<String, PropertyInfo>} properties - The properties to be compiled
|
||||
* @return {Object.<String, Property>} a mapping for name to compiled Properties
|
||||
*/
|
||||
function compileProperties(properties) {
|
||||
const { namespace } = this;
|
||||
|
||||
const ret = {};
|
||||
|
||||
Object.keys(properties || {}).forEach(name => {
|
||||
const { typeName } = properties[name];
|
||||
ret[name] = new Property({
|
||||
name,
|
||||
typeName,
|
||||
namespace,
|
||||
});
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ObjectType
|
||||
* @extends Type
|
||||
* @property {Object.<String, Property>} properties - The properties of the object type
|
||||
* @property {String} baseTypeName - The name of the base type
|
||||
* @property {Type} baseType - The base type
|
||||
*/
|
||||
class ObjectType extends Type {
|
||||
/**
|
||||
* Create an ObjectType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Object.<String, PropertyInfo>} options.properties - The property definition
|
||||
* @param {string[]} [options.navigationPropertyNames] - Navigation properties names
|
||||
* @param {String} options.baseTypeName - The name of the base type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
properties,
|
||||
navigationPropertyNames = [],
|
||||
baseTypeName,
|
||||
}) {
|
||||
super({ name });
|
||||
|
||||
defineConstProperty(this, 'properties', compileProperties.call(this, properties));
|
||||
defineConstProperty(this, 'navigationPropertyNames', navigationPropertyNames.slice());
|
||||
defineProducedProperty(this, 'navigationProperties', () => _.pickBy(this.properties, (property, propertyName) => _.includes(this.navigationPropertyNames, propertyName)));
|
||||
if (baseTypeName) {
|
||||
defineConstProperty(this, 'baseTypeName', baseTypeName);
|
||||
defineProducedProperty(this, 'baseType', () => resolveType(this.baseTypeName, this.namespace));
|
||||
}
|
||||
}
|
||||
|
||||
addProperties(properties) {
|
||||
_.extend(this.properties, compileProperties.call(this, properties));
|
||||
// only for backward compability, should use addNavigationProperties for this case
|
||||
this.navigationPropertyNames.push(..._.keys(properties));
|
||||
}
|
||||
|
||||
addNavigationProperties(properties) {
|
||||
this.addProperties(properties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ComplexType
|
||||
* @extends ObjectType
|
||||
*/
|
||||
class ComplexType extends ObjectType {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @class EntityType
|
||||
* @extends ObjectType
|
||||
* @property {String} key - The name of the key property
|
||||
* @property {Property} keyProperty - The key property of the entity type
|
||||
* @return {void}
|
||||
*/
|
||||
class EntityType extends ObjectType {
|
||||
/**
|
||||
* Create an EntityType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Object.<String, PropertyInfo>} options.properties - The property definition
|
||||
* @param {String} options.baseTypeName - The name of the base type
|
||||
* @param {String} [options.key] - The name of the key property
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
properties,
|
||||
navigationPropertyNames,
|
||||
baseTypeName,
|
||||
key,
|
||||
}) {
|
||||
super({
|
||||
name,
|
||||
properties,
|
||||
navigationPropertyNames,
|
||||
baseTypeName,
|
||||
});
|
||||
|
||||
if (key) {
|
||||
defineConstProperty(this, 'key', key);
|
||||
defineConstProperty(this, 'keyProperty', this.properties[this.key]);
|
||||
} else if (baseTypeName) {
|
||||
// The key property is inherited if there's a base type
|
||||
defineProducedProperty(this, 'key', () => this.baseType.key);
|
||||
defineProducedProperty(this, 'keyProperty', () => this.baseType.keyProperty);
|
||||
} else {
|
||||
throw new Error('The "key" property is required for an EntityType');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class CollectionType
|
||||
* @extends ObjectType
|
||||
* @property {String} elementTypeName - The name of the element type
|
||||
* @property {Type} elementType - The type of the elements
|
||||
*/
|
||||
class CollectionType extends ObjectType {
|
||||
/**
|
||||
* Create a CollectionType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {Object.<String, PropertyInfo>} options.properties - The property definition
|
||||
* @param {String} options.baseTypeName - The name of the base type
|
||||
* @param {String} options.elementTypeName - The name of the element type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
properties,
|
||||
baseTypeName,
|
||||
elementTypeName,
|
||||
}) {
|
||||
super({
|
||||
name: CollectionType.collectionTypeName(elementTypeName),
|
||||
properties,
|
||||
baseTypeName,
|
||||
});
|
||||
defineConstProperty(this, 'elementTypeName', elementTypeName);
|
||||
defineProducedProperty(this, 'elementType', () => resolveType(this.elementTypeName, this.namespace));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CollectionType's name from it's element type name
|
||||
* @param {String} typeName - The name of the elementType
|
||||
* @return {String} The name of the CollectionType
|
||||
*/
|
||||
static collectionTypeName(typeName) {
|
||||
return `${typeName}${COLLECTION_TYPE_POSTFIX}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof Type#
|
||||
* @this Type
|
||||
* @returns {CollectionType} The CollectionType of the given Type
|
||||
*/
|
||||
function collectionTypeFactory() {
|
||||
return new CollectionType({ elementTypeName: this.name });
|
||||
}
|
||||
|
||||
/**
|
||||
* @name type
|
||||
* @type Type
|
||||
* @property {CollectionType} collectionType - The collection type of the given type
|
||||
*/
|
||||
defineProducedPropertyOnClass(Type, 'collectionType', collectionTypeFactory);
|
||||
|
||||
/**
|
||||
* @typedef {Object} ParameterInfo
|
||||
* @property {String} typeName - The name of the parameter's type
|
||||
*
|
||||
* @memberof CallableType#
|
||||
* @this CallableType
|
||||
* @param {Object.<String, ParameterInfo>} parameters - The parameters to compile
|
||||
* @return {Object.<String, Parameter>} A mapping for name to compiled Parameters
|
||||
*/
|
||||
function compileParameters(parameters) {
|
||||
const { namespace } = this;
|
||||
|
||||
const ret = {};
|
||||
Object.keys(parameters || {}).forEach(name => {
|
||||
const { typeName } = parameters[name];
|
||||
ret[name] = new Parameter({ name, namespace, typeName });
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @class CallableType
|
||||
* @property {Parameter[]} parameters - The parameters required to call the callable
|
||||
* @property {String} returnTypeName - The name of the return type
|
||||
* @property {Type} returnType - The return type of the callable
|
||||
*/
|
||||
class CallableType extends Type {
|
||||
/**
|
||||
* Create a CallableType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Object.<String, ParameterInfo>} parameters - The parameter definitions
|
||||
* @param {String} returnTypeName - The name of the return type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
callableName,
|
||||
parameters,
|
||||
returnTypeName,
|
||||
}) {
|
||||
super({ name });
|
||||
|
||||
defineConstProperty(this, 'callableName', callableName);
|
||||
defineConstProperty(this, 'parameters', compileParameters.call(this, parameters));
|
||||
defineConstProperty(this, 'returnTypeName', returnTypeName);
|
||||
defineProducedProperty(this, 'returnType', () => resolveType(returnTypeName, this.namespace));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class ActionType
|
||||
* @property {Parameter[]} parameters - The parameters required to call the callable
|
||||
* @property {String} returnTypeName - The name of the return type
|
||||
* @property {Type} returnType - The return type of the callable
|
||||
*/
|
||||
class ActionType extends CallableType {
|
||||
/**
|
||||
* Create a CallableType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Object.<String, ParameterInfo>} parameters - The parameter definitions
|
||||
* @param {String} returnTypeName - The name of the return type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
callableName,
|
||||
parameters,
|
||||
returnTypeName,
|
||||
}) {
|
||||
super({
|
||||
name,
|
||||
callableName,
|
||||
parameters,
|
||||
returnTypeName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class FunctionType
|
||||
* @property {Parameter[]} parameters - The parameters required to call the callable
|
||||
* @property {String} returnTypeName - The name of the return type
|
||||
* @property {Type} returnType - The return type of the callable
|
||||
*/
|
||||
class FunctionType extends CallableType {
|
||||
/**
|
||||
* Create a CallableType
|
||||
* @param {Object} options - The constructor options
|
||||
* @param {String} options.name - The name of the type
|
||||
* @param {Object.<String, ParameterInfo>} parameters - The parameter definitions
|
||||
* @param {String} returnTypeName - The name of the return type
|
||||
* @return {void}
|
||||
*/
|
||||
constructor({
|
||||
name,
|
||||
callableName,
|
||||
parameters,
|
||||
returnTypeName,
|
||||
}) {
|
||||
super({
|
||||
name,
|
||||
callableName,
|
||||
parameters,
|
||||
returnTypeName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: wewei, support EnumType
|
||||
|
||||
defineConstProperty(types, 'Property', Property);
|
||||
defineConstProperty(types, 'Parameter', Parameter);
|
||||
defineConstProperty(types, 'Type', Type);
|
||||
defineConstProperty(types, 'PrimitiveType', PrimitiveType);
|
||||
defineConstProperty(types, 'OneOfType', OneOfType);
|
||||
defineConstProperty(types, 'ObjectType', ObjectType);
|
||||
defineConstProperty(types, 'ComplexType', ComplexType);
|
||||
defineConstProperty(types, 'EntityType', EntityType);
|
||||
defineConstProperty(types, 'CollectionType', CollectionType);
|
||||
defineConstProperty(types, 'CallableType', CallableType);
|
||||
defineConstProperty(types, 'ActionType', ActionType);
|
||||
defineConstProperty(types, 'FunctionType', FunctionType);
|
||||
|
||||
return types;
|
||||
})());
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
const _ = require('lodash');
|
||||
|
||||
function join(first, ...frags) {
|
||||
return frags.reduce((memo, frag) => {
|
||||
if (_.isEmpty(frag)) {
|
||||
return memo;
|
||||
}
|
||||
const eSlash = /\/$/.test(memo);
|
||||
const sSlash = /^\//.test(frag);
|
||||
|
||||
if (!eSlash && !sSlash) {
|
||||
return `${memo}/${frag}`;
|
||||
}
|
||||
if (eSlash && sSlash) {
|
||||
return memo + frag.substring(1);
|
||||
}
|
||||
return memo + frag;
|
||||
}, first);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
join,
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const _ = require('lodash');
|
||||
|
||||
const urlRegex = /https?:\/\//;
|
||||
|
||||
function getBaseType(schema, name) {
|
||||
return _.get(schema[name], '$$ODataExtension.BaseType.$ref', null);
|
||||
}
|
||||
|
||||
function getProperties(schema, name) {
|
||||
const { properties } = schema[name];
|
||||
const baseType = getBaseType(schema, name);
|
||||
|
||||
if (_.isArray(baseType)) {
|
||||
const baseProperties = _.reduce(
|
||||
baseType,
|
||||
(memo, bType) => _.chain({})
|
||||
.merge(memo, getProperties(schema, bType))
|
||||
.omit(schema[bType].private)
|
||||
.value(),
|
||||
{},
|
||||
);
|
||||
|
||||
return _.merge({}, baseProperties, properties);
|
||||
} if (baseType) {
|
||||
return _.chain({})
|
||||
.merge(getProperties(schema, baseType), properties)
|
||||
.omit(schema[baseType].private)
|
||||
.value();
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
function getSchemaWithBaseTypeProperties(jsonSchema) {
|
||||
return _.mapValues(jsonSchema, (val, name) => {
|
||||
const baseType = getBaseType(jsonSchema, name);
|
||||
const schema = baseType ? _.mergeWith({}, {
|
||||
properties: getProperties(jsonSchema, name),
|
||||
}, jsonSchema[name], (objValue, srcValue, key) => {
|
||||
if (key === 'enum') {
|
||||
return srcValue;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}) : jsonSchema[name];
|
||||
|
||||
return schema;
|
||||
});
|
||||
}
|
||||
|
||||
function replacer(schema, model) {
|
||||
if (!_.isObject(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const extension = {};
|
||||
Object.entries(schema).forEach(([key, val]) => {
|
||||
const refSchemaName = val;
|
||||
if (key === '$ref') {
|
||||
if (!urlRegex.test(refSchemaName)) {
|
||||
// eslint-disable-next-line no-proto
|
||||
// extension.__proto__ = model[refSchemaName];
|
||||
Object.setPrototypeOf(extension, model[refSchemaName]);
|
||||
}
|
||||
if (/^Model\//.test(refSchemaName)) {
|
||||
extension.schema = model[refSchemaName];
|
||||
}
|
||||
}
|
||||
if (key === 'oneOf') {
|
||||
if (val[0]) {
|
||||
Object.setPrototypeOf(val[0], model[val[0].$ref]);
|
||||
|
||||
Object.assign(extension, {
|
||||
nullable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(schema, { [key]: replacer(val, model) });
|
||||
});
|
||||
|
||||
if (_.isArray(schema)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
extension,
|
||||
schema,
|
||||
);
|
||||
}
|
||||
|
||||
function exportSchemaModel(namespaces) {
|
||||
const schemas = {};
|
||||
const model = {};
|
||||
|
||||
for (const [key, value] of Object.entries(namespaces)) {
|
||||
Object.keys(value).forEach(name => {
|
||||
const schemaName = `${key}/${name}`;
|
||||
schemas[schemaName] = value[name];
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(schemas).forEach(name => { model[name] = {}; });
|
||||
|
||||
const schemaWithBaseTypeProperties = getSchemaWithBaseTypeProperties(schemas);
|
||||
|
||||
Object.keys(schemaWithBaseTypeProperties).sort().forEach(name => {
|
||||
Object.assign(model[name], replacer(schemaWithBaseTypeProperties[name], model));
|
||||
});
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exportSchemaModel,
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
const { fetchMetadata } = require('./lib/fetchMetadata');
|
||||
const { parseSchemas } = require('./lib/parseSchemas');
|
||||
|
||||
async function generateODataSchema(endpoint, {
|
||||
isByDefaultNullable = ref => {
|
||||
if (ref === 'Edm/String') {
|
||||
return true;
|
||||
}
|
||||
return !ref.startsWith('Edm');
|
||||
},
|
||||
withEnumValue = false,
|
||||
} = {}) {
|
||||
const metadata = await fetchMetadata(endpoint);
|
||||
const odataSchemas = parseSchemas(metadata['edmx:Edmx']['edmx:DataServices'][0].Schema, {
|
||||
isByDefaultNullable,
|
||||
withEnumValue,
|
||||
});
|
||||
|
||||
const namespaces = {};
|
||||
|
||||
Object.entries(odataSchemas)
|
||||
.filter(([namespace, namespaceSchemas]) => !namespace.startsWith('http') && Object.getOwnPropertyNames(namespaceSchemas).length)
|
||||
.forEach(([namespace, namespaceSchemas]) => {
|
||||
namespaces[namespace] = namespaceSchemas;
|
||||
});
|
||||
|
||||
return namespaces;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateODataSchema,
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
// all primitive types https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/entity-data-model-primitive-data-types
|
||||
const EDMPrimitivesMapping = {
|
||||
Binary: {},
|
||||
Boolean: { type: 'boolean' },
|
||||
Byte: { type: 'integer' },
|
||||
DateTime: {},
|
||||
DateTimeOffset: { type: 'string', format: 'date-time' },
|
||||
Decimal: { type: 'number' },
|
||||
Double: { type: 'number' },
|
||||
Float: { type: 'number' },
|
||||
GeographyPoint: {}, // not listed in the doc
|
||||
Guid: { type: 'string', regex: '^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$' },
|
||||
Int16: { type: 'integer' },
|
||||
Int32: { type: 'integer' },
|
||||
Int64: { type: 'integer' },
|
||||
SByte: { type: 'integer' },
|
||||
Single: { type: 'number' }, // not listed in the doc
|
||||
String: { type: 'string' },
|
||||
Time: { type: 'string', format: 'time' },
|
||||
};
|
||||
|
||||
Object.entries(EDMPrimitivesMapping).forEach(([EDMType, schema]) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
schema.$$ODataExtension = {
|
||||
Name: EDMType,
|
||||
};
|
||||
});
|
||||
|
||||
exports.EDMPrimitivesMapping = EDMPrimitivesMapping;
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const { parseMethod } = require('./parseMethod');
|
||||
|
||||
function bindMethod(method, extension, type) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
extension[type] = extension[type] || {};
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
extension[type][`${method.Namespace}.${method.Name}`] = method;
|
||||
}
|
||||
|
||||
function findSchemaByRef(schemas, $ref) {
|
||||
const frags = $ref.split('/');
|
||||
const name = frags.pop();
|
||||
return schemas[frags.join('.')][name];
|
||||
}
|
||||
|
||||
function bindMethods(methods = [], Namespace, schemas, type, {
|
||||
isByDefaultNullable,
|
||||
}) {
|
||||
for (const method of methods) {
|
||||
const metadata = parseMethod(method, Namespace, {
|
||||
isByDefaultNullable,
|
||||
});
|
||||
if (metadata.IsBound) {
|
||||
const bindingParameter = metadata.Parameter[metadata.BindingParameter];
|
||||
if (bindingParameter.type === 'array') {
|
||||
const schema = findSchemaByRef(schemas, bindingParameter.items.$ref);
|
||||
schema.$$ODataExtension.Collection = schema.$$ODataExtension.Collection || {};
|
||||
bindMethod(metadata, schema.$$ODataExtension.Collection, type);
|
||||
} else {
|
||||
const schema = findSchemaByRef(schemas, bindingParameter.$ref);
|
||||
bindMethod(metadata, schema.$$ODataExtension, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.bindMethods = bindMethods;
|
|
@ -0,0 +1,11 @@
|
|||
const xml2js = require('xml2js');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function fetchMetadata(endpoint) {
|
||||
const doc = await fetch(`${endpoint}/$metadata`);
|
||||
const docText = await doc.text();
|
||||
|
||||
return xml2js.parseStringPromise(docText);
|
||||
}
|
||||
|
||||
exports.fetchMetadata = fetchMetadata;
|
|
@ -0,0 +1,5 @@
|
|||
function parseBoolean(string) {
|
||||
return string === 'true';
|
||||
}
|
||||
|
||||
exports.parseBoolean = parseBoolean;
|
|
@ -0,0 +1,7 @@
|
|||
const { parseObjectType } = require('./parseObjectType');
|
||||
|
||||
function parseComplexType(ComplexType, { isByDefaultNullable }) {
|
||||
return parseObjectType(ComplexType, { isByDefaultNullable });
|
||||
}
|
||||
|
||||
exports.parseComplexType = parseComplexType;
|
|
@ -0,0 +1,15 @@
|
|||
const { parseObjectType } = require('./parseObjectType');
|
||||
|
||||
function parseEntityType({
|
||||
Key,
|
||||
...rest
|
||||
}, { isByDefaultNullable }) {
|
||||
const schema = parseObjectType(rest, { isByDefaultNullable });
|
||||
// There could be multiple keys
|
||||
if (Key) {
|
||||
schema.$$ODataExtension.Key = Key.map(({ PropertyRef: [{ $: { Name } }] }) => Name);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
exports.parseEntityType = parseEntityType;
|
|
@ -0,0 +1,41 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const { parseBoolean } = require('./parseBoolean');
|
||||
const { parseTypeReference } = require('./parseTypeReference');
|
||||
|
||||
function parseEnumType({
|
||||
$: { Name, UnderlyingType, IsFlags },
|
||||
Member,
|
||||
}, {
|
||||
isByDefaultNullable,
|
||||
withEnumValue,
|
||||
}) {
|
||||
const schema = {
|
||||
type: 'string',
|
||||
enum: Member.map(({ $: { Name: enumValue } }) => enumValue),
|
||||
$$ODataExtension: {
|
||||
Name,
|
||||
},
|
||||
};
|
||||
|
||||
if (withEnumValue) {
|
||||
const memberValues = {};
|
||||
for (const { $: { Name: enumValue, Value } } of Member) {
|
||||
memberValues[enumValue] = Number.parseInt(Value, 10);
|
||||
}
|
||||
schema.$$ODataExtension.Value = memberValues;
|
||||
}
|
||||
|
||||
if (UnderlyingType) {
|
||||
schema.$$ODataExtension.UnderlyingType = parseTypeReference(UnderlyingType, 'false', {
|
||||
isByDefaultNullable,
|
||||
});
|
||||
}
|
||||
|
||||
if (IsFlags) {
|
||||
schema.$$ODataExtension.IsFlags = parseBoolean(IsFlags);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
exports.parseEnumType = parseEnumType;
|
|
@ -0,0 +1,5 @@
|
|||
function parseFullName(name) {
|
||||
return name.replace(/\./g, '/');
|
||||
}
|
||||
|
||||
exports.parseFullName = parseFullName;
|
|
@ -0,0 +1,36 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const { parseBoolean } = require('./parseBoolean');
|
||||
const { parseTypeReference } = require('./parseTypeReference');
|
||||
|
||||
function parseMethod({
|
||||
$: { Name, IsBound },
|
||||
Parameter,
|
||||
ReturnType,
|
||||
}, Namespace, {
|
||||
isByDefaultNullable,
|
||||
}) {
|
||||
const metadata = {
|
||||
Namespace,
|
||||
Name,
|
||||
IsBound: parseBoolean(IsBound),
|
||||
};
|
||||
if (Parameter) {
|
||||
metadata.Parameter = {};
|
||||
for (const { $: { Name: parameterName, Type, Nullable/* , Unicode */ } } of Parameter) {
|
||||
metadata.Parameter[parameterName] = parseTypeReference(Type, Nullable, {
|
||||
isByDefaultNullable,
|
||||
});
|
||||
}
|
||||
if (metadata.IsBound) {
|
||||
// The first parameter is the binding parameter
|
||||
// Check http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/cs01/odata-csdl-xml-v4.01-cs01.html#sec_Parameter
|
||||
metadata.BindingParameter = Parameter[0].$.Name;
|
||||
}
|
||||
}
|
||||
if (ReturnType) {
|
||||
metadata.ReturnType = parseTypeReference(ReturnType[0].$.Type, 'false', { isByDefaultNullable });
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
exports.parseMethod = parseMethod;
|
|
@ -0,0 +1,44 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const { parseBoolean } = require('./parseBoolean');
|
||||
const { parseTypeReference } = require('./parseTypeReference');
|
||||
|
||||
function parseObjectType({
|
||||
$: {
|
||||
Name, Abstract, BaseType, OpenType,
|
||||
},
|
||||
NavigationProperty,
|
||||
Property,
|
||||
}, { isByDefaultNullable }) {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
$$ODataExtension: {
|
||||
Name,
|
||||
},
|
||||
};
|
||||
if (Abstract) {
|
||||
schema.$$ODataExtension.Abstract = parseBoolean(Abstract);
|
||||
}
|
||||
if (BaseType) {
|
||||
schema.$$ODataExtension.BaseType = parseTypeReference(BaseType, 'false', { isByDefaultNullable });
|
||||
}
|
||||
if (OpenType) {
|
||||
schema.$$ODataExtension.OpenType = parseBoolean(OpenType);
|
||||
}
|
||||
if (NavigationProperty) {
|
||||
schema.$$ODataExtension.NavigationProperty = NavigationProperty.map(({
|
||||
$: { Name: propertyName },
|
||||
}) => propertyName);
|
||||
for (const { $: { Name: propertyName, Type, Nullable } } of NavigationProperty) {
|
||||
schema.properties[propertyName] = parseTypeReference(Type, Nullable, { isByDefaultNullable });
|
||||
}
|
||||
}
|
||||
if (Property) {
|
||||
for (const { $: { Name: propertyName, Type, Nullable } } of Property) {
|
||||
schema.properties[propertyName] = parseTypeReference(Type, Nullable, { isByDefaultNullable });
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
exports.parseObjectType = parseObjectType;
|
|
@ -0,0 +1,44 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
const { parseEntityType } = require('./parseEntityType');
|
||||
const { parseComplexType } = require('./parseComplexType');
|
||||
const { parseEnumType } = require('./parseEnumType');
|
||||
const { bindMethods } = require('./bindMethods');
|
||||
const { EDMPrimitivesMapping } = require('./EDMPrimitivesMapping');
|
||||
|
||||
function parseSchemas(schemas, { isByDefaultNullable, withEnumValue }) {
|
||||
const ans = {
|
||||
Edm: {},
|
||||
};
|
||||
for (const [EDMType, schema] of Object.entries(EDMPrimitivesMapping)) {
|
||||
ans.Edm[EDMType] = schema;
|
||||
}
|
||||
for (const {
|
||||
$: { Namespace },
|
||||
ComplexType = [],
|
||||
EntityType = [],
|
||||
EnumType = [],
|
||||
} of schemas) {
|
||||
ans[Namespace] = {};
|
||||
for (const et of EntityType) {
|
||||
const etSchema = parseEntityType(et, { isByDefaultNullable });
|
||||
ans[Namespace][etSchema.$$ODataExtension.Name] = etSchema;
|
||||
}
|
||||
for (const ct of ComplexType) {
|
||||
const ctSchema = parseComplexType(ct, { isByDefaultNullable });
|
||||
ans[Namespace][ctSchema.$$ODataExtension.Name] = ctSchema;
|
||||
}
|
||||
for (const et of EnumType) {
|
||||
const etSchema = parseEnumType(et, { isByDefaultNullable, withEnumValue });
|
||||
ans[Namespace][etSchema.$$ODataExtension.Name] = etSchema;
|
||||
}
|
||||
}
|
||||
|
||||
for (const { $: { Namespace }, Action, Function } of schemas) {
|
||||
bindMethods(Action, Namespace, ans, 'Action', { isByDefaultNullable });
|
||||
bindMethods(Function, Namespace, ans, 'Function', { isByDefaultNullable });
|
||||
}
|
||||
|
||||
return ans;
|
||||
}
|
||||
|
||||
exports.parseSchemas = parseSchemas;
|
|
@ -0,0 +1,34 @@
|
|||
const { parseFullName } = require('./parseFullName');
|
||||
const { parseBoolean } = require('./parseBoolean');
|
||||
|
||||
function addNullable(schema, nullable, { isByDefaultNullable }) {
|
||||
// by default Nullable is true
|
||||
// see: http://docs.oasis-open.org/odata/odata-csdl-xml/v4.01/cs01/odata-csdl-xml-v4.01-cs01.html#_Toc505863907
|
||||
if ((!nullable || parseBoolean(nullable)) && !isByDefaultNullable(schema.$ref)) {
|
||||
return {
|
||||
oneOf: [schema, {
|
||||
type: 'null',
|
||||
}],
|
||||
};
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
function parseTypeReference(typeReference, nullable, {
|
||||
isByDefaultNullable,
|
||||
}) {
|
||||
const collectionExec = /Collection\((.*)\)/.exec(typeReference);
|
||||
if (collectionExec) {
|
||||
return {
|
||||
type: 'array',
|
||||
items: parseTypeReference(collectionExec[1], 'false', { isByDefaultNullable }),
|
||||
};
|
||||
}
|
||||
return addNullable({
|
||||
$ref: parseFullName(typeReference),
|
||||
}, nullable, {
|
||||
isByDefaultNullable,
|
||||
});
|
||||
}
|
||||
|
||||
exports.parseTypeReference = parseTypeReference;
|
Загрузка…
Ссылка в новой задаче