@pbvision/firestore-orm 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +204 -0
- package/README.md +860 -0
- package/environment.js +2 -0
- package/package.json +81 -0
- package/requirements.txt +1 -0
- package/scripts/ci/lint-format-js.sh +13 -0
- package/scripts/ci/markdown-link.sh +8 -0
- package/scripts/start-local-db.sh +2 -0
- package/src/async-emitter.js +50 -0
- package/src/context.js +677 -0
- package/src/data.js +24 -0
- package/src/db-error.js +12 -0
- package/src/default-db.js +21 -0
- package/src/errors.js +149 -0
- package/src/fields.js +526 -0
- package/src/firestore.js +75 -0
- package/src/key.js +66 -0
- package/src/models.js +542 -0
- package/src/utils.js +75 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Convenient helper to setup our db connection using environment variables.
|
|
2
|
+
// The constructed db instance will be cached by NodeJS.
|
|
3
|
+
const Firestore = require('@google-cloud/firestore')
|
|
4
|
+
|
|
5
|
+
const setup = require('./firestore')
|
|
6
|
+
|
|
7
|
+
// make Firestore look like the Transaction object (it already has getAll but
|
|
8
|
+
// does not have these others)
|
|
9
|
+
Firestore.prototype.get = async (docRef) => docRef.get()
|
|
10
|
+
Firestore.prototype.delete = async (docRef) => docRef.delete()
|
|
11
|
+
Firestore.prototype.create = async (docRef, data) => docRef.create(data)
|
|
12
|
+
Firestore.prototype.set = async (docRef, data, options) => docRef.set(data, options)
|
|
13
|
+
Firestore.prototype.update = async (docRef, data) => docRef.update(data)
|
|
14
|
+
|
|
15
|
+
// TODO: get "host" field based on environment (e.g., dev or prod)
|
|
16
|
+
// automatically uses the emulator when FIRESTORE_EMULATOR_HOST is set
|
|
17
|
+
const firestoreClient = new Firestore({
|
|
18
|
+
ignoreUndefinedProperties: true
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
module.exports = setup(firestoreClient)
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @namespace Errors
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when supplied option is invalid.
|
|
7
|
+
*
|
|
8
|
+
* @access public
|
|
9
|
+
* @memberof Errors
|
|
10
|
+
*/
|
|
11
|
+
class InvalidOptionsError extends Error {
|
|
12
|
+
constructor (option, expectation) {
|
|
13
|
+
super(`Invalid option value for ${option}. ${expectation}.`)
|
|
14
|
+
this.name = this.constructor.name
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when some parameter is invalid.
|
|
20
|
+
*
|
|
21
|
+
* @access public
|
|
22
|
+
* @memberof Errors
|
|
23
|
+
*/
|
|
24
|
+
class InvalidParameterError extends Error {
|
|
25
|
+
constructor (param, expectation) {
|
|
26
|
+
super(`Invalid parameter ${param}. ${expectation}.`)
|
|
27
|
+
this.name = this.constructor.name
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when the library detects a field to be in an invalid state.
|
|
33
|
+
*
|
|
34
|
+
* @access public
|
|
35
|
+
* @memberof Errors
|
|
36
|
+
*/
|
|
37
|
+
class InvalidFieldError extends Error {
|
|
38
|
+
constructor (field, reason) {
|
|
39
|
+
super(`${field} ${reason}`)
|
|
40
|
+
this.name = this.constructor.name
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Thrown when a transaction fails.
|
|
46
|
+
* Original exception is attached to property `original`
|
|
47
|
+
* Original stack is appended to current stack.
|
|
48
|
+
*
|
|
49
|
+
* @arg {string} msg the error message
|
|
50
|
+
* @arg {Error} [originalException] the original error which led to this
|
|
51
|
+
* @access public
|
|
52
|
+
* @memberof Errors
|
|
53
|
+
*/
|
|
54
|
+
class TransactionFailedError extends Error {
|
|
55
|
+
constructor (msg, originalException) {
|
|
56
|
+
super(msg)
|
|
57
|
+
this.name = this.constructor.name
|
|
58
|
+
this.original = originalException
|
|
59
|
+
if (originalException instanceof Error) {
|
|
60
|
+
this.stack += '\n' + originalException.stack
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class TransactionLockTimeoutError extends TransactionFailedError {
|
|
66
|
+
constructor (reason, original) {
|
|
67
|
+
super(reason, original)
|
|
68
|
+
this.name = this.constructor.name
|
|
69
|
+
this.retryable = true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Thrown when there's some error with a particular model.
|
|
75
|
+
* @memberof Errors
|
|
76
|
+
*/
|
|
77
|
+
class GenericModelError extends Error {
|
|
78
|
+
constructor (msg, collection, _id) {
|
|
79
|
+
super(`${msg}: ${collection} _id=${_id}`)
|
|
80
|
+
this.name = this.constructor.name
|
|
81
|
+
this.retryable = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when a model is to be created, but DB already has an doc with the
|
|
87
|
+
* same key.
|
|
88
|
+
* @memberof Errors
|
|
89
|
+
*/
|
|
90
|
+
class ModelAlreadyExistsError extends GenericModelError {
|
|
91
|
+
constructor (collection, _id) {
|
|
92
|
+
super('Tried to recreate an existing model', collection, _id)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Thrown when model is tracked more than once inside a transaction.
|
|
98
|
+
*/
|
|
99
|
+
class ModelTrackedTwiceError extends GenericModelError {
|
|
100
|
+
constructor (key, trackedModel) {
|
|
101
|
+
const msg = 'Model tracked twice'
|
|
102
|
+
super(msg, key.Cls.collectionName, key.encodedKey)
|
|
103
|
+
this.newKey = key
|
|
104
|
+
this.trackedModel = trackedModel
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Thrown when a model is being deleted more than once.
|
|
110
|
+
* @memberof Errors
|
|
111
|
+
*/
|
|
112
|
+
class DeletedTwiceError extends GenericModelError {
|
|
113
|
+
constructor (collectionName, _id) {
|
|
114
|
+
super('Tried to delete model twice in the same transaction',
|
|
115
|
+
collectionName, _id)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Thrown when a tx tries to write when it was marked read-only.
|
|
121
|
+
* @memberof Errors
|
|
122
|
+
*/
|
|
123
|
+
class WriteAttemptedInReadOnlyTxError extends Error {
|
|
124
|
+
constructor (dataToWrite) {
|
|
125
|
+
super(`Tried to write model in a read-only transaction with the following
|
|
126
|
+
changes ${JSON.stringify(dataToWrite)}`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class NotImplementedError extends Error {
|
|
131
|
+
constructor (reason) {
|
|
132
|
+
super(reason)
|
|
133
|
+
this.name = this.constructor.name
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
DeletedTwiceError,
|
|
139
|
+
GenericModelError,
|
|
140
|
+
InvalidFieldError,
|
|
141
|
+
InvalidOptionsError,
|
|
142
|
+
InvalidParameterError,
|
|
143
|
+
ModelAlreadyExistsError,
|
|
144
|
+
ModelTrackedTwiceError,
|
|
145
|
+
NotImplementedError,
|
|
146
|
+
TransactionFailedError,
|
|
147
|
+
TransactionLockTimeoutError,
|
|
148
|
+
WriteAttemptedInReadOnlyTxError
|
|
149
|
+
}
|
package/src/fields.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
const assert = require('assert')
|
|
2
|
+
|
|
3
|
+
const { FieldValue } = require('@google-cloud/firestore')
|
|
4
|
+
const deepeq = require('fast-deep-equal')
|
|
5
|
+
const deepcopy = require('rfdc')()
|
|
6
|
+
|
|
7
|
+
const { InvalidFieldError, InvalidOptionsError, NotImplementedError } = require('./errors')
|
|
8
|
+
const { validateValue } = require('./utils')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Abstract class representing a field / property of a Model.
|
|
12
|
+
*
|
|
13
|
+
* @private
|
|
14
|
+
* @memberof Internal
|
|
15
|
+
*/
|
|
16
|
+
class __FieldInterface {
|
|
17
|
+
constructor () {
|
|
18
|
+
if (this.constructor === __FieldInterface) {
|
|
19
|
+
throw new Error('Can not instantiate abstract class')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get mutated () {
|
|
24
|
+
throw new NotImplementedError()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get __mayHaveMutated () {
|
|
28
|
+
throw new NotImplementedError()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get accessed () {
|
|
32
|
+
throw new NotImplementedError()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get () {
|
|
36
|
+
throw new NotImplementedError()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set (val) {
|
|
40
|
+
throw new NotImplementedError()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
__valueForFirestoreWrite () {
|
|
44
|
+
throw new NotImplementedError()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate () {
|
|
48
|
+
throw new NotImplementedError()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
hasChangesToCommit (expectWrites = true) {
|
|
52
|
+
throw new NotImplementedError()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @namespace Fields
|
|
58
|
+
* @memberof Internal
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @namespace Internal
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Base class representing a field / property of a Model.
|
|
69
|
+
*
|
|
70
|
+
* @private
|
|
71
|
+
* @memberof Internal
|
|
72
|
+
*/
|
|
73
|
+
class __BaseField extends __FieldInterface {
|
|
74
|
+
/*
|
|
75
|
+
* Indicates if any changes need to be committed in the field.
|
|
76
|
+
* @param {Boolean} expectWrites whether the model will be updated,
|
|
77
|
+
* default is true.
|
|
78
|
+
* @type {Boolean}
|
|
79
|
+
*/
|
|
80
|
+
hasChangesToCommit (expectWrites = true) {
|
|
81
|
+
// If a field is changed due to being initialized with the default
|
|
82
|
+
// value within a read-only transaction, the change shouldn't be committed.
|
|
83
|
+
// Otherwise, an error will occur when attempting to update in a
|
|
84
|
+
// read-only transaction.
|
|
85
|
+
if (this.__isForUpdate) {
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
const initDefault = !expectWrites && this.__useDefault && !this.__written
|
|
89
|
+
return this.mutated && !initDefault
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Internal object representing a field / property of a Model.
|
|
95
|
+
*
|
|
96
|
+
* @private
|
|
97
|
+
* @memberof Internal
|
|
98
|
+
*/
|
|
99
|
+
class __Field extends __BaseField {
|
|
100
|
+
static __validateFieldOptions (modelName, isKey, fieldName, schema) {
|
|
101
|
+
if (fieldName.startsWith('_')) {
|
|
102
|
+
throw new InvalidFieldError(
|
|
103
|
+
fieldName, 'property names may not start with "_"')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isKey === undefined) {
|
|
107
|
+
isKey = false
|
|
108
|
+
}
|
|
109
|
+
assert([true, false].includes(isKey), 'isKey must be true or false')
|
|
110
|
+
assert(schema.isSchema, 'must be Schema object')
|
|
111
|
+
|
|
112
|
+
const compiledSchema = schema.getValidatorAndJSONSchema(
|
|
113
|
+
`${modelName}.${fieldName}`)
|
|
114
|
+
const jsonSchema = compiledSchema.jsonSchema
|
|
115
|
+
const options = {
|
|
116
|
+
isKey,
|
|
117
|
+
schema: jsonSchema,
|
|
118
|
+
optional: schema.required === false,
|
|
119
|
+
immutable: isKey || jsonSchema.readOnly === true,
|
|
120
|
+
default: jsonSchema.default,
|
|
121
|
+
assertValid: compiledSchema.assertValid
|
|
122
|
+
}
|
|
123
|
+
const FieldCls = SCHEMA_TYPE_TO_FIELD_CLASS_MAP[options.schema.type]
|
|
124
|
+
assert.ok(FieldCls, `unsupported field type ${options.schema.type}`)
|
|
125
|
+
|
|
126
|
+
const hasDefault = Object.prototype.hasOwnProperty.call(jsonSchema, 'default')
|
|
127
|
+
if (isKey) {
|
|
128
|
+
if (hasDefault && isKey) {
|
|
129
|
+
throw new InvalidOptionsError('default',
|
|
130
|
+
'No defaults for keys.') // It just doesn\'t make sense.
|
|
131
|
+
}
|
|
132
|
+
if (jsonSchema.readOnly === false) {
|
|
133
|
+
throw new InvalidOptionsError('immutable',
|
|
134
|
+
'Keys must be immutable.')
|
|
135
|
+
}
|
|
136
|
+
if (options.optional) {
|
|
137
|
+
throw new InvalidOptionsError('optional',
|
|
138
|
+
'Keys must never be optional.')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (hasDefault) {
|
|
142
|
+
validateValue(this.name, options, options.default)
|
|
143
|
+
}
|
|
144
|
+
return options
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @typedef {Object} FieldOptions
|
|
149
|
+
* @property {Boolean} [isKey=false] If true, the field is
|
|
150
|
+
* a key. When specified, other options are forced to be
|
|
151
|
+
* { optional: false, immutable: true, default: undefined }. If user
|
|
152
|
+
* supplied values that conflicts with those values, InvalidOptionsError
|
|
153
|
+
* will be thrown.
|
|
154
|
+
* @property {Boolean} [optional=false] If field can be left undefined.
|
|
155
|
+
* @property {Boolean} [immutable=false] If field can be changed again after
|
|
156
|
+
* value is set to anything except undefined.
|
|
157
|
+
* @property {*} [default=undefined] Default value to use. IMPORTANT: Value
|
|
158
|
+
* is deeply copied, so additional modifications to the parameter will
|
|
159
|
+
* not reflect in the field.
|
|
160
|
+
* @property {schema} [schema=undefined] An optional JSON schema
|
|
161
|
+
* to validate Field's value.
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {String} name the field's name (also the name of the underlying
|
|
166
|
+
* attribute in the database where this field is stored [except key
|
|
167
|
+
* components which are not stored in the db in their own attribute])
|
|
168
|
+
* @param {FieldOptions} opts
|
|
169
|
+
* @param {*} val the initial value of the field
|
|
170
|
+
* @param {boolean} valIsFromDB whether val is from (or is expected to be
|
|
171
|
+
* from) the database
|
|
172
|
+
* @param {boolean} valSpecified whether val was specified (if this is
|
|
173
|
+
* true, then the field was present)
|
|
174
|
+
* @param {boolean} isForUpdate whether this field is part of an update
|
|
175
|
+
*/
|
|
176
|
+
constructor ({
|
|
177
|
+
name,
|
|
178
|
+
opts,
|
|
179
|
+
val,
|
|
180
|
+
valIsFromDB,
|
|
181
|
+
valSpecified,
|
|
182
|
+
isForUpdate
|
|
183
|
+
}) {
|
|
184
|
+
super()
|
|
185
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
186
|
+
Object.defineProperty(this, key, { value, writable: false })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Setup states
|
|
190
|
+
/**
|
|
191
|
+
* @memberof Internal.__Field
|
|
192
|
+
* @instance
|
|
193
|
+
* @member {String} name The name of the owning property.
|
|
194
|
+
*/
|
|
195
|
+
this.name = name
|
|
196
|
+
this.__value = undefined
|
|
197
|
+
this.__readInitialValue = false // If get is called
|
|
198
|
+
this.__written = false // If set is called
|
|
199
|
+
this.__default = opts.default // only used for new docs!
|
|
200
|
+
|
|
201
|
+
// determine whether to use the default value, or the given val
|
|
202
|
+
let useDefault
|
|
203
|
+
if (valSpecified) {
|
|
204
|
+
// if val was specified, then use it
|
|
205
|
+
useDefault = false
|
|
206
|
+
} else {
|
|
207
|
+
assert.ok(val === undefined,
|
|
208
|
+
'valSpecified can only be false if val is undefined')
|
|
209
|
+
if (this.__default === undefined) {
|
|
210
|
+
// can't use the default if there is no default value
|
|
211
|
+
useDefault = false
|
|
212
|
+
} else if (valIsFromDB && this.optional) {
|
|
213
|
+
// if the field is optional and the value is not in the db then we
|
|
214
|
+
// can't use the default (we have to assume the field was omitted)
|
|
215
|
+
useDefault = false
|
|
216
|
+
} else {
|
|
217
|
+
useDefault = true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.__useDefault = useDefault
|
|
222
|
+
this.__value = useDefault ? deepcopy(this.__default) : val
|
|
223
|
+
|
|
224
|
+
if (valIsFromDB) {
|
|
225
|
+
// The field's current value is the value stored in the database. Track
|
|
226
|
+
// that value so that we can detect if it changes, and write that
|
|
227
|
+
// change to the database.
|
|
228
|
+
// Note: val is undefined whenever useDefault is true
|
|
229
|
+
this.__initialValue = deepcopy(val)
|
|
230
|
+
} else {
|
|
231
|
+
this.__initialValue = undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// validate the value, if needed
|
|
235
|
+
if (!this.isKey) { // keys are validated elsewhere; don't re-validate
|
|
236
|
+
if (!useDefault) { // default was validated by __validateFieldOptions
|
|
237
|
+
// validate everything except values omitted from an update() call
|
|
238
|
+
if (valSpecified || !isForUpdate) {
|
|
239
|
+
this.validate()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (valSpecified && isForUpdate) {
|
|
244
|
+
this.__isForUpdate = true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns the update value to give to Firestore. Assumes the value has been
|
|
250
|
+
* mutated!
|
|
251
|
+
*/
|
|
252
|
+
__valueForFirestoreWrite () {
|
|
253
|
+
if (this.__value === undefined) {
|
|
254
|
+
if (this.__initialValue !== undefined || this.__isForUpdate) {
|
|
255
|
+
return FieldValue.delete()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// complex types (objects and arrays) can be modified without calling set()
|
|
259
|
+
// so we need to double-check the validation for them at this point
|
|
260
|
+
this.validate()
|
|
261
|
+
return deepcopy(this.__value)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* This method compares initialValue against the current value.
|
|
266
|
+
*
|
|
267
|
+
* @returns if value was changed.
|
|
268
|
+
*/
|
|
269
|
+
get mutated () {
|
|
270
|
+
return this.__value !== this.__initialValue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A quick heuristic on if current value may differ from db. When this value
|
|
275
|
+
* is true, use comparison / deep equality to confirm if the value has been
|
|
276
|
+
* mutated. If this value is false, then the field must not have been
|
|
277
|
+
* mutated, and there is no need to confirm mutation further.
|
|
278
|
+
*/
|
|
279
|
+
get __mayHaveMutated () {
|
|
280
|
+
// A field may be mutated when it's written or if the value is a complex
|
|
281
|
+
// structure when it is read.
|
|
282
|
+
if (this.accessed) {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
// A field may still be mutated when it's never read or written, since the
|
|
286
|
+
// field may be initialized with a mutated value (default value from schema
|
|
287
|
+
// or default value upon creation).
|
|
288
|
+
if (this.__initialValue === undefined && this.__value !== undefined) {
|
|
289
|
+
return true
|
|
290
|
+
}
|
|
291
|
+
// Else the field must not be different from what's in database.
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* This is primarily used for optimistic locking.
|
|
297
|
+
* @returns {Boolean} if the field was accessed (read / write) by users of
|
|
298
|
+
* this library
|
|
299
|
+
*/
|
|
300
|
+
get accessed () {
|
|
301
|
+
return this.__readInitialValue || this.__written
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Gets the field's current value. Calling this method will mark the field as
|
|
306
|
+
* "{@link accessed}".
|
|
307
|
+
*
|
|
308
|
+
* @see {@link __value} for accessing value within the library without
|
|
309
|
+
* "accessing" the field
|
|
310
|
+
* @access public
|
|
311
|
+
*/
|
|
312
|
+
get () {
|
|
313
|
+
if (!this.__written) {
|
|
314
|
+
this.__readInitialValue = true
|
|
315
|
+
}
|
|
316
|
+
return this.__value
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* If the value passed in is valid, update field's current value, mark the
|
|
321
|
+
* field as "{@link accessed}". If the value is not valid, throws
|
|
322
|
+
* InvalidFieldError.
|
|
323
|
+
*
|
|
324
|
+
* @param {*} value New value for the field.
|
|
325
|
+
* @affects {@link __Field#accessed}
|
|
326
|
+
* @access public
|
|
327
|
+
*/
|
|
328
|
+
set (val) {
|
|
329
|
+
if (this.immutable) {
|
|
330
|
+
throw new InvalidFieldError(
|
|
331
|
+
this.name,
|
|
332
|
+
'is immutable so value cannot be changed after first initialized.')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const prev = [this.__value, this.__written]
|
|
336
|
+
this.__value = val
|
|
337
|
+
this.__written = true
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
this.validate()
|
|
341
|
+
} catch (e) {
|
|
342
|
+
[this.__value, this.__written] = prev
|
|
343
|
+
throw e
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Checks if the field's current value is valid. Throws InvalidFieldError if
|
|
349
|
+
* check fails.
|
|
350
|
+
*/
|
|
351
|
+
validate () {
|
|
352
|
+
validateValue(this.name, this, this.__value)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @extends Internal.__Field
|
|
358
|
+
* @memberof Internal.Fields
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
class NumberField extends __Field {
|
|
362
|
+
constructor (options) {
|
|
363
|
+
super(options)
|
|
364
|
+
this.__diff = undefined
|
|
365
|
+
this.__mustUseSet = false
|
|
366
|
+
|
|
367
|
+
// figure out what value the diff will be added to
|
|
368
|
+
if (this.__initialValue !== undefined) {
|
|
369
|
+
// this occurs when the field is on an existing doc AND the doc already
|
|
370
|
+
// had a value for the field
|
|
371
|
+
this.__base = this.__initialValue
|
|
372
|
+
} else if (this.__value !== undefined) {
|
|
373
|
+
// this case occurs when the field is on a new doc
|
|
374
|
+
this.__base = this.__value
|
|
375
|
+
} else {
|
|
376
|
+
// this case occurs if the field is not currently present on the doc;
|
|
377
|
+
// in this case increment cannot be used to update the doc
|
|
378
|
+
this.__base = undefined
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
set (val) {
|
|
383
|
+
super.set(val)
|
|
384
|
+
// don't change any state unless set() succeeds
|
|
385
|
+
this.__diff = undefined // no longer computed as a diff
|
|
386
|
+
this.__mustUseSet = true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Updates the field's value by an unconditioned increment IF the field is
|
|
391
|
+
* never read (reduces contention). If the field is ever read, there's no
|
|
392
|
+
* reason to use this.
|
|
393
|
+
* @param {Number} diff The diff amount.
|
|
394
|
+
*/
|
|
395
|
+
incrementBy (diff) {
|
|
396
|
+
// add the new diff to our current diff, if any
|
|
397
|
+
// wait to set __diff until after super.set() succeeds to ensure no
|
|
398
|
+
// changes are made if set() fails!
|
|
399
|
+
const newDiff = (this.__diff === undefined) ? diff : this.__diff + diff
|
|
400
|
+
|
|
401
|
+
// if we've already read the value, there's no point in generating an
|
|
402
|
+
// increment update expression as we must lock on the original value anyway
|
|
403
|
+
if (this.__readInitialValue || this.__mustUseSet) {
|
|
404
|
+
this.set(this.__sumIfValid(false, newDiff))
|
|
405
|
+
// this.__diff isn't set because we can't not using diff updates now
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// call directly on super to avoid clearing the diff value
|
|
410
|
+
super.set(this.__sumIfValid(true, newDiff))
|
|
411
|
+
this.__diff = newDiff
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Returns the sum of diff and some value. Throws if the latter is undefined.
|
|
416
|
+
* @param {boolean} fromBase whether to add __base (else __value)
|
|
417
|
+
* @param {number} diff how much to add
|
|
418
|
+
* @returns {number} sum
|
|
419
|
+
*/
|
|
420
|
+
__sumIfValid (fromBase, diff) {
|
|
421
|
+
const base = fromBase ? this.__base : this.__value
|
|
422
|
+
if (base === undefined) {
|
|
423
|
+
throw new InvalidFieldError(
|
|
424
|
+
this.name, 'cannot increment a field whose value is undefined')
|
|
425
|
+
}
|
|
426
|
+
return base + diff
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Whether this field can be updated with an increment expression.
|
|
431
|
+
*/
|
|
432
|
+
get canUpdateWithIncrement () {
|
|
433
|
+
return (
|
|
434
|
+
// if there's no diff, we cannot use increment
|
|
435
|
+
this.__diff !== undefined &&
|
|
436
|
+
// if the field didn't have an old value, we can't increment it
|
|
437
|
+
this.__initialValue !== undefined &&
|
|
438
|
+
// if we read the value, then it's not a blind increment
|
|
439
|
+
!this.__readInitialValue)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
__valueForFirestoreWrite () {
|
|
443
|
+
// if we're locking, there's no point in doing an increment
|
|
444
|
+
if (this.canUpdateWithIncrement) {
|
|
445
|
+
return FieldValue.increment(this.__diff)
|
|
446
|
+
}
|
|
447
|
+
return super.__valueForFirestoreWrite()
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @extends Internal.__Field
|
|
453
|
+
* @memberof Internal.Fields
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
class StringField extends __Field {
|
|
457
|
+
get mutated () {
|
|
458
|
+
return this.__mayHaveMutated && super.mutated
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* @extends Internal.__Field
|
|
464
|
+
* @memberof Internal.Fields
|
|
465
|
+
* @private
|
|
466
|
+
*/
|
|
467
|
+
class ObjectField extends __Field {
|
|
468
|
+
/**
|
|
469
|
+
* This method checks for equality deeply against the initial
|
|
470
|
+
* value so use it as sparsely as possible. It is primarily meant to be
|
|
471
|
+
* used internally for deciding whether a field needs to be transmitted to
|
|
472
|
+
* the server.
|
|
473
|
+
*
|
|
474
|
+
* @returns if value was changed.
|
|
475
|
+
*/
|
|
476
|
+
get mutated () {
|
|
477
|
+
return this.__mayHaveMutated && !deepeq(this.__value, this.__initialValue)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* @extends Internal.__Field
|
|
483
|
+
* @memberof Internal.Fields
|
|
484
|
+
* @private
|
|
485
|
+
*/
|
|
486
|
+
class BooleanField extends __Field {}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @extends Internal.__Field
|
|
490
|
+
* @memberof Internal.Fields
|
|
491
|
+
* @private
|
|
492
|
+
*/
|
|
493
|
+
class ArrayField extends __Field {
|
|
494
|
+
/**
|
|
495
|
+
* This method checks for equality deeply against the initial
|
|
496
|
+
* value so use it as sparsely as possible. It is primarily meant to be
|
|
497
|
+
* used internally for deciding whether a field needs to be transmitted to
|
|
498
|
+
* the server.
|
|
499
|
+
*
|
|
500
|
+
* @returns if value was changed.
|
|
501
|
+
*/
|
|
502
|
+
get mutated () {
|
|
503
|
+
return this.__mayHaveMutated && !deepeq(this.__value, this.__initialValue)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const SCHEMA_TYPE_TO_FIELD_CLASS_MAP = {
|
|
508
|
+
array: ArrayField,
|
|
509
|
+
boolean: BooleanField,
|
|
510
|
+
float: NumberField,
|
|
511
|
+
integer: NumberField,
|
|
512
|
+
number: NumberField,
|
|
513
|
+
object: ObjectField,
|
|
514
|
+
string: StringField
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
module.exports = {
|
|
518
|
+
__FieldInterface,
|
|
519
|
+
__Field,
|
|
520
|
+
NumberField,
|
|
521
|
+
ArrayField,
|
|
522
|
+
BooleanField,
|
|
523
|
+
StringField,
|
|
524
|
+
ObjectField,
|
|
525
|
+
SCHEMA_TYPE_TO_FIELD_CLASS_MAP
|
|
526
|
+
}
|