UNPKG

breeze-client-labs

Version:

Breeze Labs are extensions and utilities for Breeze.js client apps that are not part of core breeze.

407 lines (369 loc) 17 kB
//#region Copyright, Version, and Description /* * Copyright 2015 IdeaBlade, Inc. All Rights Reserved. * Use, reproduction, distribution, and modification of this code is subject to the terms and * conditions of the IdeaBlade Breeze license, available at http://www.breezejs.com/license * * Author: Ward Bell * Version: 1.0.7 * -------------------------------------------------------------------------------- * Adds metadataHelper extensions to Breeze * Source: * https://github.com/Breeze/breeze.js.labs/blob/master/breeze.metadata-helper.js * * Depends on Breeze which it patches * * You can use these helpers when creating metadata by hand * to improve workflow and reduce data entry errors. * * The helpers reflect an opinion about developer workflow * that may or may not work for you. * Use these helpers "as is" or use for inspiration in creating your own. * * For example usage, see: * https://github.com/Breeze/breeze.js.samples/blob/master/net/DocCode/DocCode/tests/helpers/metadataOnClient.js * * For a discussion of how they work and why, see: * http://www.breezejs.com/documentation/metadata-by-hand#addTypeToStore * */ //#endregion // ReSharper disable InconsistentNaming (function (definition) { if (typeof breeze === "object") { definition(breeze); } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") { // CommonJS or Node var b = require('breeze-client'); definition(b); } else if (typeof define === "function" && define["amd"]) { // Requirejs / AMD define(['breeze-client'], definition); } else { throw new Error("Can't find breeze"); } }(function (breeze) { 'use strict'; // MetadataHelper constructor var helper = function (defaultNamespace, defaultAutoGeneratedKeyType) { this.defaultNamespace = defaultNamespace || ''; this.defaultAutoGeneratedKeyType = defaultAutoGeneratedKeyType || breeze.AutoGeneratedKeyType.None; }; helper.prototype = { constructor: helper, addDataService: addDataService, addTypeNameAsResource: addTypeNameAsResource, addTypeToStore: addTypeToStore, convertValidators: convertValidators, findEntityKey: findEntityKey, inferDefaultResourceName: inferDefaultResourceName, inferValidators: inferValidators, patch: patch, pluralize: pluralize, replaceDataPropertyAliases: replaceDataPropertyAliases, replaceNavPropertyAliases: replaceNavPropertyAliases, setDefaultAutoGeneratedKeyType: setDefaultAutoGeneratedKeyType, setDefaultNamespace: setDefaultNamespace, _hasOwnProperty: _hasOwnProperty, _isArray: _isArray }; breeze.config.MetadataHelper = helper; var DT = breeze.DataType; var Validator = breeze.Validator; function addDataService(store, serviceName) { store.addDataService( new breeze.DataService({ serviceName: serviceName }) ); } // Create the type from the definition hash and add the type to the store // fixes some defaults, infers certain validators, // add adds the type's "shortname" as a resource name function addTypeToStore(store, typeDef) { this.patch(typeDef); var type = typeDef.isComplexType ? new breeze.ComplexType(typeDef) : new breeze.EntityType(typeDef); store.addEntityType(type); this.inferValidators(type); this.addTypeNameAsResource(store, type); return type; } // Often helpful to have the type's 'shortName' available as a resource name // as when composing a query to be executed locally against the cache. // This function adds the type's 'shortName' as one of the resource names for the type. // Theoretically two types in different models could have the same 'shortName' // and thus we would associate the same resource name with the two different types. // While unlikely, breeze should offer a way to remove a resource name for a type. function addTypeNameAsResource(store, type) { if (!type.isComplexType) { store.setEntityTypeForResourceName(type.shortName, type); } } // While Breeze requires that the validators collection be defined with Validator instances // we support alternative expression of validators in JSON form (as if coming from the server) // Validator: // phone: { maxLength: 24, validators: [ Validator.phone() ] }, // JSON: // phone: { maxLength: 24, validators: [ {name: 'phone'} ] }, // This fn converts JSON to a Validator instance function convertValidators(typeName, propName, propDef) { var validators = propDef.validators; if (!_isArray(validators)) { //throw "{0}.{1}.validators must be an array".format(typeName, propName); // coerce to array instead of throwing propDef.validators = validators = [validators]; } validators.forEach(function (val, ix) { if (val instanceof Validator) return; try { validators[ix] = Validator.fromJSON(val); } catch (ex) { throw "{0}.{1}.validators[{2}] = '{3}' can't be converted to a known Validator." .format(typeName, propName, ix, JSON.stringify(val)); } }); } function findEntityKey(typeDef) { var dps = typeDef.dataProperties; var typenameId = typeDef.shortName.toLowerCase() + 'id'; for (var key in dps) { var prop = dps[key]; if (prop.isPartOfKey) { // found a key part; stop analysis return key; } // if type were Person, would look for 'id' or 'personid' if (prop.isPartOfKey == null) { // isPartOfKey is null or undefined; is it a candidate? var keyLc = key.toLowerCase(); if (keyLc === 'id' || keyLc === typenameId) { // infer this property is the key; stop further analysis prop.isPartOfKey = true; return key; } } } return null; } function inferDefaultResourceName(typeDef) { if (typeDef.defaultResourceName === undefined) { typeDef.defaultResourceName = this.pluralize(typeDef.shortName); } } function inferValidators(entityType) { entityType.dataProperties.forEach(function (prop) { if (!prop.isNullable) { // is required. addValidator(prop, Validator.required()); }; addValidator(prop, getDataTypeValidator(prop)); if (prop.maxLength != null && prop.dataType === DT.String) { addValidator(prop, Validator.maxLength({ maxLength: prop.maxLength })); } }); return entityType; function addValidator(prop, validator) { if (!validator) { return; } // no validator arg var valName = validator.name; var validators = prop.validators; var found = validators.filter(function (val) { return val.name == valName; }); if (!found.length) { // this validator has not already been specified validators.push(validator); } } function getDataTypeValidator(prop) { var dataType = prop.dataType; var validatorCtor = !dataType || dataType === DT.String ? null : dataType.validatorCtor; return validatorCtor ? validatorCtor() : null; } } function normalizeNavProp(key, prop) { switch (typeof (prop)) { case 'string': return { entityTypeName: prop }; case 'object': return prop; default: // nav prop name (key) is same as EntityName (PascalCased) var ename = key.substr(0, 1).toUpperCase() + key.substr(1); return { entityTypeName: ename }; } } // Patch some defaults in the type definition object // Todo: consider moving some of these patches into breeze itself function patch(typeDef) { var key, prop; if (typeDef.name) { // 'name' -> 'shortName' property renameAttrib(typeDef, 'name', 'shortName'); } var typeName = typeDef.shortName; // if no namespace specified, assign the helper defaultNamespace var namespace = typeDef.namespace = typeDef.namespace || this.defaultNamespace; if (!typeDef.isComplexType) { this.inferDefaultResourceName(typeDef); this.findEntityKey(typeDef); // if entityType lacks an autoGeneratedKeyType, use the helper defaultAutoGeneratedKeyType typeDef.autoGeneratedKeyType = typeDef.autoGeneratedKeyType || this.defaultAutoGeneratedKeyType; } var dps = typeDef.dataProperties; for (key in dps) { prop = dps[key]; this.replaceDataPropertyAliases(prop, key); if (prop.complexTypeName && prop.complexTypeName.indexOf(":#") === -1) { // if complexTypeName is unqualified, suffix with the entity's own namespace prop.complexTypeName += ':#' + namespace; } // key always required (not nullable) unless explictly nullable if (prop.isPartOfKey) { prop.isNullable = prop.isNullable === true; } if (prop.validators) { this.convertValidators(typeName, key, prop); } }; var navs = typeDef.navigationProperties; for (key in navs) { prop = navs[key] = normalizeNavProp(key, navs[key]); this.replaceNavPropertyAliases(prop,key); var propTypeName = prop.entityTypeName; // append the namespace to entityTypeName if missing var nsStart = propTypeName.indexOf(":#"); if (nsStart === -1) { // name is unqualified; append the namespace prop.entityTypeName += ':#' + namespace; } else { propTypeName = propTypeName.slice(0, nsStart); } // Infer that it's a child nav if no FKs, no invFKs, and not a collection if (prop.foreignKeyNames === undefined && prop.isScalar !== false && prop.invForeignKeyNames === undefined) { // Look for candidate FK property among the data properties as // (1) propertyname + id OR (2) unqualified typename + 'id' var candidate1 = key.toLowerCase() + 'id'; var candidate2 = propTypeName.toLowerCase() + 'id'; var fk = Object.keys(dps).filter( function (k) { k = k.toLowerCase(); return k === candidate1 || k === candidate2; })[0]; if (fk) { prop.foreignKeyNames = [fk]; } } if (prop.associationName === undefined) { var isParent = prop.isScalar === false || prop.invForeignKeyNames || prop.foreignKeyNames === undefined; // association name is 'ChildType_ParentType' prop.associationName = (isParent ? propTypeName : typeName) + '_' + (isParent ? typeName : propTypeName); } // coerce FK names to array var keyNames = prop.foreignKeyNames; if (keyNames && !_isArray(keyNames)) { prop.foreignKeyNames = [keyNames]; } keyNames = prop.invForeignKeyNames; if (keyNames && !_isArray(keyNames)) { prop.invForeignKeyNames = [keyNames]; } }; } function pluralize(word) { // Lame English pluralizer; plenty better on the web var len = word.length; switch (word[len - 1]) { case 's': // class -> classes return word + 'es'; case 'x': // box -> boxes return word + 'es'; case 'y': // fly -> flies return word.substr(0, len - 1) + 'ies'; default: // cat -> cats return word + 's'; } } function renameAttrib(obj, oldName, newName) { if (obj[newName] !== undefined) { throw "renameAttrib error; new name, '" + newName + "' is already defined for the object."; } obj[newName] = obj[oldName]; delete obj[oldName]; } /* * Support common aliases in DataProperty attributes to reduce tedium * type -> dataType * complex || complexType -> complexTypeName * null -> isNullable * max -> maxLength * default -> defaultValue */ function replaceDataPropertyAliases(prop, propertyName) { for (var key in prop) { if (_hasOwnProperty(prop, key)) { var keyLc = key.toLowerCase(); if (keyLc === 'type') { renameAttrib(prop, key, 'dataType'); } else if (keyLc === 'complex' || keyLc === 'complextype') { renameAttrib(prop, key, 'complexTypeName'); } else if (keyLc === 'max' && (prop.dataType === undefined || prop.dataType === DT.String)) { renameAttrib(prop, key, 'maxLength'); } else if (keyLc.indexOf('null') > -1 && key !== 'isNullable' && typeof (prop[key]) === 'boolean') { renameAttrib(prop, key, 'isNullable'); } else if (keyLc === 'required') { prop[key] = !prop[key]; renameAttrib(prop, key, 'isNullable'); } else if (keyLc.indexOf('key') > -1 && key !== 'isPartOfKey' && typeof (prop[key]) === 'boolean') { renameAttrib(prop, key, 'isPartOfKey'); } else if (keyLc === 'default') { renameAttrib(prop, key, 'defaultValue'); } else if (keyLc === 'isone' || keyLc === 'hasone') { renameAttrib(prop, key, 'isScalar'); // Mongo subdocuments could be collections of complex types } else if (keyLc === 'ismany' || keyLc === 'hasmany') { prop[key] = !prop[key]; renameAttrib(prop, key, 'isScalar'); } } } } /* * Support common aliases in Navigation Property attributes to reduce tedium * type -> entityTypeName * FK|FKs -> foreignKeyNames * invFK|invFKs -> invForeignKeyNames * assoc -> associationName * isOne | hasOne -> isScalar * isMany | hasMany -> isScalar with boolean flipped */ function replaceNavPropertyAliases(prop, propertyName) { for (var key in prop) { if (_hasOwnProperty(prop, key)) { var keyLc = key.toLowerCase(); if (keyLc === 'type') { renameAttrib(prop, key, 'entityTypeName'); } else if (keyLc === 'fk' || keyLc === 'fks' || keyLc === 'key') { renameAttrib(prop, key, 'foreignKeyNames'); } else if (keyLc === 'isone' || keyLc === 'hasone') { renameAttrib(prop, key, 'isScalar'); } else if (keyLc === 'ismany' || keyLc === 'hasmany') { prop[key] = !prop[key]; renameAttrib(prop, key, 'isScalar'); } else if (keyLc === 'invfk' || keyLc === 'invfks') { renameAttrib(prop, key, 'invForeignKeyNames'); } else if (keyLc.indexOf('assoc') > -1 && key !== 'associationName') { renameAttrib(prop, key, 'associationName'); } } } } function setDefaultAutoGeneratedKeyType(autoGeneratedKeyType) { this.defaultAutoGeneratedKeyType = autoGeneratedKeyType || breeze.AutoGeneratedKeyType.None; } function setDefaultNamespace(namespace) { this.defaultNamespace = namespace || ''; } function _hasOwnProperty(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } function _isArray(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; } }));