]> git.proxmox.com Git - extjs.git/blame - extjs/packages/core/src/data/Session.js
add extjs 6.0.1 sources
[extjs.git] / extjs / packages / core / src / data / Session.js
CommitLineData
6527f429
DM
1/**\r
2 * This class manages models and their associations. Instances of `Session` are typically\r
3 * associated with some `Component` (perhaps the Viewport or a Window) and then used by\r
4 * their `{@link Ext.app.ViewModel view models}` to enable data binding.\r
5 *\r
6 * The primary job of a Session is to manage a collection of records of many different\r
7 * types and their associations. This often starts by loading records when requested (via\r
8 * bind - see below) and culminates when it is time to save to the server.\r
9 *\r
10 * Because the Session tracks all records it loads, it ensures that for any given type of\r
11 * model, only one record exists with a given `id`. This means that all edits of that\r
12 * record are properly targeted at that one instance.\r
13 *\r
14 * Similarly, when associations are loaded, the `Ext.data.Store` created to hold the\r
15 * associated records is tracked by the Session. So all requests for the "OrderItems" of\r
16 * a particular Order id will result in the same Store. Adding and removing items from\r
17 * that Order then is sure to remain consistent.\r
18 *\r
19 * # Data\r
20 *\r
21 * Since the Session is managing all this data, there are several methods it provides\r
22 * to give convenient access to that data. The most important of these is `update` and\r
23 * `getChanges`.\r
24 *\r
25 * The `update` and `getChanges` methods both operate on object that contains a summary\r
26 * of records and associations and different CRUD operations.\r
27 *\r
28 * ## Saving\r
29 *\r
30 * There are two basic ways to save the contents of a Session: `getChanges` and\r
31 * `getSaveBatch`. We've already seen `getChanges`. The data contained in the CRUD object\r
32 * can be translated into whatever shape is needed by the server.\r
33 *\r
34 * To leverage the `{@link Ext.data.Model#proxy proxy}` facilities defined by each Model\r
35 * class, there is the `getSaveBatch` method. That method returns an `Ext.data.Batch`\r
36 * object populated with the necessary `create`, `update` and `destory` operations to\r
37 * save all of the changes in the Session.\r
38 * \r
39 * @since 5.0.0\r
40 */\r
41Ext.define('Ext.data.Session', {\r
42 requires: [\r
43 'Ext.data.schema.Schema',\r
44 'Ext.data.Batch',\r
45 'Ext.data.matrix.Matrix',\r
46 'Ext.data.session.ChangesVisitor',\r
47 'Ext.data.session.ChildChangesVisitor',\r
48 'Ext.data.session.BatchVisitor'\r
49 ],\r
50\r
51 isSession: true,\r
52\r
53 config: {\r
54 /**\r
55 * @cfg {String/Ext.data.schema.Schema} schema\r
56 */\r
57 schema: 'default',\r
58\r
59 /**\r
60 * @cfg {Ext.data.Session} parent\r
61 * The parent session for this session.\r
62 */\r
63 parent: null,\r
64\r
65 /**\r
66 * @cfg {Boolean} autoDestroy\r
67 * `true` to automatically destroy this session when a component it is attached\r
68 * to is destroyed. This should be set to false if the session is intended to be\r
69 * used across multiple root level components.\r
70 *\r
71 * @since 5.0.1\r
72 */\r
73 autoDestroy: true,\r
74\r
75 crudProperties: {\r
76 create: 'C',\r
77 read: 'R',\r
78 update: 'U',\r
79 drop: 'D'\r
80 }\r
81 },\r
82\r
83 destroyed: false,\r
84\r
85 crudOperations: [{\r
86 type: 'R',\r
87 entityMethod: 'readEntities'\r
88 }, {\r
89 type: 'C',\r
90 entityMethod: 'createEntities'\r
91 }, {\r
92 type: 'U',\r
93 entityMethod: 'updateEntities'\r
94 }, {\r
95 type: 'D',\r
96 entityMethod: 'dropEntities'\r
97 }],\r
98\r
99 crudKeys: {\r
100 C: 1,\r
101 R: 1,\r
102 U: 1,\r
103 D: 1\r
104 },\r
105\r
106 constructor: function (config) {\r
107 var me = this;\r
108\r
109 /*\r
110 * {\r
111 * User: {\r
112 * 1: {\r
113 * record: user1Instance,\r
114 * refs: {\r
115 * posts: {\r
116 * 101: post101Instance,\r
117 * 102: post202Instance\r
118 * }\r
119 * }\r
120 * }\r
121 * }\r
122 * }\r
123 */\r
124 me.data = {};\r
125\r
126 /*\r
127 * {\r
128 * UserGroups: new Ext.data.matrix.Matrix({\r
129 * association: UserGroups\r
130 * })\r
131 * }\r
132 */\r
133 me.matrices = {};\r
134\r
135 me.identifierCache = {};\r
136\r
137 // Bind ourselves so we're always called in our own scope.\r
138 me.recordCreator = me.recordCreator.bind(me);\r
139\r
140 me.initConfig(config);\r
141 },\r
142\r
143 destroy: function () {\r
144 var me = this,\r
145 matrices = me.matrices,\r
146 data = me.data,\r
147 entityName, entities,\r
148 record, id;\r
149\r
150 for (id in matrices) {\r
151 matrices[id].destroy();\r
152 }\r
153\r
154 for (entityName in data) {\r
155 entities = data[entityName];\r
156 for (id in entities) {\r
157 record = entities[id].record;\r
158 if (record) {\r
159 // Clear up any source if we pushed one on, remove\r
160 // the session reference\r
161 record.$source = record.session = null;\r
162 }\r
163 }\r
164 }\r
165\r
166 me.recordCreator = me.matrices = me.data = null;\r
167 me.setSchema(null);\r
168 me.callParent();\r
169 },\r
170\r
171 /**\r
172 * Adds an existing record instance to the session. The record\r
173 * may not belong to another session. The record cannot be a phantom record, instead\r
174 * use {@link #createRecord}.\r
175 * @param {Ext.data.Model} record The record to adopt.\r
176 */\r
177 adopt: function(record) {\r
178 var me = this,\r
179 associations = record.associations,\r
180 roleName;\r
181\r
182 //<debug>\r
183 me.checkModelType(record.self);\r
184 if (record.session && record.session !== me) {\r
185 Ext.raise('Record already belongs to an existing session');\r
186 }\r
187 //</debug>\r
188 if (record.session !== me) {\r
189 record.session = me;\r
190 me.add(record);\r
191 if (associations) {\r
192 for (roleName in associations) {\r
193 associations[roleName].adoptAssociated(record, me);\r
194 }\r
195 }\r
196 }\r
197 },\r
198\r
199 /**\r
200 * Marks the session as "clean" by calling {@link Ext.data.Model#commit} on each record\r
201 * that is known to the session.\r
202 *\r
203 * - Phantom records will no longer be phantom.\r
204 * - Modified records will no longer be dirty.\r
205 * - Dropped records will be erased.\r
206 *\r
207 * @since 5.1.0\r
208 */\r
209 commit: function() {\r
210 var data = this.data,\r
211 matrices = this.matrices,\r
212 entityName, entities, id, record;\r
213\r
214 for (entityName in data) {\r
215 entities = data[entityName];\r
216 for (id in entities) {\r
217 record = entities[id].record;\r
218 if (record) {\r
219 record.commit();\r
220 }\r
221 }\r
222 }\r
223\r
224 for (id in matrices) {\r
225 matrices[id].commit();\r
226 }\r
227 },\r
228\r
229 /**\r
230 * Creates a new record and tracks it in this session.\r
231 *\r
232 * @param {String/Ext.Class} type The `entityName` or the actual class of record to create.\r
233 * @param {Object} [data] The data for the record.\r
234 * @return {Ext.data.Model} The new record.\r
235 */\r
236 createRecord: function (type, data) {\r
237 //<debug>\r
238 this.checkModelType(type);\r
239 //</debug>\r
240 var Model = type.$isClass ? type : this.getSchema().getEntity(type),\r
241 parent = this.getParent(),\r
242 id;\r
243\r
244 // If we have no data, we're creating a phantom\r
245 if (data && parent) {\r
246 id = Model.getIdFromData(data);\r
247 if (parent.peekRecord(Model, id)) {\r
248 Ext.raise('A parent session already contains an entry for ' + Model.entityName + ': ' + id);\r
249 }\r
250 }\r
251 // By passing the session to the constructor, it will call session.add()\r
252 return new Model(data, this);\r
253 },\r
254\r
255 /**\r
256 * Returns an object describing all of the modified fields, created or dropped records\r
257 * and many-to-many association changes maintained by this session.\r
258 *\r
259 * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are no changes.\r
260 */\r
261 getChanges: function () {\r
262 var visitor = new Ext.data.session.ChangesVisitor(this);\r
263 this.visitData(visitor);\r
264 return visitor.result;\r
265 },\r
266\r
267 /**\r
268 * The same functionality as {@link #getChanges}, however we also take into account our\r
269 * parent session.\r
270 * \r
271 * @return {Object} An object in the CRUD format (see the intro docs). `null` if there are no changes.\r
272 *\r
273 * @protected\r
274 */\r
275 getChangesForParent: function() {\r
276 var visitor = new Ext.data.session.ChildChangesVisitor(this);\r
277 this.visitData(visitor);\r
278 return visitor.result;\r
279 },\r
280\r
281 /**\r
282 * Get a cached record from the session. If the record does not exist, it will\r
283 * be created. If the `autoLoad` parameter is not set to `false`, the record will\r
284 * be loaded via the {@link Ext.data.Model#proxy proxy} of the Model. \r
285 * \r
286 * If this session is configured with a `{@link #parent}` session, a *copy* of any existing record \r
287 * in the `parent` will be adopted into this session. If the `parent` does not contain the record,\r
288 * the record will be created and *not* inserted into the parent.\r
289 * \r
290 * See also {@link #peekRecord}.\r
291 *\r
292 * @param {String/Ext.Class/Ext.data.Model} type The `entityName` or the actual class of record to create.\r
293 * This may also be a record instance, where the type and id will be inferred from the record. If the record is\r
294 * not attached to a session, it will be adopted. If it exists in a parent, an appropriate copy will be made as\r
295 * described.\r
296 * @param {Object} id The id of the record.\r
297 * @param {Boolean/Object} [autoLoad=true] `false` to prevent the record from being loaded if\r
298 * it does not exist. If this parameter is an object, it will be passed to the {@link Ext.data.Model#load} call.\r
299 * @return {Ext.data.Model} The record.\r
300 */\r
301 getRecord: function(type, id, autoLoad) {\r
302 var me = this,\r
303 wasInstance = type.isModel,\r
304 record, Model, parent, parentRec;\r
305\r
306 if (wasInstance) {\r
307 wasInstance = type;\r
308 id = type.id;\r
309 type = type.self;\r
310 }\r
311 record = me.peekRecord(type, id);\r
312\r
313 if (!record) {\r
314 Model = type.$isClass ? type : me.getSchema().getEntity(type);\r
315 parent = me.getParent();\r
316 if (parent) {\r
317 parentRec = parent.peekRecord(Model, id);\r
318 }\r
319 if (parentRec) {\r
320 if (parentRec.isLoading()) {\r
321 // If the parent is loading, it's as though it doesn't have\r
322 // the record, so we can't copy it, but we don't want to\r
323 // adopt it either.\r
324 wasInstance = false;\r
325 } else {\r
326 record = parentRec.copy(undefined, me);\r
327 record.$source = parentRec;\r
328 }\r
329 }\r
330\r
331 if (!record) {\r
332 if (wasInstance) {\r
333 record = wasInstance;\r
334 me.adopt(record);\r
335 } else {\r
336 record = Model.createWithId(id, null, me);\r
337 if (autoLoad !== false) {\r
338 record.load(Ext.isObject(autoLoad) ? autoLoad : undefined);\r
339 }\r
340 }\r
341 }\r
342 }\r
343 return record;\r
344 },\r
345\r
346 /**\r
347 * Returns an `Ext.data.Batch` containing the `Ext.data.operation.Operation` instances\r
348 * that are needed to save all of the changes in this session. This sorting is based\r
349 * on operation type, associations and foreign keys. Generally speaking the operations\r
350 * in the batch can be committed to a server sequentially and the server will never be\r
351 * sent a request with an invalid (client-generated) id in a foreign key field.\r
352 *\r
353 * @param {Boolean} [sort=true] Pass `false` to disable the batch operation sort.\r
354 * @return {Ext.data.Batch}\r
355 */\r
356 getSaveBatch: function (sort) {\r
357 var visitor = new Ext.data.session.BatchVisitor();\r
358\r
359 this.visitData(visitor);\r
360\r
361 return visitor.getBatch(sort);\r
362 },\r
363\r
364 /**\r
365 * Triggered when an associated item from {@link #update} references a record\r
366 * that does not exist in the session.\r
367 * @param {Ext.Class} entityType The entity type.\r
368 * @param {Object} id The id of the model.\r
369 *\r
370 * @protected\r
371 * @template\r
372 */\r
373 onInvalidAssociationEntity: function(entityType, id) {\r
374 Ext.raise('Unable to read association entity: ' + this.getModelIdentifier(entityType, id));\r
375 },\r
376\r
377 /**\r
378 * Triggered when an drop block from {@link #update} tries to create a record\r
379 * that already exists.\r
380 * @param {Ext.Class} entityType The entity type.\r
381 * @param {Object} id The id of the model.\r
382 *\r
383 * @protected\r
384 * @template\r
385 */\r
386 onInvalidEntityCreate: function(entityType, id) {\r
387 Ext.raise('Cannot create, record already not exists: ' + this.getModelIdentifier(entityType, id));\r
388 },\r
389\r
390 /**\r
391 * Triggered when an drop block from {@link #update} references a record\r
392 * that does not exist in the session.\r
393 * @param {Ext.Class} entityType The entity type.\r
394 * @param {Object} id The id of the model.\r
395 *\r
396 * @protected\r
397 * @template\r
398 */\r
399 onInvalidEntityDrop: function(entityType, id) {\r
400 Ext.raise('Cannot drop, record does not exist: ' + this.getModelIdentifier(entityType, id));\r
401 },\r
402\r
403 /**\r
404 * Triggered when an drop block from {@link #update} tries to create a record\r
405 * that already exists.\r
406 * @param {Ext.Class} entityType The entity type.\r
407 * @param {Object} id The id of the model.\r
408 *\r
409 * @protected\r
410 * @template\r
411 */\r
412 onInvalidEntityRead: function(entityType, id) {\r
413 Ext.raise('Cannot read, record already not exists: ' + this.getModelIdentifier(entityType, id));\r
414 },\r
415\r
416 /**\r
417 * Triggered when an update block from {@link #update} references a record\r
418 * that does not exist in the session.\r
419 * @param {Ext.Class} entityType The entity type.\r
420 * @param {Object} id The id of the model.\r
421 * @param {Boolean} dropped `true` if the record was dropped.\r
422 *\r
423 * @protected\r
424 * @template\r
425 */\r
426 onInvalidEntityUpdate: function(entityType, id, dropped) {\r
427 if (dropped) {\r
428 Ext.raise('Cannot update, record dropped: ' + this.getModelIdentifier(entityType, id));\r
429 } else {\r
430 Ext.raise('Cannot update, record does not exist: ' + this.getModelIdentifier(entityType, id));\r
431 }\r
432 },\r
433\r
434 /**\r
435 * Gets an existing record from the session. The record will *not* be created if it does\r
436 * not exist.\r
437 *\r
438 * See also: {@link #getRecord}.\r
439 * \r
440 * @param {String/Ext.Class} type The `entityName` or the actual class of record to create.\r
441 * @param {Object} id The id of the record.\r
442 * @param {Boolean} [deep=false] `true` to consult \r
443 * @return {Ext.data.Model} The record, `null` if it does not exist.\r
444 */\r
445 peekRecord: function(type, id, deep) {\r
446 // Duplicate some of this logic from getEntry here to prevent the creation\r
447 // of entries when asking for the existence of records. We may not need them\r
448 //<debug>\r
449 this.checkModelType(type);\r
450 //</debug>\r
451 var entityType = type.$isClass ? type : this.getSchema().getEntity(type),\r
452 entityName = entityType.entityName,\r
453 entry = this.data[entityName],\r
454 ret, parent;\r
455\r
456 entry = entry && entry[id];\r
457 ret = entry && entry.record;\r
458\r
459 if (!ret && deep) {\r
460 parent = this.getParent();\r
461 ret = parent && parent.peekRecord(type, id, deep);\r
462 }\r
463 return ret || null;\r
464 },\r
465\r
466 /**\r
467 * Save any changes in this session to a {@link #parent} session.\r
468 */\r
469 save: function() {\r
470 //<debug>\r
471 if (!this.getParent()) {\r
472 Ext.raise('Cannot commit session, no parent exists');\r
473 }\r
474 //</debug>\r
475 var visitor = new Ext.data.session.ChildChangesVisitor(this);\r
476 this.visitData(visitor);\r
477 this.getParent().update(visitor.result);\r
478 },\r
479\r
480 /**\r
481 * Create a child session with this session as the {@link #parent}.\r
482 * @return {Ext.data.Session} The copied session.\r
483 */\r
484 spawn: function () {\r
485 return new this.self({\r
486 schema: this.getSchema(),\r
487 parent: this\r
488 });\r
489 },\r
490\r
491 /**\r
492 * Complete a bulk update for this session.\r
493 * @param {Object} data Data in the CRUD format (see the intro docs).\r
494 */\r
495 update: function(data) {\r
496 var me = this,\r
497 schema = me.getSchema(),\r
498 crudOperations = me.crudOperations,\r
499 len = crudOperations.length,\r
500 crudKeys = me.crudKeys,\r
501 entityName, entityType, entityInfo, i,\r
502 operation, item, associations, key, role, associationData;\r
503\r
504 // Force the schema to process any pending drops\r
505 me.getSchema().processKeyChecks(true);\r
506\r
507 // Do a first pass to setup all the entities first\r
508 for (entityName in data) {\r
509 entityType = schema.getEntity(entityName);\r
510 //<debug>\r
511 if (!entityType) {\r
512 Ext.raise('Invalid entity type: ' + entityName);\r
513 }\r
514 //</debug>\r
515 entityInfo = data[entityName];\r
516\r
517 for (i = 0; i < len; ++i) {\r
518 operation = crudOperations[i];\r
519 item = entityInfo[operation.type];\r
520 if (item) {\r
521 me[operation.entityMethod](entityType, item);\r
522 }\r
523 }\r
524 }\r
525\r
526 // A second pass to process associations once we have all the entities in place\r
527 for (entityName in data) {\r
528 entityType = schema.getEntity(entityName);\r
529 associations = entityType.associations;\r
530 entityInfo = data[entityName];\r
531\r
532 for (key in entityInfo) {\r
533 // Skip over CRUD, just looking for associations here\r
534 if (crudKeys[key]) {\r
535 continue;\r
536 }\r
537 role = associations[key];\r
538 //<debug>\r
539 if (!role) {\r
540 Ext.raise('Invalid association key for ' + entityName + ', "' + key + '"');\r
541 }\r
542 //</debug>\r
543 associationData = entityInfo[role.role];\r
544 role.processUpdate(me, associationData);\r
545 }\r
546 }\r
547 }, \r
548\r
549 //-------------------------------------------------------------------------\r
550 privates: {\r
551 /**\r
552 * Add a record instance to this session. Called by model.\r
553 * @param {Ext.data.Model} record The record.\r
554 * \r
555 * @private\r
556 */\r
557 add: function (record) {\r
558 var me = this,\r
559 id = record.id,\r
560 entry = me.getEntry(record.self, id),\r
561 associations, roleName;\r
562\r
563 //<debug>\r
564 if (entry.record) {\r
565 Ext.raise('Duplicate id ' + record.id + ' for ' + record.entityName);\r
566 }\r
567 //</debug>\r
568\r
569 entry.record = record;\r
570\r
571 me.registerReferences(record);\r
572 associations = record.associations;\r
573 for (roleName in associations) {\r
574 associations[roleName].checkMembership(me, record);\r
575 }\r
576 },\r
577\r
578 /**\r
579 * Template method, will be called by Model after a record is dropped.\r
580 * @param {Ext.data.Model} record The record.\r
581 *\r
582 * @private\r
583 */\r
584 afterErase: function(record) {\r
585 this.evict(record);\r
586 },\r
587\r
588 /**\r
589 * @private\r
590 */\r
591 applySchema: function (schema) {\r
592 return Ext.data.schema.Schema.get(schema);\r
593 },\r
594\r
595 //<debug>\r
596 /**\r
597 * Checks if the model type being referenced is valid for this session. That includes checking\r
598 * if the model name is correct & is one used in this {@link #schema} for this session. Will raise\r
599 * an exception if the model type is not correct.\r
600 * @param {String/Ext.Class} name The model name or model type.\r
601 *\r
602 * @private\r
603 */\r
604 checkModelType: function(name) {\r
605 if (name.$isClass) {\r
606 name = name.entityName;\r
607 }\r
608\r
609 if (!name) {\r
610 Ext.raise('Unable to use anonymous models in a Session');\r
611 } else if (!this.getSchema().getEntity(name)) {\r
612 Ext.raise('Unknown entity type ' + name);\r
613 }\r
614 },\r
615 //</debug>\r
616\r
617 /**\r
618 * Process a create block of entities from the {@link #update} method.\r
619 * @param {Ext.Class} entityType The entity type.\r
620 * @param {Object[]} items The data objects to create.\r
621 *\r
622 * @private\r
623 */\r
624 createEntities: function(entityType, items) {\r
625 var len = items.length,\r
626 i, data, rec, id;\r
627\r
628 for (i = 0; i < len; ++i) {\r
629 data = items[i];\r
630 id = entityType.getIdFromData(data);\r
631 rec = this.peekRecord(entityType, id);\r
632 if (!rec) {\r
633 rec = this.createRecord(entityType, data);\r
634 } else {\r
635 this.onInvalidEntityCreate(entityType, id);\r
636 }\r
637 // This record has been marked as being created, so we must\r
638 // be a phantom\r
639 rec.phantom = true;\r
640 }\r
641 },\r
642\r
643 /**\r
644 * Process a drop block for entities from the {@link #update} method.\r
645 * @param {Ext.Class} entityType The entity type.\r
646 * @param {Object[]} ids The identifiers of the items to drop.\r
647 *\r
648 * @private\r
649 */\r
650 dropEntities: function(entityType, ids) {\r
651 var len = ids.length,\r
652 i, rec, id, extractId;\r
653\r
654 if (len) {\r
655 // Handle writeAllFields here, we may not have an array of raw ids\r
656 extractId = Ext.isObject(ids[0]);\r
657 }\r
658\r
659 for (i = 0; i < len; ++i) {\r
660 id = ids[i];\r
661 if (extractId) {\r
662 id = entityType.getIdFromData(id);\r
663 }\r
664 rec = this.peekRecord(entityType, id);\r
665 if (rec) {\r
666 rec.drop();\r
667 } else {\r
668 this.onInvalidEntityDrop(entityType, id);\r
669 }\r
670 }\r
671 },\r
672\r
673 /**\r
674 * Remove a record and any references from the session.\r
675 * @param {Ext.data.Model} record The record\r
676 *\r
677 * @private\r
678 */\r
679 evict: function(record) {\r
680 var entityName = record.entityName,\r
681 entities = this.data[entityName],\r
682 id = record.id,\r
683 entry;\r
684\r
685 if (entities) {\r
686 delete entities[id];\r
687 }\r
688 },\r
689\r
690 /**\r
691 * Transforms a list of ids into a list of records for a particular type.\r
692 * @param {Ext.Class} entityType The entity type.\r
693 * @param {Object[]} ids The ids to transform.\r
694 * @return {Ext.data.Model[]} The models corresponding to the ids.\r
695 */\r
696 getEntityList: function(entityType, ids) {\r
697 var len = ids.length,\r
698 i, id, rec, invalid;\r
699\r
700 for (i = 0; i < len; ++i) {\r
701 id = ids[i];\r
702 rec = this.peekRecord(entityType, id);\r
703 if (rec) {\r
704 ids[i] = rec;\r
705 } else {\r
706 invalid = true;\r
707 ids[i] = null;\r
708 this.onInvalidAssociationEntity(entityType, id);\r
709 }\r
710 }\r
711 if (invalid) {\r
712 ids = Ext.Array.clean(ids);\r
713 }\r
714 return ids;\r
715 },\r
716\r
717 /**\r
718 * Return an entry for the data property for a particular type/id.\r
719 * @param {String/Ext.Class} type The entity name or model type.\r
720 * @param {Object} id The id of the record\r
721 * @return {Object} The data entry.\r
722 *\r
723 * @private\r
724 */\r
725 getEntry: function(type, id) {\r
726 if (type.isModel) {\r
727 id = type.getId(); \r
728 type = type.self;\r
729 }\r
730\r
731 var entityType = type.$isClass ? type : this.getSchema().getEntity(type),\r
732 entityName = entityType.entityName,\r
733 data = this.data,\r
734 entry;\r
735\r
736 entry = data[entityName] || (data[entityName] = {});\r
737 entry = entry[id] || (entry[id] = {});\r
738\r
739 return entry;\r
740 },\r
741\r
742 getRefs: function(record, role, includeParent) {\r
743 var entry = this.getEntry(record),\r
744 refs = entry && entry.refs && entry.refs[role.role],\r
745 parent = includeParent && this.getParent(),\r
746 parentRefs, id, rec;\r
747\r
748 if (parent) {\r
749 parentRefs = parent.getRefs(record, role);\r
750 if (parentRefs) {\r
751 for (id in parentRefs) {\r
752 rec = parentRefs[id];\r
753 if ((!refs || !refs[id])) {\r
754 // We don't know about this record but the parent does. We need to\r
755 // pull it down so it may be edited as part of the collection\r
756 this.getRecord(rec.self, rec.id);\r
757 }\r
758 }\r
759 // Recalculate our refs after we pull down all the required records\r
760 refs = entry && entry.refs && entry.refs[role.role];\r
761 }\r
762 }\r
763\r
764 return refs || null;\r
765 },\r
766\r
767 getIdentifier: function (entityType) {\r
768 var parent = this.getParent(),\r
769 cache, identifier, key, ret;\r
770\r
771 if (parent) {\r
772 ret = parent.getIdentifier(entityType);\r
773 } else {\r
774 cache = this.identifierCache;\r
775 identifier = entityType.identifier;\r
776 key = identifier.id || entityType.entityName;\r
777 ret = cache[key];\r
778\r
779 if (!ret) {\r
780 if (identifier.clone) {\r
781 ret = identifier.clone({\r
782 cache: cache\r
783 });\r
784 } else {\r
785 ret = identifier;\r
786 }\r
787\r
788 cache[key] = ret;\r
789 }\r
790 }\r
791\r
792 return ret;\r
793 },\r
794\r
795 getMatrix: function (matrix, preventCreate) {\r
796 var name = matrix.isManyToMany ? matrix.name : matrix,\r
797 matrices = this.matrices,\r
798 ret;\r
799\r
800 ret = matrices[name];\r
801 if (!ret && !preventCreate) {\r
802 ret = matrices[name] = new Ext.data.matrix.Matrix(this, matrix);\r
803 }\r
804 return ret || null;\r
805 },\r
806\r
807 getMatrixSlice: function (role, id) {\r
808 var matrix = this.getMatrix(role.association),\r
809 side = matrix[role.side];\r
810\r
811 return side.get(id);\r
812 },\r
813\r
814 /**\r
815 * Gets a user friendly identifier for a Model.\r
816 * @param {Ext.Class} entityType The entity type.\r
817 * @param {Object} id The id of the entity.\r
818 * @return {String} The identifier.\r
819 */\r
820 getModelIdentifier: function(entityType, id) {\r
821 return id + '@' + entityType.entityName;\r
822 },\r
823\r
824 onIdChanged: function (record, oldId, newId) {\r
825 var me = this,\r
826 matrices = me.matrices,\r
827 entityName = record.entityName,\r
828 id = record.id,\r
829 bucket = me.data[entityName],\r
830 entry = bucket[oldId],\r
831 associations = record.associations,\r
832 refs = entry.refs,\r
833 setNoRefs = me._setNoRefs,\r
834 association, fieldName, matrix, refId, role, roleName, roleRefs, key;\r
835\r
836 //<debug>\r
837 if (bucket[newId]) {\r
838 Ext.raise('Cannot change ' + entityName + ' id from ' + oldId +\r
839 ' to ' + newId + ' id already exists');\r
840 }\r
841 //</debug>\r
842\r
843 delete bucket[oldId];\r
844 bucket[newId] = entry;\r
845\r
846 for (key in matrices) {\r
847 matrices[key].updateId(record, oldId, newId);\r
848 }\r
849\r
850 if (refs) {\r
851 for (roleName in refs) {\r
852 roleRefs = refs[roleName];\r
853 role = associations[roleName];\r
854 association = role.association;\r
855\r
856 if (!association.isManyToMany) {\r
857 fieldName = association.field.name;\r
858\r
859 for (refId in roleRefs) {\r
860 roleRefs[refId].set(fieldName, id, setNoRefs);\r
861 }\r
862 }\r
863 }\r
864 }\r
865\r
866 me.registerReferences(record, oldId);\r
867 },\r
868\r
869 processManyBlock: function(entityType, role, items, processor) {\r
870 var me = this,\r
871 id, record, records, store;\r
872\r
873 if (items) {\r
874 for (id in items) {\r
875 record = me.peekRecord(entityType, id);\r
876 if (record) {\r
877 records = me.getEntityList(role.cls, items[id]);\r
878 store = role.getAssociatedItem(record);\r
879 me[processor](role, store, record, records);\r
880 } else {\r
881 me.onInvalidAssociationEntity(entityType, id);\r
882 }\r
883 }\r
884 }\r
885 },\r
886\r
887 processManyCreate: function(role, store, record, records) {\r
888 if (store) {\r
889 // Will handle any duplicates\r
890 store.add(records);\r
891 } else {\r
892 record[role.getterName](null, null, records);\r
893 }\r
894 \r
895 },\r
896\r
897 processManyDrop: function(role, store, record, records) {\r
898 if (store) {\r
899 store.remove(records);\r
900 }\r
901 },\r
902\r
903 processManyRead: function(role, store, record, records) {\r
904 if (store) {\r
905 store.setRecords(records);\r
906 } else {\r
907 // We don't have a store. Create it and add the records.\r
908 record[role.getterName](null, null, records);\r
909 }\r
910 },\r
911\r
912 /**\r
913 * Process a read block of entities from the {@link #update} method.\r
914 * @param {Ext.Class} entityType The entity type.\r
915 * @param {Object[]} items The data objects to read.\r
916 *\r
917 * @private\r
918 */\r
919 readEntities: function(entityType, items) {\r
920 var len = items.length,\r
921 i, data, rec, id;\r
922\r
923 for (i = 0; i < len; ++i) {\r
924 data = items[i];\r
925 id = entityType.getIdFromData(data);\r
926 rec = this.peekRecord(entityType, id);\r
927 if (!rec) {\r
928 rec = this.createRecord(entityType, data);\r
929 } else {\r
930 this.onInvalidEntityRead(entityType, id);\r
931 }\r
932 // We've been read from a "server", so we aren't a phantom,\r
933 // regardless of whether or not we have an id\r
934 rec.phantom = false;\r
935 }\r
936 },\r
937\r
938 recordCreator: function (data, Model) {\r
939 var me = this,\r
940 id = Model.getIdFromData(data),\r
941 record = me.peekRecord(Model, id, true);\r
942\r
943 // It doesn't exist anywhere, create it\r
944 if (!record) {\r
945 // We may have a stub that is loading the record (in fact this may be the\r
946 // call coming from that Reader), but the resolution is simple. By creating\r
947 // the record it is registered in the data[entityName][id] entry anyway\r
948 // and the stub will deal with it onLoad.\r
949 record = new Model(data, me);\r
950 } else {\r
951 //TODO no easy answer here... we are trying to create a record and have\r
952 //TODO some (potentially new) data. We probably should check for mid-air\r
953 //TODO collisions using versionProperty but for now we just ignore the\r
954 //TODO new data in favor of our potentially edited data.\r
955 \r
956 // Peek checks if it exists at any level, by getting it we ensure that the record is copied down\r
957 record = me.getRecord(Model, id);\r
958 }\r
959\r
960 return record;\r
961 },\r
962\r
963 registerReferences: function (record, oldId) {\r
964 var entityName = record.entityName,\r
965 id = record.id,\r
966 recordData = record.data,\r
967 remove = oldId || oldId === 0,\r
968 entry, i, fk, len, reference, references, refs, roleName;\r
969\r
970 // Register this records references to other records\r
971 len = (references = record.references).length;\r
972\r
973 for (i = 0; i < len; ++i) {\r
974 reference = references[i]; // e.g., an orderId field\r
975 fk = recordData[reference.name]; // the orderId\r
976\r
977 if (fk || fk === 0) {\r
978 reference = reference.reference; // the "order" association role\r
979 entityName = reference.type;\r
980 roleName = reference.inverse.role;\r
981\r
982 // Track down the entry for the associated record\r
983 entry = this.getEntry(reference.cls, fk);\r
984 refs = entry.refs || (entry.refs = {});\r
985 refs = refs[roleName] || (refs[roleName] = {});\r
986\r
987 refs[id] = record;\r
988 if (remove) {\r
989 delete refs[oldId];\r
990 }\r
991 }\r
992 }\r
993 },\r
994\r
995 /**\r
996 * Process an update block for entities from the {@link #update} method.\r
997 * @param {Ext.Class} entityType The entity type.\r
998 * @param {Object[]} items The data objects to update.\r
999 *\r
1000 * @private\r
1001 */\r
1002 updateEntities: function(entityType, items) {\r
1003 var len = items.length,\r
1004 i, data, rec, id, modified;\r
1005\r
1006 // Repeating some code here, but we want to optimize this for speed\r
1007 if (Ext.isArray(items)) {\r
1008 for (i = 0; i < len; ++i) {\r
1009 data = items[i];\r
1010 id = entityType.getIdFromData(data);\r
1011 rec = this.peekRecord(entityType, id);\r
1012 if (rec) {\r
1013 rec.set(data);\r
1014 } else {\r
1015 this.onInvalidEntityUpdate(entityType, id);\r
1016 }\r
1017 }\r
1018 } else {\r
1019 for (id in items) {\r
1020 data = items[id];\r
1021 rec = this.peekRecord(entityType, id);\r
1022 if (rec && !rec.dropped) {\r
1023 modified = rec.set(data);\r
1024 } else {\r
1025 this.onInvalidEntityUpdate(entityType, id, !!rec);\r
1026 }\r
1027 }\r
1028 }\r
1029 },\r
1030\r
1031 updateReference: function (record, field, newValue, oldValue) {\r
1032 var reference = field.reference,\r
1033 entityName = reference.type,\r
1034 roleName = reference.inverse.role,\r
1035 id = record.id,\r
1036 entry, refs;\r
1037\r
1038 if (oldValue || oldValue === 0) {\r
1039 // We must be already in this entry.refs collection\r
1040 refs = this.getEntry(entityName, oldValue).refs[roleName];\r
1041 delete refs[id];\r
1042 }\r
1043\r
1044 if (newValue || newValue === 0) {\r
1045 entry = this.getEntry(entityName, newValue);\r
1046 refs = entry.refs || (entry.refs = {});\r
1047 refs = refs[roleName] || (refs[roleName] = {});\r
1048 refs[id] = record;\r
1049 }\r
1050 },\r
1051\r
1052 /**\r
1053 * Walks the internal data tracked by this session and calls methods on the provided\r
1054 * `visitor` object. The visitor can then accumulate whatever data it finds important.\r
1055 * The visitor object can provide a number of methods, but all are optional.\r
1056 *\r
1057 * This method does not enumerate associations since these can be traversed given the\r
1058 * records that are enumerated. For many-to-many associations, however, this method\r
1059 * does enumerate the changes because these changes are not "owned" by either side of\r
1060 * such associations.\r
1061 *\r
1062 * @param {Object} visitor\r
1063 * @param {Function} [visitor.onCleanRecord] This method is called to describe a record\r
1064 * that is known but unchanged.\r
1065 * @param {Ext.data.Model} visitor.onCleanRecord.record The unmodified record.\r
1066 * @param {Function} [visitor.onDirtyRecord] This method is called to describe a record\r
1067 * that has either been created, dropped or modified.\r
1068 * @param {Ext.data.Model} visitor.onDirtyRecord.record The modified record.\r
1069 * @param {Function} [visitor.onMatrixChange] This method is called to describe a\r
1070 * change in a many-to-many association (a "matrix").\r
1071 * @param {Ext.data.schema.Association} visitor.onMatrixChange.association The object\r
1072 * describing the many-to-many ("matrix") association.\r
1073 * @param {Mixed} visitor.onMatrixChange.leftId The `idProperty` of the record on the\r
1074 * "left" of the association.\r
1075 * @param {Mixed} visitor.onMatrixChange.rightId The `idProperty` of the record on the\r
1076 * "right" of the association.\r
1077 * @param {Number} visitor.onMatrixChange.state A negative number if the two records\r
1078 * are being disassociated or a positive number if they are being associated. For\r
1079 * example, when adding User 10 to Group 20, this would be 1. When removing the User\r
1080 * this argument would be -1.\r
1081 * @return {Object} The visitor instance\r
1082 */\r
1083 visitData: function (visitor) {\r
1084 var me = this,\r
1085 data = me.data,\r
1086 matrices = me.matrices,\r
1087 all, assoc, id, id2, matrix, members, name, record, slice, slices, state;\r
1088\r
1089 // Force the schema to process any pending drops\r
1090 me.getSchema().processKeyChecks(true);\r
1091\r
1092 for (name in data) {\r
1093 all = data[name]; // all entities of type "name"\r
1094\r
1095 for (id in all) {\r
1096 record = all[id].record;\r
1097\r
1098 if (record) {\r
1099 if (record.phantom || record.dirty || record.dropped) {\r
1100 if (visitor.onDirtyRecord) {\r
1101 visitor.onDirtyRecord(record);\r
1102 }\r
1103 } else if (visitor.onCleanRecord) {\r
1104 visitor.onCleanRecord(record);\r
1105 }\r
1106 }\r
1107 }\r
1108 }\r
1109\r
1110 if (visitor.onMatrixChange) {\r
1111 for (name in matrices) {\r
1112 matrix = matrices[name].left; // e.g., UserGroups.left (Users)\r
1113 slices = matrix.slices;\r
1114 assoc = matrix.role.association;\r
1115\r
1116 for (id in slices) {\r
1117 slice = slices[id];\r
1118 members = slice.members;\r
1119\r
1120 for (id2 in members) {\r
1121 state = (record = members[id2])[2];\r
1122\r
1123 if (state) {\r
1124 visitor.onMatrixChange(assoc, record[0], record[1], state);\r
1125 }\r
1126 }\r
1127 }\r
1128 }\r
1129 }\r
1130\r
1131 return visitor;\r
1132 },\r
1133\r
1134 //---------------------------------------------------------------------\r
1135 // Record callbacks called because we are the "session" for the record.\r
1136\r
1137 _setNoRefs: {\r
1138 refs: false\r
1139 }\r
1140 }\r
1141});\r