/** * A Schema is a collection of related {@link Ext.data.Model entities} and their respective * {@link Ext.data.schema.Association associations}. * * # Schema Instances * * By default a single instance of this class is created which serves as the schema for all * entities that do not have an explicit `{@link Ext.data.Model#cfg-schema schema}` config * either specified or inherited. This is sufficient in most cases. * * When an entity does specify a `{@link Ext.data.Model#cfg-schema schema}`, however, that * looks up (or creates) an instance for that entity class which is then inherited. * * **Important:** All related entities *must* belong to a single schema instance in order * to properly link up their associations. * * ## Configuring Schemas * * The best way to control the configuration of your `schema` is to define a base class for * all of your entities and use the `{@link Ext.data.Model#cfg-schema schema}` config like * this: * * Ext.define('MyApp.model.Base', { * extend: 'Ext.data.Model', * * // This configures the default schema because we don't assign an "id": * schema: { * // configs go here * } * }); * * **Note:** Only one explicit configuration can be applied to the default schema. In most * applications this will not be an issue. * * By using a base class for your entities you can ensure that the default schema is fully * configured before declaration of your classes proceeds. This is especially helpful if * you need to set the `namespace` for your schema (see below). * * ## Relative Naming * * When describing associations between entities, it is desirable to use shorthand names * that do not contain the common namespace portion. This is called the `entityName` as * opposed to its class name. By default, the `entityName` is the full class name. However, * if a namespace is used, the common portion can be discarded and we can derive a shorter name. * In the following code, `"MyApp.model.Foo"` has an `entityName` of `"Foo"` and the schema has * a `namespace` of "MyApp.model". * * If you use deeper nesting for entities, you may need to set the `namespace` config to * account for this. For example: * * Ext.define('MyApp.model.Base', { * extend: 'Ext.data.Model', * * schema: { * namespace: 'MyApp.model' * } * }); * * Your derived classes now will generate proper default `entityName` values even if they * have further namespaces. For example, "MyApp.model.foo.Thing" will produce "foo.Thing" * as the `entityName` given the above as a base class. * * # Association Naming * * There are various terms involved when describing associations. Perhaps the simplest * example that will clarify these terms is that of the common many-to-many association * of User and Group. * * * `entityName` - The names "User" and "Group" are the `entityName` values associated * with these two classes. These are derived from their full classnames (perhaps * something like "App.model.User" and "App.model.Group"). * * * `associationName` - When talking about associations, especially the many-to-many * variety, it is important to give them names. Associations are not owned by either of * the entities involved, so this name is similar to an `entityName`. In the case of * "User" and "Group", the default `associationName` would be "GroupUsers". * * * `left` and `right` - Associations describe a relationship between two entities. To * talk about specific associations we would use the `entityName` of the parties (such * as "User" or "Group"). When discussing associations in the abstract, however, it is * very helpful to be able to talk about the entities in an association in a general way. * In the case of the "GroupUsers" association, "User" is said to be the `left` while * "Group" is said to be the `right`. In a many-to-many association the selection of * `left` and `right` is arbitrary. When a foreign-key is involved, the `left` entity * is the one containing the foreign-key. * * ## Custom Naming Conventions * * One of the jobs the the `Schema` is to manage name generation (such as `entityName`). * This job is delegated to a class called the `namer`. If you need to generate names in * other ways, you can provide a custom `namer` for your classes: * * Ext.define('MyApp.model.Base', { * extend: 'Ext.data.Model', * * schema: { * namespace: 'MyApp.model', * namer: 'custom' * } * }); * * This will create a class using the alias "namer.custom". For example: * * Ext.define('MyApp.model.CustomNamer', { * extend: 'Ext.data.schema.Namer', * * alias: 'namer.custom', * ... * }); * * For details see the documentation for {@link Ext.data.schema.Namer Namer}. */Ext.define('Ext.data.schema.Schema', { mixins: [ 'Ext.mixin.Factoryable' ], requires: [ 'Ext.util.ObjectTemplate', 'Ext.data.schema.OneToOne', 'Ext.data.schema.ManyToOne', 'Ext.data.schema.ManyToMany', 'Ext.data.schema.Namer' ], alias: 'schema.default', // also configures Factoryable aliasPrefix: 'schema.', isSchema: true, /** * @property {String} type * The name of the schema's type. This should be the suffix of the `alias` for this * class following the "schema." prefix. For example, if the `alias` for a schema is * "schema.foo" then `type` should "foo". If an `alias` is specified on the derived * class, this property is set automatically. * @readonly */ type: 'default', statics: { /** * @property {Object} instances * A collection of `Schema` instances keyed by its `type`. * * var mySchema = Ext.data.schema.Schema.instances.mySchema; * * If the `Schema` may not have been created yet, use the {@link #get} method to * create the instance on first request: * * var mySchema = Ext.data.schema.Schema.get('mySchema'); * * @readonly * @private */ instances: {}, //<debug> // Method used for testing to clear cache for custom instances. clearInstance: function(id) { var schema = this.instances[id]; delete this.instances[id]; if (schema) { schema.clear(true); schema.destroy(); } }, //</debug> /** * Returns the `Schema` instance given its `id` or config object. If only the `id` * is specified, that `Schema` instance is looked up and returned. If there is no * instance already created, the `id` is assumed to be the `type`. For example: * * schema: 'foo' * * Would be created from the alias `"schema.foo"` and assigned the `id` of "foo" * as well. * * @param {String/Object} config The id, type or config object of the schema. * @param {String} [config.type] The type alias of the schema. A "schema." prefix * is added to this string, if provided, to complete the alias. This should match * match the "alias" of some class derived from `Ext.data.schema.Schema`. * @return {Ext.data.schema.Schema} The previously existing or newly created * instance. */ get: function(config) { var Schema = this, cache = Schema.instances, id = 'default', isString = config && Ext.isString(config), instance, newConfig; if (config) { if (config.isSchema) { return config; } id = isString ? config : (config.id || id); } if (!(instance = cache[id])) { cache[id] = instance = Schema.create(config); instance.id = id; } else if (config && !isString) { //<debug> if (id !== 'default') { Ext.raise('Only the default Schema instance can be reconfigured'); } //</debug> // When a Model contains a "schema" config object it is allowed to set the // configuration of the default schema. This is the default behavior of // this config on a model unless there is an "id" specified on it. So // the trick is that we already have an instance so we want to merge the // incoming config with the initial config of the default schema and then // make that the effective initial config. newConfig = Ext.merge({}, instance.config); Ext.merge(newConfig, config); instance.setConfig(newConfig); instance.config = newConfig; //<debug> instance.setConfig = function() { Ext.raise('The schema can only be reconfigured once'); }; //</debug> } return instance; }, lookupEntity: function(entity) { var ret = null, instances = this.instances, match, name, schema; if (entity) { if (entity.isEntity) { ret = entity.self; // a record } else if (Ext.isFunction(entity)) { // A function (assume that a constructor is the Class). ret = entity; } else if (Ext.isString(entity)) { ret = Ext.ClassManager.get(entity); // If we've found a singleton or non-Entity class by that name, ignore it. if (ret && (!ret.prototype || !ret.prototype.isEntity)) { ret = null; } if (!ret) { for (name in instances) { schema = instances[name]; match = schema.getEntity(entity); if (match) { if (ret) { Ext.raise('Ambiguous entity name "' + entity + '". Defined by schema "' + ret.schema.type + '" and "' + name + '"'); } ret = match; } } } if (!ret) { Ext.raise('No such Entity "' + entity + '".'); } } } return ret; } }, /** * @property {Number} assocCount The number of {@link Ext.data.schema.Association associations} * in this `schema`. * @readonly */ assocCount: 0, /** * @property {Number} entityCount The number of {@link Ext.data.Model entities} in this * `schema`. * @readonly */ entityCount: 0, config: { /** * @cfg {Object} defaultIdentifier * This config is used to initialize the `{@link Ext.data.Model#identifier}` config * for classes that do not define one. */ defaultIdentifier: null, /** * @cfg {Number} keyCheckDelay * The time to wait (in ms) before checking for null foreign keys on records that * will cause them to be dropped. This is useful for allowing records to be moved to * a different source. * @private * @since 5.0.1 */ keyCheckDelay: 10, /** * @cfg {String/Object/Ext.data.schema.Namer} namer * Specifies or configures the name generator for the schema. */ namer: 'default', /** * @cfg {String} namespace * The namespace for entity classes in this schema. */ namespace: null, /** * @cfg {Object/Ext.util.ObjectTemplate} proxy * This is a template used to produce `Ext.data.proxy.Proxy` configurations for * Models that do not define an explicit `{@link Ext.data.Model#cfg-proxy proxy}`. * * This template is processed with the Model class as the data object which means * any static properties of the Model are available. The most useful of these are * * * `prefix` - The `urlPrefix` property of this instance. * * `entityName` - The {@link Ext.data.Model#entityName name} of the Model * (for example, "User"). * * `schema` - This instance. */ proxy: { type: 'ajax', url: '{prefix}/{entityName}' }, /** * @cfg {String} [urlPrefix=""] * This is the URL prefix used for all requests to the server. It could be something * like "/~api". This value is included in the `proxy` template data as "prefix". */ urlPrefix: '' }, onClassExtended: function(cls, data) { var alias = data.alias; if (alias && !data.type) { if (!Ext.isString(alias)) { alias = alias[0]; } cls.prototype.type = alias.substring(this.prototype.aliasPrefix.length); } }, constructor: function(config) { this.initConfig(config); this.clear(); }, //------------------------------------------------------------------------- // Config // <editor-fold> applyDefaultIdentifier: function(identifier) { return identifier && Ext.Factory.dataIdentifier(identifier); }, applyNamer: function(namer) { var ret = Ext.data.schema.Namer.create(namer); ret.schema = this; return ret; }, applyNamespace: function(namespace) { var end; if (namespace) { end = namespace.length - 1; if (namespace.charAt(end) !== '.') { namespace += '.'; } } return namespace; }, applyProxy: function(proxy) { return Ext.util.ObjectTemplate.create(proxy); }, // </editor-fold> //------------------------------------------------------------------------- // Public eachAssociation: function(fn, scope) { var associations = this.associations, name; for (name in associations) { if (associations.hasOwnProperty(name)) { if (fn.call(scope, name, associations[name]) === false) { break; } } } }, eachEntity: function(fn, scope) { var entities = this.entities, name; for (name in entities) { if (entities.hasOwnProperty(name)) { if (fn.call(scope, name, entities[name].cls) === false) { break; } } } }, /** * Returns an `Association` by name. * @param {String} name The name of the association. * @return {Ext.data.schema.Association} The association instance. */ getAssociation: function(name) { var entry = this.associations[name]; return entry || null; }, /** * Returns an entity by name. * @param {String} name The name of the entity * @return {Ext.data.Model} The entity class. */ getEntity: function(name) { var entry = this.entityClasses[name] || this.entities[name]; return (entry && entry.cls) || null; }, /** * Get the entity name taking into account the {@link #namespace}. * @param {String/Ext.data.Model} cls The model class or name of the class. * @return {String} The entity name */ getEntityName: function(cls) { var ns = this.getNamespace(), index, name; if (typeof cls === 'string') { name = cls; } else { name = cls.$className || null; } if (name) { // if (not anonymous class) if (ns) { index = ns.length; if (name.substring(0, index) !== ns) { return name; } } if (index) { name = name.substring(index); } } return name; }, /** * Checks if the passed entity has attached associations that need to be read when * using nested loading. * * @param {String/Ext.Class/Ext.data.Model} name The name, instance, or Model class. * @return {Boolean} `true` if there are associations attached to the entity. */ hasAssociations: function(name) { name = name.entityName || name; return !!this.associationEntityMap[name]; }, /** * Checks if an entity is defined * @param {String/Ext.data.Model} entity The name or model * @return {Boolean} True if this entity is defined */ hasEntity: function(entity) { var name = this.getEntityName(entity); return !!(this.entities[name] || this.entityClasses[name]); }, //------------------------------------------------------------------------- // Protected /** * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an * entity. * * This is the ideal method to override in a derived class if the standard, default * naming conventions need to be adjusted. In the override, apply whatever logic is * appropriate to determine the missing values and pass along the proper results to * this method in the `callParent`. * * @param {Ext.Class} entityType A class derived from `Ext.data.Model`. * * @param {String} matrixName The name of the matrix association. * * @param {String} [relation] A base name for the matrix. For information about the * meaning of this see {@link Ext.data.schema.Schema#ManyToMany}. * * @param {Object} left The descriptor for the "left" of the matrix. * @param {String} left.type The type of the entity on the "left" of the matrix. * * @param {String} [left.field] The name of the field in the matrix table for the "left" * side entity. If not provided, this defaults to the `left.type` name * {@link Ext.util.Inflector#singularize singularized} and uncapitalized followed by * "Id". For example, "userId" for a `left.type` of "Users". * * @param {String} [left.role] The name of the relationship from the `left.type` to the * `right.type`. If not provided, this defaults to the `left.type` name * {@link Ext.util.Inflector#pluralize pluralized} and uncapitalized. For example, * "users" for a `left.type` of "User". * * @param {Object} right The descriptor for the "right" of the matrix. * @param {String} right.type The type of the entity on the "right" of the matrix. * * @param {String} [right.field] The name of the field in the matrix table for the * "right" side entity. If not provided, this defaults in the same way as `left.field` * except this is based on `right.type`. * * @param {String} [right.role] The name of the relationship from the `right.type` to * the `left.type`. If not provided, this defaults in the same way as `left.role` * except this is based on `right.type`. * * @protected */ addMatrix: function(entityType, matrixName, relation, left, right) { var me = this, namer = me.getNamer(), associations = me.associations, entities = me.entities, leftType = left.type, rightType = right.type, leftField = left.field || namer.apply('idField', leftType), rightField = right.field || namer.apply('idField', rightType), leftRole = left.role || namer.matrixRole(relation, leftType), rightRole = right.role || namer.matrixRole(relation, rightType), matrix, leftEntry, rightEntry; leftEntry = entities[leftType] || (entities[leftType] = { cls: null, name: leftType, associations: {} }); rightEntry = entities[rightType] || (entities[rightType] = { cls: null, name: rightType, associations: {} }); ++me.assocCount; associations[matrixName] = matrix = new Ext.data.schema.ManyToMany({ name: matrixName, schema: me, definedBy: entityType, left: { cls: leftEntry.cls, type: leftType, role: leftRole, field: leftField, associationKey: left.associationKey }, right: { cls: rightEntry.cls, type: rightType, role: rightRole, field: rightField, associationKey: right.associationKey } }); leftEntry.associations[matrix.right.role] = matrix.right; rightEntry.associations[matrix.left.role] = matrix.left; if (leftEntry.cls) { me.associationEntityMap[leftEntry.cls.entityName] = true; } if (rightEntry.cls) { me.associationEntityMap[rightEntry.cls.entityName] = true; } me.decorateModel(matrix); }, /** * Adds a {@link Ext.data.Field#reference reference} field association for an entity * to this `schema`. * * This is the ideal method to override in a derived class if the standard, default * naming conventions need to be adjusted. In the override, apply whatever logic is * appropriate to determine the missing values and pass along the proper results to * this method in the `callParent`. * * @param {Ext.Class} entityType A class derived from `Ext.data.Model`. * * @param {Ext.data.field.Field} referenceField The `field` with the `reference` config. * * @param {Object} [descr] The `reference` descriptor from the `referenceField` if one * was given in the field definition. * * @param {String} [descr.association] The name of the association. If empty or null, this * will be derived from `entityType`, `role`, `inverse` and * `referenceField.unique`. * * @param {String} [descr.role] The name of the relationship from `entityType` to the target * `type`. If not specified, the default is the `referenceField.name` (minus any "Id" * suffix if present). * * @param {String} [descr.inverse] The name of the relationship from the target `type` * to the `entityType`. If not specified, this is derived from the * {@link Ext.data.Model#entityName entityName} of the `entityType` * ({@link Ext.util.Inflector#singularize singularized} or * {@link Ext.util.Inflector#pluralize pluralized} based on `referenceField.unique`). * * @param {String} descr.type The {@link Ext.data.Model#entityName entityName} of the * target of the reference. * * @param {Boolean} [unique=false] Indicates if the reference is one-to-one. * @param {Boolean} [dupeCheck] (private) * * @protected */ addReference: function(entityType, referenceField, descr, unique, dupeCheck) { var me = this, namer = me.getNamer(), entities = me.entities, associations = me.associations, entityName = entityType.entityName, association = descr.association, child = descr.child, parent = descr.parent, rightRole = descr.role, // Allow { child: 'OrderItem' } or the reverse (for one-to-one mostly): rightType = descr.type || parent || child, leftVal = descr.inverse, left = Ext.isString(leftVal) ? { role: leftVal } : leftVal, leftRole = left && left.role, entry, T; if (!rightRole) { // In a FK association, the left side has the key in a field named something // like "orderId". The default implementation of "fieldRole" namer is to drop // the id suffix which gives is the role of the right side. if (!referenceField || descr.legacy) { rightRole = namer.apply('uncapitalize', rightType); } else { rightRole = namer.apply('fieldRole', referenceField.name); } } if (!leftRole) { leftRole = namer.inverseFieldRole(entityName, unique, rightRole, rightType); } if (!association) { if (unique) { association = namer.oneToOne(entityType, leftRole, rightType, rightRole); } else { association = namer.manyToOne(entityType, leftRole, rightType, rightRole); } } if (dupeCheck && association in associations) { if (dupeCheck(associations[association], association, leftRole, rightRole) !== false) { return; } } //<debug> if (association in associations) { Ext.raise('Duplicate association: "' + association + '" declared by ' + entityName + (referenceField ? ('.' + referenceField.name) : '') + ' (collides with ' + associations[association].definedBy.entityName + ')'); } if (referenceField && referenceField.definedBy === entities[rightType]) { Ext.raise('ForeignKey reference should not be owned by the target model'); } //</debug> // Lookup the entry for the target of the reference. Since it may not as yet be // defined, we may need to create the entry. entry = entities[rightType] || (entities[rightType] = { cls: null, name: rightType, associations: {} }); // as a field w/reference we are always "left": T = unique ? Ext.data.schema.OneToOne : Ext.data.schema.ManyToOne; association = new T({ name: association, // Note: "parent" or "child" can be strings so don't assume otherwise owner: child ? 'left' : (parent ? 'right' : null), definedBy: entityType, schema: me, field: referenceField, nullable: referenceField ? !!referenceField.allowBlank : true, left: { cls: entityType, type: entityName, role: leftRole, extra: left }, right: { cls: entry.cls, type: rightType, role: rightRole, extra: descr }, meta: descr }); // Add the left and right association "sides" to the appropriate collections, but // remember that the right-side entity class may not yet be declared (that's ok as // we store the associations in the entry): entityType.associations[rightRole] = association.right; entry.associations[leftRole] = association.left; if (referenceField) { // Store the role on the FK field. This "upgrades" legacy associations to the // new "field.reference" form. referenceField.reference = association.right; entityType.references.push(referenceField); } ++me.assocCount; me.associationEntityMap[entityName] = true; if (entry.cls) { me.associationEntityMap[entry.cls.entityName] = true; } associations[association.name] = association; if (association.right.cls) { me.decorateModel(association); } }, //------------------------------------------------------------------------- privates: { /** * Adds an {@link Ext.data.Model entity} to this `schema`. * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model}. * @private */ addEntity: function(entityType) { var me = this, entities = me.entities, entityName = entityType.entityName, entry = entities[entityName], fields = entityType.fields, associations, field, i, length, name; if (!entry) { entities[entityName] = entry = { name: entityName, associations: {} }; } //<debug> else if (entry.cls) { Ext.raise('Duplicate entity name "' + entityName + '": ' + entry.cls.$className + ' and ' + entityType.$className); } //</debug> else { associations = entry.associations; for (name in associations) { // the associations collection describes the types to which this entity is // related, but the inverse descriptors need this entityType: associations[name].inverse.cls = entityType; me.associationEntityMap[entityName] = true; // We already have an entry, which means other associations have likely // been added for us, so go ahead and do the inverse decoration me.decorateModel(associations[name].association); } } entry.cls = entityType; entityType.prototype.associations = entityType.associations = entry.associations; me.entityClasses[entityType.$className] = entry; ++me.entityCount; for (i = 0, length = fields.length; i < length; ++i) { field = fields[i]; if (field.reference) { me.addReferenceDescr(entityType, field); } } }, /** * Adds the matrix associations of an {@link Ext.data.Model entity} to this `schema`. * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}. * @param {Object/String[]} matrices The manyToMany matrices for the class. * @private */ addMatrices: function(entityType, matrices) { var me = this, i, length, matrixName; if (Ext.isString(matrices)) { me.addMatrixDescr(entityType, null, matrices); } else if (matrices[0]) { // if (isArray) for (i = 0, length = matrices.length; i < length; ++i) { me.addMatrixDescr(entityType, null, matrices[i]); } } else { for (matrixName in matrices) { me.addMatrixDescr(entityType, matrixName, matrices[matrixName]); } } }, /** * Adds an entry from a {@link Ext.data.schema.ManyToMany matrix config} declared by an * {@link Ext.data.Model entity}. * * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Entity}. * @param {String} [matrixName] The name of the matrix association. * @param {String/Object} matrixDef A {@link Ext.data.schema.ManyToMany matrix config} * declared by an {@link Ext.data.Model entity}. * @private */ addMatrixDescr: function(entityType, matrixName, matrixDef) { var me = this, entityName = entityType.entityName, associations = me.associations, namer = me.getNamer(), left = matrixDef.left, right = matrixDef.right, last, relation; if (Ext.isString(matrixDef)) { if (matrixDef.charAt(0) === '#') { // "#User" (entity is on the left) /* * Ext.define('User', { * extend: 'Ext.data.Model', * manyToMany: '#Group' * }); */ left = { type: entityName }; // User right = { type: matrixDef.substring(1) }; // Group } else if (matrixDef.charAt(last = matrixDef.length - 1) === '#') { // "User#" /* * Ext.define('Group', { * extend: 'Ext.data.Model', * manyToMany: 'User#' * }); */ left = { type: matrixDef.substring(0, last) }; // User right = { type: entityName }; // Group } else if (namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef)) { /* * Ext.define('Group', { * extend: 'Ext.data.Model', * manyToMany: 'User' * }); */ left = { type: entityName }; // Group right = { type: matrixDef }; // User } else { /* * Ext.define('User', { * extend: 'Ext.data.Model', * manyToMany: 'Group' * }); */ left = { type: matrixDef }; // Group right = { type: entityName }; // User } } else { //<debug> Ext.Assert.isString(matrixDef.type, 'No "type" for manyToMany in ' + entityName); //</debug> relation = matrixDef.relation; /* eslint-disable-next-line max-len */ if (left || (!right && namer.apply('multiRole', entityName) < namer.apply('multiRole', matrixDef.type))) { if (!left || left === true) { /* * Ext.define('User', { * extend: 'Ext.data.Model', * manyToMany: { * type: 'Group', * left: true * } * }); */ left = { type: entityName }; // User } else { /* * Ext.define('User', { * extend: 'Ext.data.Model', * manyToMany: { * type: 'Group', * left: { * role: 'useroids' * } * } * }); */ left = Ext.apply({ type: entityName }, left); // User } right = matrixDef; // Group } else { if (!right || right === true) { /* * Ext.define('Group', { * extend: 'Ext.data.Model', * manyToMany: { * type: 'User', * right: true * } * }); */ right = { type: entityName }; // Group } else { /* * Ext.define('Group', { * extend: 'Ext.data.Model', * manyToMany: { * type: 'User', * right: { * role: 'groupoids' * } * } * }); */ right = Ext.apply({ type: entityName }, right); // Group } left = matrixDef; // User } } if (!matrixName) { matrixName = namer.manyToMany(relation, left.type, right.type); } if (!(matrixName in associations)) { me.addMatrix(entityType, matrixName, relation, left, right); } //<debug> // // In the case of a matrix association, both sides may need to declare it to allow // them to be used w/o the other present. In development mode, we want to check // that they declare the same thing! // else { /* eslint-disable-next-line vars-on-top, one-var */ var entry = associations[matrixName], before = [entry.kind, entry.left.type, entry.left.role, entry.left.field, entry.right.type, entry.right.role, entry.right.field].join('|'), after; // Call back in to bypass this check and realize the new association: delete associations[matrixName]; me.addMatrix(entityType, matrixName, relation, left, right); after = associations[matrixName]; // Restore the originals so we match production behavior (for testing) associations[matrixName] = entry; entry.left.cls.associations[entry.right.role] = entry.right; entry.right.cls.associations[entry.left.role] = entry.left; --me.assocCount; // Now we can compare the old and the new to see if they are the same. after = [after.kind, after.left.type, after.left.role, after.left.field, after.right.type, after.right.role, after.right.field].join('|'); if (before != after) { // eslint-disable-line eqeqeq Ext.log.warn(matrixName + '(' + entry.definedBy.entityName + '): ' + before); Ext.log.warn(matrixName + '(' + entityName + '): ' + after); Ext.raise('Conflicting association: "' + matrixName + '" declared by ' + entityName + ' was previously declared by ' + entry.definedBy.entityName); } } //</debug> }, /** * Adds a {@link Ext.data.Field#reference reference} {@link Ext.data.Field field} * association for an entity to this `schema`. This method decodes the `reference` * config of the `referenceField` and calls {@link #addReference}. * * @param {Ext.Class} entityType A class derived from {@link Ext.data.Model Model}. * @param {Ext.data.Field} referenceField The `field` with the `reference` config. * @private */ addReferenceDescr: function(entityType, referenceField) { var me = this, descr = referenceField.$reference; if (Ext.isString(descr)) { descr = { type: descr }; } else { descr = Ext.apply({}, descr); } me.addReference(entityType, referenceField, descr, referenceField.unique); }, addBelongsTo: function(entityType, assoc) { this.addKeylessSingle(entityType, assoc, false); }, addHasOne: function(entityType, assoc) { this.addKeylessSingle(entityType, assoc, true); }, addKeylessSingle: function(entityType, assoc, unique) { var foreignKey, referenceField; assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc)); assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type); foreignKey = assoc.foreignKey || (assoc.type.toLowerCase() + '_id'); referenceField = entityType.getField(foreignKey); assoc.fromSingle = true; if (referenceField) { referenceField.$reference = assoc; referenceField.unique = true; assoc.legacy = true; //<debug> Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference'); //</debug> } this.addReference(entityType, referenceField, assoc, unique); }, addHasMany: function(entityType, assoc) { var me = this, entities = me.entities, pending = me.pending, cls, name, referenceField, target, foreignKey, inverseOptions, child, declaredInverse; assoc = Ext.apply({}, this.checkLegacyAssociation(entityType, assoc)); assoc.type = this.getEntityName(assoc.child || assoc.parent || assoc.type); name = assoc.type; target = entities[name]; cls = target && target.cls; if (cls) { name = entityType.entityName; foreignKey = assoc.foreignKey || (name.toLowerCase() + '_id'); delete assoc.foreignKey; // The assoc is really the inverse, so we only set the minimum. // We copy the inverse from assoc and apply it over assoc! declaredInverse = Ext.apply({}, assoc.inverse); delete assoc.inverse; inverseOptions = Ext.apply({}, assoc); delete inverseOptions.type; assoc = Ext.apply({ type: name, inverse: inverseOptions }, declaredInverse); child = inverseOptions.child; if (child) { delete inverseOptions.child; assoc.parent = name; } referenceField = cls.getField(foreignKey); if (referenceField) { referenceField.$reference = assoc; assoc.legacy = true; //<debug> Ext.log.warn('Using foreignKey is deprecated, use a keyed association. ' + 'See Ext.data.field.Field.reference'); //</debug> } // We already have the entity, we can process it me.addReference(cls, referenceField, assoc, false //<debug> /* eslint-disable-next-line comma-style */ , function(association, name, leftRole, rightRole) { // Check to see if the user has used belongsTo/hasMany in conjunction. var result = !!association.meta.fromSingle && cls === association.left.cls, l, r; if (result) { l = cls.entityName; r = entityType.entityName; Ext.raise('hasMany ("' + r + '") and belongsTo ("' + l + '") should not be used in conjunction to declare ' + 'a relationship. Use only one.'); } return result; } //</debug> ); } else { // Pending, push it in the queue for when we load it if (!pending[name]) { pending[name] = []; } pending[name].push([entityType, assoc]); } }, checkLegacyAssociation: function(entityType, assoc) { var name; if (Ext.isString(assoc)) { assoc = { type: assoc }; } else { assoc = Ext.apply({}, assoc); } if (assoc.model) { assoc.type = assoc.model; // TODO: warn delete assoc.model; } name = assoc.associatedName || assoc.name; if (name) { // TODO: warn delete assoc.associatedName; delete assoc.name; assoc.role = name; } return assoc; }, afterKeylessAssociations: function(cls) { var pending = this.pending, name = cls.entityName, mine = pending[name], i, len; if (mine) { for (i = 0, len = mine.length; i < len; ++i) { this.addHasMany.apply(this, mine[i]); } delete pending[name]; } }, clear: function(clearNamespace) { // for testing var me = this, timer = me.timer; delete me.setConfig; if (timer) { window.clearTimeout(timer); me.timer = null; } me.associations = {}; me.associationEntityMap = {}; me.entities = {}; me.entityClasses = {}; me.pending = {}; me.assocCount = me.entityCount = 0; if (clearNamespace) { me.setNamespace(null); } }, constructProxy: function(Model) { var me = this, data = Ext.Object.chain(Model), proxy = me.getProxy(); data.schema = me; data.prefix = me.getUrlPrefix(); return proxy.apply(data); }, applyDecoration: function(role) { var me = this, // To decorate a role like "users" (of a User / Group matrix) we need to add // getter/setter methods to access the "users" collection ... to Group! All // other data about the "users" role and the User class belong to the given // "role" but the receiver class is the inverse. cls = role.inverse.cls, namer = me.getNamer(), getterName, setterName, proto; // The cls may not be loaded yet, so we need to check if it is before // we can decorate it. if (cls && !role.decorated) { role.decorated = true; proto = cls.prototype; if (!(getterName = role.getterName)) { role.getterName = getterName = namer.getterName(role); } proto[getterName] = role.createGetter(); // Not all associations will create setters if (role.createSetter) { if (!(setterName = role.setterName)) { role.setterName = setterName = namer.setterName(role); } proto[setterName] = role.createSetter(); } } }, decorateModel: function(association) { this.applyDecoration(association.left); this.applyDecoration(association.right); }, processKeyChecks: function(processAll) { var me = this, keyCheckQueue = me.keyCheckQueue, timer = me.timer, len, i, item; if (timer) { window.clearTimeout(timer); me.timer = null; } if (!keyCheckQueue) { return; } // It's possible that processing a drop may cause another drop // to occur. If we're trying to forcibly resolve the state, then // we need to trigger all the drops at once. With processAll: false, // the loop will jump out after the first iteration. do { keyCheckQueue = me.keyCheckQueue; me.keyCheckQueue = []; for (i = 0, len = keyCheckQueue.length; i < len; ++i) { item = keyCheckQueue[i]; item.role.checkKeyForDrop(item.record); } } while (processAll && me.keyCheckQueue.length); }, queueKeyCheck: function(record, role) { var me = this, keyCheckQueue = me.keyCheckQueue, timer = me.timer; if (!keyCheckQueue) { me.keyCheckQueue = keyCheckQueue = []; } keyCheckQueue.push({ record: record, role: role }); if (!timer) { me.timer = timer = Ext.defer(me.processKeyChecks, me.getKeyCheckDelay(), me); } }, rankEntities: function() { var me = this, entities = me.entities, entityNames = Ext.Object.getKeys(entities), length = entityNames.length, entityType, i; me.nextRank = 1; // We do an alpha sort to make the results more stable. entityNames.sort(); for (i = 0; i < length; ++i) { entityType = entities[entityNames[i]].cls; if (!entityType.rank) { me.rankEntity(entityType); } } //<debug> me.topoStack = null; // cleanup diagnostic stack //</debug> }, rankEntity: function(entityType) { var associations = entityType.associations, associatedType, role, roleName; //<debug> /* eslint-disable-next-line vars-on-top, one-var */ var topoStack = this.topoStack || (this.topoStack = []), entityName = entityType.entityName; topoStack.push(entityName); if (entityType.rank === 0) { Ext.raise(entityName + " has circular foreign-key references: " + topoStack.join(" --> ")); } entityType.rank = 0; // mark as "adding" so we can detect cycles //</debug> for (roleName in associations) { role = associations[roleName]; // The role describes the thing to which entityType is associated, so we // want to know about *this* type and whether it has a foreign-key to the // associated type. The left side is the FK owner so if the associated // type is !left then entityType is left. // if (!role.left && role.association.field) { // This entityType has a foreign-key to the associated type, so add // that type first. associatedType = role.cls; if (!associatedType.rank) { this.rankEntity(associatedType); } } } entityType.rank = this.nextRank++; //<debug> topoStack.pop(); //</debug> } } // private});