summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/models/model/model.js
blob: 3696332ae85fa4e51116fedc687cf45676af2d24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
odoo.define('mail/static/src/models/Model', function (require) {
'use strict';

const { registerNewModel } = require('mail/static/src/model/model_core.js');
const { RecordDeletedError } = require('mail/static/src/model/model_errors.js');

/**
 * This function generates a class that represent a model. Instances of such
 * model (or inherited models) represent logical objects used in whole
 * application. They could represent server record (e.g. Thread, Message) or
 * UI elements (e.g. MessagingMenu, ChatWindow). These instances are called
 * "records", while the classes are called "models".
 */
function factory() {

    class Model {

        /**
         * @param {Object} [param0={}]
         * @param {boolean} [param0.valid=false] if set, this constructor is
         *   called by static method `create()`. This should always be the case.
         * @throws {Error} in case constructor is called in an invalid way, i.e.
         *   by instantiating the record manually with `new` instead of from
         *   static method `create()`.
         */
        constructor({ valid = false } = {}) {
            if (!valid) {
                throw new Error("Record must always be instantiated from static method 'create()'");
            }
        }

        /**
         * This function is called during the create cycle, when the record has
         * already been created, but its values have not yet been assigned.
         *
         * It is usually preferable to override @see `_created`.
         *
         * The main use case is to prepare the record for the assignation of its
         * values, for example if a computed field relies on the record to have
         * some purely technical property correctly set.
         *
         * @abstract
         * @private
         */
        _willCreate() {}

        /**
         * This function is called after the record has been created, more
         * precisely at the end of the update cycle (which means all implicit
         * changes such as computes have been applied too).
         *
         * The main use case is to register listeners on the record.
         *
         * @abstract
         * @private
         */
        _created() {}

        /**
         * This function is called when the record is about to be deleted. The
         * record still has all of its fields values accessible, but for all
         * intents and purposes the record should already be considered
         * deleted, which means update shouldn't be called inside this method.
         *
         * The main use case is to unregister listeners on the record.
         *
         * @abstract
         * @private
         */
        _willDelete() {}

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        /**
         * Returns all records of this model that match provided criteria.
         *
         * @static
         * @param {function} [filterFunc]
         * @returns {mail.model[]}
         */
        static all(filterFunc) {
            return this.env.modelManager.all(this, filterFunc);
        }

        /**
         * This method is used to create new records of this model
         * with provided data. This is the only way to create them:
         * instantiation must never been done with keyword `new` outside of this
         * function, otherwise the record will not be registered.
         *
         * @static
         * @param {Object|Object[]} [data] data object with initial data, including relations.
         *  If data is an iterable, multiple records will be created.
         * @returns {mail.model|mail.model[]} newly created record(s)
         */
        static create(data) {
            return this.env.modelManager.create(this, data);
        }

        /**
         * Get the record that has provided criteria, if it exists.
         *
         * @static
         * @param {function} findFunc
         * @returns {mail.model|undefined}
         */
        static find(findFunc) {
            return this.env.modelManager.find(this, findFunc);
        }

        /**
         * Gets the unique record that matches the given identifying data, if it
         * exists.
         * @see `_createRecordLocalId` for criteria of identification.
         *
         * @static
         * @param {Object} data
         * @returns {mail.model|undefined}
         */
        static findFromIdentifyingData(data) {
            return this.env.modelManager.findFromIdentifyingData(this, data);
        }

        /**
         * This method returns the record of this model that matches provided
         * local id. Useful to convert a local id to a record. Note that even
         * if there's a record in the system having provided local id, if the
         * resulting record is not an instance of this model, this getter
         * assumes the record does not exist.
         *
         * @static
         * @param {string} localId
         * @param {Object} param1
         * @param {boolean} [param1.isCheckingInheritance]
         * @returns {mail.model|undefined}
         */
        static get(localId, { isCheckingInheritance } = {}) {
            return this.env.modelManager.get(this, localId, { isCheckingInheritance });
        }

        /**
         * This method creates a record or updates one, depending
         * on provided data.
         *
         * @static
         * @param {Object|Object[]} data
         *  If data is an iterable, multiple records will be created/updated.
         * @returns {mail.model|mail.model[]} created or updated record(s).
         */
        static insert(data) {
            return this.env.modelManager.insert(this, data);
        }

        /**
         * Perform an async function and wait until it is done. If the record
         * is deleted, it raises a RecordDeletedError.
         *
         * @param {function} func an async function
         * @throws {RecordDeletedError} in case the current record is not alive
         *   at the end of async function call, whether it's resolved or
         *   rejected.
         * @throws {any} forwards any error in case the current record is still
         *   alive at the end of rejected async function call.
         * @returns {any} result of resolved async function.
         */
        async async(func) {
            return new Promise((resolve, reject) => {
                Promise.resolve(func()).then(result => {
                    if (this.exists()) {
                        resolve(result);
                    } else {
                        reject(new RecordDeletedError(this.localId));
                    }
                }).catch(error => {
                    if (this.exists()) {
                        reject(error);
                    } else {
                        reject(new RecordDeletedError(this.localId));
                    }
                });
            });
        }

        /**
         * This method deletes this record.
         */
        delete() {
            this.env.modelManager.delete(this);
        }

        /**
         * Returns whether the current record exists.
         *
         * @returns {boolean}
         */
        exists() {
            return this.env.modelManager.exists(this.constructor, this);
        }

        /**
         * Update this record with provided data.
         *
         * @param {Object} [data={}]
         */
        update(data = {}) {
            this.env.modelManager.update(this, data);
        }

        //----------------------------------------------------------------------
        // Private
        //----------------------------------------------------------------------

        /**
         * This method generates a local id for this record that is
         * being created at the moment.
         *
         * This function helps customizing the local id to ease mapping a local
         * id to its record for the developer that reads the local id. For
         * instance, the local id of a thread cache could combine the thread
         * and stringified domain in its local id, which is much easier to
         * track relations and records in the system instead of arbitrary
         * number to differenciate them.
         *
         * @static
         * @private
         * @param {Object} data
         * @returns {string}
         */
        static _createRecordLocalId(data) {
            return _.uniqueId(`${this.modelName}_`);
        }

        /**
         * This function is called when this record has been explicitly updated
         * with `.update()` or static method `.create()`, at the end of an
         * record update cycle. This is a backward-compatible behaviour that
         * is deprecated: you should use computed fields instead.
         *
         * @deprecated
         * @abstract
         * @private
         * @param {Object} previous contains data that have been stored by
         *   `_updateBefore()`. Useful to make extra update decisions based on
         *   previous data.
         */
        _updateAfter(previous) {}

        /**
         * This function is called just at the beginning of an explicit update
         * on this function, with `.update()` or static method `.create()`. This
         * is useful to remember previous values of fields in `_updateAfter`.
         * This is a backward-compatible behaviour that is deprecated: you
         * should use computed fields instead.
         *
         * @deprecated
         * @abstract
         * @private
         * @param {Object} data
         * @returns {Object}
         */
        _updateBefore() {
            return {};
        }

    }

    /**
     * Models should define fields in static prop or getter `fields`.
     * It contains an object with name of field as key and value are objects
     * that define the field. There are some helpers to ease the making of these
     * objects, @see `mail/static/src/model/model_field.js`
     *
     * Note: fields of super-class are automatically inherited, therefore a
     * sub-class should (re-)define fields without copying ancestors' fields.
     */
    Model.fields = {};

    /**
     * Name of the model. Important to refer to appropriate model class
     * like in relational fields. Name of model classes must be unique.
     */
    Model.modelName = 'mail.model';

    return Model;
}

registerNewModel('mail.model', factory);

});