UNPKG

jaydata-dynamic-metadata

Version:

OData v4 metadata to JayData context util

690 lines (584 loc) 26.7 kB
import { Annotations } from './annotations' import { JayData } from './dts' var containsField = (obj, field, cb) => { // if (field in (obj || {})) { // cb(obj[field]) // } if (obj && field in obj && typeof obj[field] !== "undefined") { cb(obj[field]) } } var parsebool = (b, d) => { if ("boolean" === typeof b) { return b } switch (b) { case "true": return true case "false": return false default: return d } } var _collectionRegex = /^Collection\((.*)\)$/ const dtsTypeMapping = { 'Edm.Boolean': 'boolean', 'Edm.Binary': 'Uint8Array', 'Edm.DateTime': 'Date', 'Edm.DateTimeOffset': 'Date', 'Edm.Time': 'string', 'Edm.Duration': 'string', 'Edm.TimeOfDay': 'string', 'Edm.Date': 'string', 'Edm.Decimal': 'string', 'Edm.Single': 'number', 'Edm.Float': 'number', 'Edm.Double': 'number', 'Edm.Guid': 'string', 'Edm.Int16': 'number', 'Edm.Int32': 'number', 'Edm.Int64': 'string', 'Edm.Byte': 'number', 'Edm.SByte': 'number', 'Edm.String': 'string', 'Edm.GeographyPoint': '$data.Geography', 'Edm.GeographyLineString': '$data.GeographyLineString', 'Edm.GeographyPolygon': '$data.GeographyPolygon', 'Edm.GeographyMultiPoint': '$data.GeographyMultiPoint', 'Edm.GeographyMultiPolygon': '$data.GeographyMultiPolygon', 'Edm.GeographyMultiLineString': '$data.GeographyMultiLineString', 'Edm.GeographyCollection': '$data.GeographyCollection', 'Edm.GeometryPoint': '$data.Geometry', 'Edm.GeometryLineString': '$data.GeometryLineString', 'Edm.GeometryPolygon': '$data.GeometryPolygon', 'Edm.GeometryMultiPoint': '$data.GeometryMultiPoint', 'Edm.GeometryMultiPolygon': '$data.GeometryMultiPolygon', 'Edm.GeometryMultiLineString': '$data.GeometryMultiLineString', 'Edm.GeometryCollection': '$data.GeometryCollection' }; export class Metadata { options: any metadata: any private $data: any private annotationHandler: Annotations private storedTypes: Object; constructor($data: any, options: any, metadata: any) { this.$data = $data; this.options = options || {}; this.metadata = metadata; this.options.container = this.$data.Container; //this.options.container || $data.createContainer() this.options.baseType = this.options.baseType || '$data.Entity' this.options.entitySetType = this.options.entitySetType || '$data.EntitySet' this.options.contextType = this.options.contextType || '$data.EntityContext' this.options.collectionBaseType = this.options.collectionBaseType || 'Array' this.annotationHandler = new Annotations() this.storedTypes = {}; } _getMaxValue(maxValue) { if ("number" === typeof maxValue) return maxValue if ("max" === maxValue) return Number.MAX_VALUE return parseInt(maxValue) } createTypeDefinition(propertySchema, definition) { containsField(propertySchema, "type", v => { var match = _collectionRegex.exec(v) if (match) { definition.type = this.options.collectionBaseType definition.elementType = match[1] } else { definition.type = v } }) } createReturnTypeDefinition(propertySchema, definition) { containsField(propertySchema, "type", v => { var match = _collectionRegex.exec(v) if (match) { definition.returnType = '$data.Queryable' definition.elementType = match[1] } else { definition.returnType = v } }) } createProperty(entityFullName, entitySchema, propertySchema) { var self = this; if (!propertySchema) { propertySchema = entitySchema entitySchema = undefined } var definition: any = {} this.createTypeDefinition(propertySchema, definition) containsField(propertySchema, "nullable", v => { definition.nullable = parsebool(v, true), definition.required = parsebool(v, true) === false }) containsField(propertySchema, "maxLength", v => { definition.maxLength = this._getMaxValue(v) }) containsField(entitySchema, "key", keys => { if (keys.propertyRefs.some(pr => pr.name === propertySchema.name)) { definition.key = true } }) containsField(propertySchema, "annotations", v => { this.annotationHandler.processEntityPropertyAnnotations(entityFullName, propertySchema.name, v) }) return { name: propertySchema.name, definition } } createNavigationProperty(entityFullName, entitySchema, propertySchema) { if (!propertySchema) { propertySchema = entitySchema entitySchema = undefined } var definition: any = {} this.createTypeDefinition(propertySchema, definition) containsField(propertySchema, "nullable", v => { definition.nullable = parsebool(v, true), definition.required = parsebool(v, true) === false }) containsField(propertySchema, "partner", p => { definition.inverseProperty = p }) if (!definition.inverseProperty) { definition.inverseProperty = '$$unbound' } containsField(propertySchema, "referentialConstraints", p => { if (p.length) { definition.keys = p.map(r => r.property) definition.foreignKeys = p.map(r => r.referencedProperty) } }) containsField(propertySchema, "annotations", v => { this.annotationHandler.processEntityPropertyAnnotations(entityFullName, propertySchema.name, v) }) return { name: propertySchema.name, definition } } createEntityDefinition(entitySchema, entityFullName) { var props = (entitySchema.properties || []).map(this.createProperty.bind(this, entityFullName, entitySchema)) var navigationProps = (entitySchema.navigationProperties || []).map(this.createNavigationProperty.bind(this, entityFullName, entitySchema)) props = props.concat(navigationProps) var result = props.reduce((p, c) => { p[c.name] = c.definition return p }, {}) return result } createEntityType(entitySchema, namespace) { let baseType = (entitySchema.baseType ? entitySchema.baseType : this.options.baseType) let entityFullName = `${namespace}.${entitySchema.name}` let definition = this.createEntityDefinition(entitySchema, entityFullName) let staticDefinition: any = {} containsField(entitySchema, "openType", v => { if (parsebool(v, false)) { staticDefinition.openType = { value: true } } }) containsField(entitySchema, "annotations", v => { this.annotationHandler.processEntityAnnotations(entityFullName, v) }) return { namespace, typeName: entityFullName, baseType, params: [entityFullName, this.options.container, definition, staticDefinition], definition, type: 'entity' } } createEnumOption(enumFullName, entitySchema, propertySchema, i) { if (!propertySchema) { propertySchema = entitySchema entitySchema = undefined } var definition: any = { name: propertySchema.name, index: i } containsField(propertySchema, "value", value => { var v = +value if (!isNaN(v)) { definition.value = v } }) containsField(propertySchema, "annotations", v => { this.annotationHandler.processEntityPropertyAnnotations(enumFullName, propertySchema.name, v, true) }) return definition } createEnumDefinition(enumSchema, enumFullName) { var props = (enumSchema.members || []).map(this.createEnumOption.bind(this, enumFullName, enumSchema)) return props } createEnumType(enumSchema, namespace) { var self = this; let enumFullName = `${namespace}.${enumSchema.name}` let definition = this.createEnumDefinition(enumSchema, enumFullName) containsField(enumSchema, "annotations", v => { this.annotationHandler.processEntityAnnotations(enumFullName, v, true) }) return { namespace, typeName: enumFullName, baseType: '$data.Enum', params: [enumFullName, this.options.container, enumSchema.underlyingType, definition], definition, type: 'enum' } } createEntitySetProperty(entitySetSchema, contextSchema) { //var c = this.options.container var t = entitySetSchema.entityType //c.classTypes[c.classNames[entitySetSchema.entityType]] // || entitySetSchema.entityType var prop = { name: entitySetSchema.name, definition: { type: this.options.entitySetType, elementType: t } } containsField(entitySetSchema, "annotations", v => { this.annotationHandler.processEntitySetAnnotations(t, v) }) return prop } indexBy(fieldName, pick) { return [(p, c) => { p[c[fieldName]] = c[pick]; return p }, {}] } createContextDefinition(contextSchema, namespace) { var props = (contextSchema.entitySets || []).map(es => this.createEntitySetProperty(es, contextSchema)) var result = props.reduce(...this.indexBy("name", "definition")) return result } createContextType(contextSchema, namespace) { if (Array.isArray(contextSchema)) { throw new Error("Array type is not supported here") } var definition = this.createContextDefinition(contextSchema, namespace) var baseType = this.options.contextType var typeName = `${namespace}.${contextSchema.name}` var contextImportMethods = [] contextSchema.actionImports && contextImportMethods.push(...contextSchema.actionImports) contextSchema.functionImports && contextImportMethods.push(...contextSchema.functionImports) return { namespace, typeName, baseType, params: [typeName, this.options.container, definition], definition, type: 'context', contextImportMethods } } createMethodParameter(parameter, definition) { var paramDef = { name: parameter.name } this.createTypeDefinition(parameter, paramDef) definition.params.push(paramDef) } applyBoundMethod(actionInfo, ns, typeDefinitions, type) { let definition = { type, namespace: ns, returnType: null, params: [] } containsField(actionInfo, "returnType", value => { this.createReturnTypeDefinition(value, definition) }) let parameters = [].concat(actionInfo.parameters) parameters.forEach((p) => this.createMethodParameter(p, definition)) if (parsebool(actionInfo.isBound, false)) { let bindingParameter = definition.params.shift() if (bindingParameter.type === this.options.collectionBaseType) { let filteredContextDefinitions = typeDefinitions.filter((d) => d.namespace === ns && d.type === 'context') filteredContextDefinitions.forEach(ctx => { for (var setName in ctx.definition) { let set = ctx.definition[setName] if (set.elementType === bindingParameter.elementType) { set.actions = set.actions || {} set.actions[actionInfo.name] = definition } } }) } else { let filteredTypeDefinitions = typeDefinitions.filter((d) => d.typeName === bindingParameter.type && d.type === 'entity') filteredTypeDefinitions.forEach(t => { t.definition[actionInfo.name] = definition }) } } else { delete definition.namespace let methodFullName = ns + '.' + actionInfo.name let filteredContextDefinitions = typeDefinitions.filter((d) => d.type === 'context') filteredContextDefinitions.forEach((ctx) => { ctx.contextImportMethods.forEach(methodImportInfo => { if (methodImportInfo.action === methodFullName || methodImportInfo.function === methodFullName) { ctx.definition[actionInfo.name] = definition } }) }) } } processMetadata(createdTypes?) { var types = createdTypes || [] var typeDefinitions = [] var serviceMethods = [] containsField(this.metadata, "references", references => { references.forEach(ref => { containsField(ref, "includes", includes => { includes.forEach(include => { this.annotationHandler.addInclude(include) }) }) }) }) var dtsModules = {}; types.dts = '/*//////////////////////////////////////////////////////////////////////////////////////\n' + '////// Autogenerated by JaySvcUtil http://JayData.org for more info /////////\n' + '////// OData V4 TypeScript /////////\n' + '//////////////////////////////////////////////////////////////////////////////////////*/\n\n'; types.dts += JayData.src + '\n\n'; //types.dts += 'declare module Edm {\n' + Object.keys(dtsTypeMapping).map(t => ' type ' + t.split('.')[1] + ' = ' + dtsTypeMapping[t] + ';').join('\n') + '\n}\n\n'; var self = this; this.metadata.dataServices.schemas.forEach(schema => { var ns = schema.namespace dtsModules[ns] = ['declare module ' + ns + ' {', '}']; if (schema.enumTypes) { let enumTypes = schema.enumTypes.map(ct => this.createEnumType(ct, ns)) typeDefinitions.push(...enumTypes) } if (schema.complexTypes) { let complexTypes = schema.complexTypes.map(ct => this.createEntityType(ct, ns)) typeDefinitions.push(...complexTypes) } if (schema.entityTypes) { let entityTypes = schema.entityTypes.map(et => this.createEntityType(et, ns)) typeDefinitions.push(...entityTypes) } if (schema.actions) { serviceMethods.push(...schema.actions.map(m => defs => this.applyBoundMethod(m, ns, defs, '$data.ServiceAction'))) } if (schema.functions) { serviceMethods.push(...schema.functions.map(m => defs => this.applyBoundMethod(m, ns, defs, '$data.ServiceFunction'))) } if (schema.entityContainer) { let contexts = schema.entityContainer.map(ctx => this.createContextType(ctx, self.options.namespace || ns)) typeDefinitions.push(...contexts) } //console.log('annotations', schema) containsField(schema, 'annotations', (annotations) => { annotations.forEach((annot) => { containsField(annot, "target", target => { containsField(annot, "annotations", v => { this.annotationHandler.processSchemaAnnotations(target, v, annot.qualifier) }) }) }) }) }) serviceMethods.forEach(m => m(typeDefinitions)) var contextFullName; types.src = '(function(mod) {\n' + ' if (typeof exports == "object" && typeof module == "object") return mod(exports, require("jaydata/core")); // CommonJS\n' + ' if (typeof define == "function" && define.amd) return define(["exports", "jaydata/core"], mod); // AMD\n' + ' mod($data.generatedContext || ($data.generatedContext = {}), $data); // Plain browser env\n' + '})(function(exports, $data) {\n\n' + 'exports.$data = $data;\n\n' + 'var types = {};\n\n'; typeDefinitions = this.orderTypeDefinitions(typeDefinitions) types.push(...typeDefinitions.map((d) => { this.annotationHandler.preProcessAnnotation(d) this.storeExportable( d.params[0] ); var dtsm = dtsModules[d.namespace]; if (!dtsm){ dtsm = dtsModules[d.namespace] = ['declare module ' + d.namespace + ' {', '}']; } var dtsPart = []; var srcPart = ''; if (d.baseType == '$data.Enum') { dtsPart.push(' export enum ' + d.typeName.split('.').pop() + ' {'); if (d.params[3] && Object.keys(d.params[3]).length > 0){ Object.keys(d.params[3]).forEach(dp => dtsPart.push(' ' + d.params[3][dp].name + ',')); } srcPart += 'types["' + d.params[0] + '"] = $data.createEnum("' + d.params[0] + '", [\n' + Object.keys(d.params[3]).map(dp => ' ' + this._createPropertyDefString(d.params[3][dp])).join(',\n') + '\n]);\n\n'; } else { dtsPart.push(' export class ' + d.typeName.split('.').pop() + ' extends ' + d.baseType + ' {'); if (d.baseType == self.options.contextType){ dtsPart.push(' onReady(): Promise<' + d.typeName.split('.').pop() + '>;'); dtsPart.push(''); }else{ dtsPart.push(' constructor();'); var ctr = ' constructor(initData: { '; if (d.params[2] && Object.keys(d.params[2]).length > 0){ ctr += Object.keys(d.params[2]).map(dp => dp + '?: ' + (d.params[2][dp].type == 'Array' ? d.params[2][dp].elementType + '[]' : d.params[2][dp].type)).join('; '); } ctr += ' });'; dtsPart.push(ctr); dtsPart.push(''); } var typeName = d.baseType; if (d.baseType == this.options.contextType){ srcPart += 'exports.type = '; contextFullName = d.typeName; } srcPart += 'types["' + d.params[0] + '"] = ' + (typeName == this.options.baseType || typeName == this.options.contextType ? ('$data("' + typeName + '")') : 'types["' + typeName + '"]') + '.extend("' + d.params[0] + '", '; if (d.params[2] && Object.keys(d.params[2]).length > 0){ srcPart += '{\n' + Object.keys(d.params[2]).map(dp => ' ' + dp + ': ' + this._createPropertyDefString(d.params[2][dp])).join(',\n') + '\n}'; if (d.baseType == this.options.contextType){ Object.keys(d.params[2]).forEach(dp => dtsPart.push(' ' + dp + ': ' + this._typeToTS(d.params[2][dp].type, d.params[2][dp].elementType, d.params[2][dp]) + ';')); }else{ Object.keys(d.params[2]).forEach(dp => dtsPart.push(' ' + dp + ': ' + this._typeToTS(d.params[2][dp].type, d.params[2][dp].elementType, d.params[2][dp]) + ';')); } } else srcPart += 'null'; if (d.params[3] && Object.keys(d.params[3]).length > 0){ srcPart += ', {\n' + Object.keys(d.params[3]).map(dp => ' ' + dp + ': ' + this._createPropertyDefString(d.params[3][dp])).join(',\n') + '\n}'; } srcPart += ');\n\n'; } types.src += srcPart; dtsPart.push(' }'); dtsm.splice(1, 0, dtsPart.join('\n')); if (this.options.debug) console.log('Type generated:', d.params[0]); if (this.options.generateTypes !== false) { var baseType = this.options.container.resolveType(d.baseType) var type = baseType.extend.apply(baseType, d.params) this.annotationHandler.addAnnotation(type) return type } })); this.addExportables( types ); types.src += 'var ctxType = exports.type;\n' + 'exports.factory = function(config){\n' + ' if (ctxType){\n' + ' var cfg = $data.typeSystem.extend({\n' + ' name: "oData",\n' + ' oDataServiceHost: "' + (this.options.url && this.options.url.replace('/$metadata', '') || '') + '",\n' + ' withCredentials: ' + (this.options.withCredentials || false) + ',\n' + ' maxDataServiceVersion: "' + (this.options.maxDataServiceVersion || '4.0') + '"\n' + ' }, config);\n' + ' return new ctxType(cfg);\n' + ' }else{\n' + ' return null;\n' + ' }\n' + '};\n\n'; if (this.options.autoCreateContext) { var contextName = typeof this.options.autoCreateContext == 'string' ? this.options.autoCreateContext : 'context'; types.src += 'exports["' + contextName + '"] = exports.factory();\n\n'; } types.src += this.annotationHandler.annotationsText() types.src += '});'; // declare modules types.dts += Object.keys(dtsModules) .filter(m => dtsModules[m] && dtsModules[m].length > 2) .map(m => dtsModules[m].join('\n\n')) .join('\n\n'); // export modules types.dts += Object.keys(dtsModules) .filter(m => dtsModules[m] && dtsModules[m].length > 2) .map(m => m.split(".")[0]) .filter((v, i, a) => a.indexOf(v) === i) // distinct .map(m => '\n\nexport {'+m+' as '+m+'}') .join(''); if (contextFullName){ var mod = ['\n\nexport var type: typeof ' + contextFullName + ';', 'export var factory: (config:any) => ' + contextFullName + ';']; if (this.options.autoCreateContext){ var contextName = typeof this.options.autoCreateContext == 'string' ? this.options.autoCreateContext : 'context'; mod.push('export var ' + contextName + ': ' + contextFullName + ';'); } types.dts += mod.join('\n'); } if (this.options.generateTypes === false) { types.length = 0; } return types; } private _createPropertyDefString(definition){ if(definition.concurrencyMode){ return JSON.stringify(definition).replace('"concurrencyMode":"fixed"}', '"concurrencyMode":$data.ConcurrencyMode.Fixed}') } else { return JSON.stringify(definition) } } private _typeToTS(type, elementType, definition){ if (type == this.options.entitySetType){ return '$data.EntitySet<typeof ' + elementType + ', ' + elementType + '>'; }else if (type == '$data.Queryable'){ return '$data.Queryable<' + elementType + '>'; }else if (type == this.options.collectionBaseType){ return elementType + '[]'; }else if (type == '$data.ServiceAction'){ return '{ (' + (definition.params.length > 0 ? definition.params.map(p => p.name + ': ' + this._typeToTS(p.type, p.elementType, p)).join(', ') : '') + '): Promise<void>; }'; }else if (type == '$data.ServiceFunction'){ var t = this._typeToTS(definition.returnType, definition.elementType, definition); if (t.indexOf('$data.Queryable') < 0) t = 'Promise<' + t + '>'; return '{ (' + (definition.params.length > 0 ? definition.params.map(p => p.name + ': ' + this._typeToTS(p.type, p.elementType, p)).join(', ') : '') + '): ' + t + '; }'; }else return type; } orderTypeDefinitions(typeDefinitions) { let contextTypes = typeDefinitions.filter(t => t.type === 'context') let ordered = [] let dependants = [].concat(typeDefinitions.filter(t => t.type !== 'context')) let addedTypes let baseType = this.options.baseType let dependantCount = Number.MAX_VALUE while (dependants.length) { var dependantItems = [].concat(dependants) dependants.length = 0 dependantItems.forEach(typeDef => { if (dependantCount === dependantItems.length || typeDef.type !== "entity" || typeDef.baseType === baseType || ordered.some(t => t.typeName === typeDef.baseType) ) { ordered.push(typeDef) } else { dependants.push(typeDef) } }) dependantCount = dependantItems.length } return ordered.concat(contextTypes); } private storeExportable( typesStr ) { let typesArr = typesStr.split("."); let container = this.storedTypes; typesArr.forEach(( current )=> { if(typesArr[typesArr.length-1] === current) { container[current] = "@@" + typesStr+"@@"; } else { if( !container[current] ) { container[current] = {}; } container = container[current]; } }); } private addExportables( meta ) { for( let key in this.storedTypes ) { let types = "exports." + key + " = " + JSON.stringify(this.storedTypes[key], null, 2) .replace(/"@@/g,"types[\"") .replace(/@@"/g,"\"]") + ";\n\n"; meta.src += types } } }