Merge remote-tracking branch 'upstream/main' into statusCode

This commit is contained in:
Like Zhu 2021-07-20 12:24:09 -07:00
Родитель 15e2c2218e 2bfc0417d9
Коммит 88597944f3
28 изменённых файлов: 2317 добавлений и 4 удалений

17
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -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"
}
]
}

3
.vscode/settings.json поставляемый
Просмотреть файл

@ -1,6 +1,9 @@
{
"editor.renderWhitespace": "all",
"editor.tabSize": 2,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"files.trimTrailingWhitespace": true,
"[markdown]": {
"files.trimTrailingWhitespace": false,

82
package-lock.json сгенерированный
Просмотреть файл

@ -1,9 +1,53 @@
{
"name": "@microsoft/overreact-root",
"lockfileVersion": 2,
"requires": true,
"lockfileVersion": 1,
"packages": {
"": {
"name": "@microsoft/overreact-root",
"workspaces": [
"packages"
],
"dependencies": {
"@algolia/autocomplete-core": {
"@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=="
},

52
packages/odata/README.md Normal file
Просмотреть файл

@ -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;