@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
package/README.md
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
# Firestore ORM Library <!-- omit in toc -->
|
|
2
|
+
This library is used to interact with the Firestore NoSQL database. It provides
|
|
3
|
+
high-level abstractions to structure data and prevent race conditions.
|
|
4
|
+
|
|
5
|
+
[](https://pbv-public.github.io/firestore-orm)
|
|
6
|
+
|
|
7
|
+
- [Core Concepts](#core-concepts)
|
|
8
|
+
- [Minimal Example](#minimal-example)
|
|
9
|
+
- [Collections](#collections)
|
|
10
|
+
- [Keys](#keys)
|
|
11
|
+
- [Fields](#fields)
|
|
12
|
+
- [Schema Enforcement](#schema-enforcement)
|
|
13
|
+
- [Custom Methods](#custom-methods)
|
|
14
|
+
- [Transactions](#transactions)
|
|
15
|
+
- [ACID Properties](#acid-properties)
|
|
16
|
+
- [Retries](#retries)
|
|
17
|
+
- [Read-Only](#read-only)
|
|
18
|
+
- [Pre-Commit Hook](#pre-commit-hook)
|
|
19
|
+
- [Warning: Race Conditions](#warning-race-conditions)
|
|
20
|
+
- [Warning: Side Effects](#warning-side-effects)
|
|
21
|
+
- [Operations](#operations)
|
|
22
|
+
- [Addressing Documents](#addressing-documents)
|
|
23
|
+
- [Create](#create)
|
|
24
|
+
- [Read](#read)
|
|
25
|
+
- [Create if Missing](#create-if-missing)
|
|
26
|
+
- [Read Consistency](#read-consistency)
|
|
27
|
+
- [Batch Read](#batch-read)
|
|
28
|
+
- [Write](#write)
|
|
29
|
+
- [Delete](#delete)
|
|
30
|
+
- [Performance](#performance)
|
|
31
|
+
- [Blind Writes](#blind-writes)
|
|
32
|
+
- [incrementBy()](#incrementby)
|
|
33
|
+
- [Locking](#locking)
|
|
34
|
+
- [Niche Concepts](#niche-concepts)
|
|
35
|
+
- [Key Encoding](#key-encoding)
|
|
36
|
+
- [Nested Transactions are NOT Nested](#nested-transactions-are-not-nested)
|
|
37
|
+
- [Collection Creation \& Persistence](#collection-creation--persistence)
|
|
38
|
+
- [Repeated Reads](#repeated-reads)
|
|
39
|
+
- [Key Collection](#key-collection)
|
|
40
|
+
- [Library Collaborator's Guide](#library-collaborators-guide)
|
|
41
|
+
- [Transactions](#transactions-1)
|
|
42
|
+
- [Appendix](#appendix)
|
|
43
|
+
- [Useful Links](#useful-links)
|
|
44
|
+
- [Not Yet Implemented](#not-yet-implemented)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Core Concepts
|
|
48
|
+
Data is organized into Firestore Collections (similar to what's often called
|
|
49
|
+
tables in other databases). Each contains Firestore Documents (similar to rows
|
|
50
|
+
in other databases). Each document ("doc") is uniquely identified by a
|
|
51
|
+
[_Key_](#keys).
|
|
52
|
+
|
|
53
|
+
## Minimal Example
|
|
54
|
+
Define a new collection like this, using the
|
|
55
|
+
[Schema library](https://github.com/pbv-public/js-schema) to enforce a
|
|
56
|
+
schema:
|
|
57
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:Order -->
|
|
58
|
+
class OrderWithNoPrice extends db.Model {
|
|
59
|
+
static FIELDS = {
|
|
60
|
+
product: S.str,
|
|
61
|
+
quantity: S.int
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then we can create a new doc:
|
|
67
|
+
```javascript
|
|
68
|
+
const id = uuidv4()
|
|
69
|
+
tx.create(Order, { id, product: 'coffee', quantity: 1 })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Later, we can retrieve it from the database and modify it:
|
|
73
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:DBReadmeTest:testMinimalExample:Example -->
|
|
74
|
+
// Example
|
|
75
|
+
await db.Context.run(async tx => {
|
|
76
|
+
const order = await tx.get(OrderWithNoPrice, id)
|
|
77
|
+
expect(order.id).toBe(id)
|
|
78
|
+
expect(order.product).toBe('coffee')
|
|
79
|
+
expect(order.quantity).toBe(1)
|
|
80
|
+
order.quantity = 2
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Collections
|
|
86
|
+
|
|
87
|
+
### Keys
|
|
88
|
+
Each doc is uniquely identified by a key. By default, the key is composed of a
|
|
89
|
+
single field named `id` which has the format of a UUIDv4 string (e.g.,
|
|
90
|
+
`"c40ef065-4034-4be8-8a1d-0959695b213e"`) typically produced by calling
|
|
91
|
+
`uuidv4()`, as shown in the minimal example above. A doc's key cannot be
|
|
92
|
+
changed.
|
|
93
|
+
|
|
94
|
+
You can override the default and define your key to be composed of one _or
|
|
95
|
+
more_ fields with an arbitrary
|
|
96
|
+
[Schema](https://github.com/pbv-public/js-schema)s (`S`):
|
|
97
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:RaceResult -->
|
|
98
|
+
class RaceResult extends db.Model {
|
|
99
|
+
static KEY = {
|
|
100
|
+
raceID: S.int,
|
|
101
|
+
runnerName: S.str
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Access each component of a key just like any other field:
|
|
107
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:DBReadmeTest:testKeys -->
|
|
108
|
+
async testKeys () {
|
|
109
|
+
await db.Context.run(async tx => {
|
|
110
|
+
const raceResult = await tx.get(
|
|
111
|
+
RaceResult,
|
|
112
|
+
{ raceID: 99, runnerName: 'Bo' },
|
|
113
|
+
{ createIfMissing: true })
|
|
114
|
+
expect(raceResult.raceID).toBe(99)
|
|
115
|
+
expect(raceResult.runnerName).toBe('Bo')
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
It is best practice for keys to have semantic meaning whenever possible. In
|
|
121
|
+
this example, each runner finishes each race just one time so making the key a
|
|
122
|
+
combination of those values is ideal. This is better than a meaningless random
|
|
123
|
+
value because this:
|
|
124
|
+
1. Enforces the constraint that each runner finishes each race no more than
|
|
125
|
+
once. If the ID was a random value, we could accidentally create two race
|
|
126
|
+
results for one runner in the same race.
|
|
127
|
+
1. Enables us efficiently construct the ID from relevant information (e.g.,
|
|
128
|
+
to check if a runner finished a specific race). If the ID was was a random
|
|
129
|
+
value, we'd have to do some sort of search to figure out the ID associated
|
|
130
|
+
with a given race ID and runner name (slow because this would involve a
|
|
131
|
+
database query instead of a simple local computation!).
|
|
132
|
+
|
|
133
|
+
Note: Keys are collection-specific. Two different docs in different collections may have
|
|
134
|
+
the same key.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### Fields
|
|
138
|
+
Fields are pieces of data attached to a doc. They are defined similar to
|
|
139
|
+
`KEY` -- a doc can have one _or more_ fields with arbitrary
|
|
140
|
+
[Schema](https://github.com/pbv-public/js-schema)s:
|
|
141
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:ModelWithFields -->
|
|
142
|
+
class ModelWithFieldsExample extends db.Model {
|
|
143
|
+
static FIELDS = {
|
|
144
|
+
someInt: S.int.min(0),
|
|
145
|
+
someBool: S.bool,
|
|
146
|
+
someObj: S.obj().prop('arr', S.arr(S.str))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
* Field names are serialized and stored in the database.
|
|
152
|
+
Avoid having fields with long verbose names, especially for repeated values.
|
|
153
|
+
* If you change the schema, existing data isn't changed.
|
|
154
|
+
That includes docs with now missing field names. [Schema Enforcement](#schema-enforcement)
|
|
155
|
+
|
|
156
|
+
Fields can be configured to be optional, immutable and/or have default values:
|
|
157
|
+
* `optional()` - unless a field is marked as optional, a value must be
|
|
158
|
+
provided (i.e., it cannot be omitted or set to `undefined`)
|
|
159
|
+
* `readOnly()` - if a field is marked as read only, it cannot be changed once
|
|
160
|
+
the doc has been created
|
|
161
|
+
* `default()` - the default value for a field
|
|
162
|
+
* This value gets deep copied so you can safely use non-primitive types
|
|
163
|
+
like objects as a default value.
|
|
164
|
+
* The default value is assigned to a field when:
|
|
165
|
+
* A doc is created and no value is specified for the value.
|
|
166
|
+
* A doc is fetched and is is missing the specified field _AND_ the
|
|
167
|
+
field is required.
|
|
168
|
+
* The default value is _not_ assigned to an optional field that is missing
|
|
169
|
+
when it is fetched from the database.
|
|
170
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:ComplexFieldsExample -->
|
|
171
|
+
class ComplexFieldsExample extends db.Model {
|
|
172
|
+
static FIELDS = {
|
|
173
|
+
aNonNegInt: S.int.min(0),
|
|
174
|
+
anOptBool: S.bool.optional(), // default value is undefined
|
|
175
|
+
// this field defaults to 5; once it is set, it cannot be changed (though
|
|
176
|
+
// it won't always be 5 since it can be created with a non-default value)
|
|
177
|
+
immutableInt: S.int.readOnly().default(5)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
```javascript <!-- embed:./test/unit-test-doc.js:section:example1122start:example1122end -->
|
|
182
|
+
// can omit the optional field
|
|
183
|
+
const doc = tx.create(ComplexFieldsExample, {
|
|
184
|
+
id: uuidv4(),
|
|
185
|
+
aNonNegInt: 0,
|
|
186
|
+
immutableInt: 3
|
|
187
|
+
})
|
|
188
|
+
expect(doc.aNonNegInt).toBe(0)
|
|
189
|
+
// omitted optional field => undefined
|
|
190
|
+
expect(doc.anOptBool).toBe(undefined)
|
|
191
|
+
expect(doc.immutableInt).toBe(3)
|
|
192
|
+
|
|
193
|
+
// can override the default value
|
|
194
|
+
const doc2 = tx.create(ComplexFieldsExample, {
|
|
195
|
+
id: uuidv4(),
|
|
196
|
+
aNonNegInt: 1,
|
|
197
|
+
anOptBool: true
|
|
198
|
+
})
|
|
199
|
+
expect(doc2.aNonNegInt).toBe(1)
|
|
200
|
+
expect(doc2.anOptBool).toBe(true)
|
|
201
|
+
expect(doc2.immutableInt).toBe(5) // the default value
|
|
202
|
+
// can't change read only fields:
|
|
203
|
+
expect(() => { doc2.immutableInt = 3 }).toThrow(
|
|
204
|
+
'immutableInt is immutable so value cannot be changed')
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
### Schema Enforcement
|
|
209
|
+
A model's schema (i.e., the structure of its data) is enforced by this library
|
|
210
|
+
— _NOT_ the underlying database! Firestore, like most NoSQL databases, is
|
|
211
|
+
effectively schemaless. This means each doc may
|
|
212
|
+
theoretically contain completely different data. This normally won't be the
|
|
213
|
+
case because `db.Model` enforces a consistent schema on documents in a collection.
|
|
214
|
+
|
|
215
|
+
However, it's important to understand that this schema is _only_ enforced by
|
|
216
|
+
`db.Model` and not the underlying database. This means **changing the model
|
|
217
|
+
does not change any underlying data** in the database. For example, if we make
|
|
218
|
+
a previously optional field required, old documents which omitted the value will
|
|
219
|
+
still be missing the value.
|
|
220
|
+
|
|
221
|
+
The schema is checked as follows:
|
|
222
|
+
1. When a field's value is changed, it is validated. If a value is a
|
|
223
|
+
reference (e.g., an object or array), then changing a value inside the
|
|
224
|
+
reference does _not_ trigger a validation check.
|
|
225
|
+
```javascript
|
|
226
|
+
// fields are checked immediately when creating a new doc; this throws
|
|
227
|
+
// S.ValidationError because someInt should be an integer
|
|
228
|
+
const data = {
|
|
229
|
+
id: uuidv4(),
|
|
230
|
+
someInt: '1', // does not match the schema S.int)!
|
|
231
|
+
someBool: true,
|
|
232
|
+
someObj: { arr: [] }
|
|
233
|
+
}
|
|
234
|
+
tx.create(ModelWithFields, data) // throws because someInt is invalid
|
|
235
|
+
|
|
236
|
+
data.someInt = 1
|
|
237
|
+
const x = tx.create(ModelWithFields, data)
|
|
238
|
+
|
|
239
|
+
// fields are checked when set too
|
|
240
|
+
x.someBool = 1 // throws because the type should be boolean not int
|
|
241
|
+
x.someObj = {} // throws because the required "arr" key is missing
|
|
242
|
+
x.someObj = { arr: [5] } // throws b/c this arr must contain strings
|
|
243
|
+
x.someObj = { arr: ['ok'] } // ok!
|
|
244
|
+
|
|
245
|
+
// changes within a non-primitive type aren't detected or validated
|
|
246
|
+
// until we try to write the change so this next line won't throw!
|
|
247
|
+
x.someObj.arr.push(5)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
2. Any fields that will be written to the database are validated prior to
|
|
251
|
+
writing them. This occurs when a [transaction](#transactions) commit
|
|
252
|
+
starts. This catches schema validation errors like the one on the last
|
|
253
|
+
line of the previous example.
|
|
254
|
+
|
|
255
|
+
3. Keys are validated whenever they are created or read, like these examples:
|
|
256
|
+
```javascript
|
|
257
|
+
const compoundID = { raceID: 1, runnerName: 'Alice' }
|
|
258
|
+
// each of these three trigger a validation check (to verify that
|
|
259
|
+
// compoundID contains every key component and that each of them meet
|
|
260
|
+
// their respective schemas requirements)
|
|
261
|
+
RaceResult.key(compoundID)
|
|
262
|
+
tx.create(RaceResult, compoundID)
|
|
263
|
+
await tx.get(RaceResult, compoundID)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
4. Fields validation can be manually triggered:
|
|
267
|
+
```javascript
|
|
268
|
+
x.getField('someObj').validate()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Custom Methods
|
|
272
|
+
As you've noticed, key components and fields are simply accessed by their names
|
|
273
|
+
(e.g., `raceResult.runnerName` or `order.product`). You can also define
|
|
274
|
+
instance methods on your models to provide additional functionality:
|
|
275
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:OrderWithPrice -->
|
|
276
|
+
class OrderWithPrice extends db.Model {
|
|
277
|
+
static FIELDS = {
|
|
278
|
+
quantity: S.int,
|
|
279
|
+
unitPrice: S.int.desc('price per unit in cents')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
totalPrice (salesTax = 0.1) {
|
|
283
|
+
const subTotal = this.quantity * this.unitPrice
|
|
284
|
+
return subTotal * (1 + salesTax)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
And use them like you'd expect:
|
|
289
|
+
```javascript
|
|
290
|
+
const order = tx.create(OrderWithPrice, { id, quantity: 2 , unitPrice: 200 })
|
|
291
|
+
expect(order.totalPrice(0.1)).toBeCloseTo(440)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
## Transactions
|
|
296
|
+
A transaction is a function which contains logic and database operations. A
|
|
297
|
+
transaction guarantees that all _database_ side effects (e.g., updating a
|
|
298
|
+
doc) execute in an all-or-nothing manner, providing [ACID](#acid-properties) properties.
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
### ACID Properties
|
|
302
|
+
[ACID](https://en.wikipedia.org/wiki/ACID) properties are commonly provided by
|
|
303
|
+
traditional, transaction-processing databases:
|
|
304
|
+
|
|
305
|
+
* _Atomicity_ - every database operation (e.g., an update) will succeed, or
|
|
306
|
+
none will succeed. The database will never be partially updated.
|
|
307
|
+
* _Consistency_ - data written to the database will always be consistent with
|
|
308
|
+
the constraints specified by the models (e.g., it is not possible to store a
|
|
309
|
+
string in an integer field).
|
|
310
|
+
* _Isolation_ - each transaction will appear to operate sequentially;
|
|
311
|
+
uncommitted data cannot be read.
|
|
312
|
+
* _Durability_ - if a transaction succeeds, any data that is changed will be
|
|
313
|
+
remembered. There is no chance of it being lost (e.g., due to a power
|
|
314
|
+
outage).
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
### Retries
|
|
318
|
+
When a transaction fails due to contention, it will retry after a short, random
|
|
319
|
+
delay. Randomness helps prevent conflicting transactions from conflicting again
|
|
320
|
+
when they retry. Context retry behaviors can be customized:
|
|
321
|
+
```javascript
|
|
322
|
+
const retryOptions = {
|
|
323
|
+
retries: 4, // 1 initial run + up to 4 retry attempts = max 5 total attempts
|
|
324
|
+
initialBackoff: 100, // 100 milliseconds (+/- a small random offset)
|
|
325
|
+
maxBackoff: 500 // no more than 500 milliseconds
|
|
326
|
+
}
|
|
327
|
+
await db.Context.run(retryOptions, async tx => {
|
|
328
|
+
// you can also manually force your transaction to retry by throwing a
|
|
329
|
+
// custom exception with the "retryable" property set to true
|
|
330
|
+
const error = new Error()
|
|
331
|
+
error.retryable = true
|
|
332
|
+
throw error
|
|
333
|
+
})
|
|
334
|
+
// Exponential backoff function doubles the backoff each time (up to the max)
|
|
335
|
+
// t=0ms, initial run
|
|
336
|
+
// t=100ms, retry 1 (backed off for 100ms)
|
|
337
|
+
// t=300ms, retry 2 (backed off again, this time for 200ms)
|
|
338
|
+
// t=700ms, retry 3 (backed off again, this time for 400ms)
|
|
339
|
+
// t=1200ms, retry 4 (backed off for 500ms this time; was capped by maxBackoff)
|
|
340
|
+
// fail
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Read-Only
|
|
344
|
+
You can ensure a transaction does not make any database changes by setting the
|
|
345
|
+
`readOnly` option to true, or calling `tx.makeReadOnly()`:
|
|
346
|
+
```javascript
|
|
347
|
+
const readOnlyOption = { readOnly: true }
|
|
348
|
+
await db.Context.run(readOnlyOption, async tx => { /* ... */ })
|
|
349
|
+
await db.Context.run(async tx => {
|
|
350
|
+
tx.makeReadOnly()
|
|
351
|
+
// ...
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Pre-Commit Hook
|
|
356
|
+
A model might need to execute some logic before it is committed. For example, a `Ledger` model may want to update a `ver` field any time it is updated. Such logic can be achieved through the `Model.finalize` hook.
|
|
357
|
+
|
|
358
|
+
```javascript <!-- embed:./test/unit-test-transaction.js:scope:HookExample -->
|
|
359
|
+
class HookExample extends db.Model {
|
|
360
|
+
static KEY = { id: S.str.min(1) }
|
|
361
|
+
static FIELDS = {
|
|
362
|
+
field1: S.int.default(0),
|
|
363
|
+
latestUpdateEpoch: S.int.default(0)
|
|
364
|
+
.desc('latest update epoch in milliseconds')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async finalize () {
|
|
368
|
+
this.latestUpdateEpoch = Date.now()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
### Warning: Race Conditions
|
|
375
|
+
Race conditions are still possible! If your context doesn't use a transaction,
|
|
376
|
+
individual reads may not be consistent with one another. A transaction is used
|
|
377
|
+
if your context is not read only or if using consistent reads (the default).
|
|
378
|
+
Consider a ski resort which records some stats about skiers and lifts:
|
|
379
|
+
```javascript
|
|
380
|
+
class SkierStats extends db.Model {
|
|
381
|
+
static KEY = { resort: S.str }
|
|
382
|
+
static FIELDS = { numSkiers: S.int.min(0).default(0) }
|
|
383
|
+
}
|
|
384
|
+
class LiftStats extends db.Model {
|
|
385
|
+
static KEY = { resort: S.str }
|
|
386
|
+
static FIELDS = { numLiftRides: S.int.min(0).default(0) }
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
We can correctly update these numbers in a transaction like this:
|
|
391
|
+
```javascript
|
|
392
|
+
async function liftRideTaken(resort, isNewSkier) {
|
|
393
|
+
await db.Context.run(async tx => {
|
|
394
|
+
const opts = { createIfMissing: true }
|
|
395
|
+
const [skierStats, liftStats] = await Promise.all([
|
|
396
|
+
!isNewSkier ? Promise.resolve() : tx.get(SkierStats, resort, opts),
|
|
397
|
+
tx.get(LiftStats, resort, opts)])
|
|
398
|
+
if (isNewSkier) {
|
|
399
|
+
skierStats.numSkiers += 1
|
|
400
|
+
}
|
|
401
|
+
liftStats.numLiftRides += 1
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
However, if we try to read them individually from different transactions or
|
|
407
|
+
without setting the consistent read option (the default) we can't guarantee a
|
|
408
|
+
consistent snapshot.
|
|
409
|
+
```javascript
|
|
410
|
+
const skierStats = await tx.get(SkierStats, resort)
|
|
411
|
+
const liftStats = await tx.get(LiftStats, resort)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
This sequence is possible:
|
|
415
|
+
1. We issue requests to read SkierStats and LiftStats, as above.
|
|
416
|
+
1. We call `liftRideTaken('someResort', true)`
|
|
417
|
+
1. The request to read skier stats complete: `numSkiers=0`
|
|
418
|
+
1. The `liftRideTaken('someResort', true)` completes, transactionally
|
|
419
|
+
updating the database to `numSkiers=1` and `numLiftRides=1`.
|
|
420
|
+
1. The request to read lift stats complete: `numLiftRides=1` _!!!_
|
|
421
|
+
1. Our application code thinks there was one lift ride taken, but no skiers.
|
|
422
|
+
|
|
423
|
+
To ensure this does not occur, use a transaction (readOnly=false) and/or stick
|
|
424
|
+
with the default consistentReads=true.
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
### Warning: Side Effects
|
|
428
|
+
Keep in mind that transactions only guarantee all-or-nothing (or more
|
|
429
|
+
precisely, exactly once or not at all semantics) for _database_ operations. If
|
|
430
|
+
the application code which defines the transaction has side effects, those side
|
|
431
|
+
effects may occur even if the transaction doesn't commit. They could even occur
|
|
432
|
+
multiple times (if your transaction retries).
|
|
433
|
+
```javascript
|
|
434
|
+
await db.Context.run(async tx => {
|
|
435
|
+
const doc = await tx.get(...)
|
|
436
|
+
doc.someInt += 1
|
|
437
|
+
if (doc.someInt > 10) {
|
|
438
|
+
// making an HTTP request is a side effect!
|
|
439
|
+
await got('https://example.com/gotSomeIntBiggerThan10')
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
In this example, the HTTP request might be completed one or more times, even if
|
|
445
|
+
the transaction never completes successfully!
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
## Operations
|
|
449
|
+
_All_ databases operations occur in the scope of a transaction (unless you
|
|
450
|
+
specifically disable it by setting readOnly=true _and_ consistentRead=false).
|
|
451
|
+
We typically name the transaction object `tx` in code. This section discusses
|
|
452
|
+
the operations supported by `tx`.
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
### Addressing Documents
|
|
456
|
+
Database operations always occur on a particular doc. The canonical way to identify a particular doc is:
|
|
457
|
+
```javascript
|
|
458
|
+
MyModel.key({ /* a map of key component names to their values */ })
|
|
459
|
+
Order.key({ id: uuidv4() })
|
|
460
|
+
RaceResult.key({ raceID: 1, runnerName: 'Dave' })
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
For models which have only a single key field, you _may_ omit the field name:
|
|
464
|
+
```javascript
|
|
465
|
+
Order.key(uuid4())
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The `db.Key` object produced by this `key()` method is used as the first
|
|
469
|
+
argument for retrieving data:
|
|
470
|
+
```javascript
|
|
471
|
+
tx.get(Order.key(id))
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
For convenience, you may also split the model class and key values up into two
|
|
475
|
+
arguments:
|
|
476
|
+
```javascript
|
|
477
|
+
tx.get(Order, id)
|
|
478
|
+
tx.get(RaceResult, { raceID, runnerName })
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
### Create
|
|
483
|
+
`tx.create()` instantiates a new doc in local memory. This method is a local,
|
|
484
|
+
**synchronous** method (no network traffic is generated). If a doc with the
|
|
485
|
+
same key already exists, a `db.ModelAlreadyExistsError` is thrown when the
|
|
486
|
+
transaction attempts to commit (without retries, as we don't expect docs to be
|
|
487
|
+
deleted).
|
|
488
|
+
|
|
489
|
+
To create a doc, you need to supply the model (the type of data you're
|
|
490
|
+
creating) and a map of its initial values:
|
|
491
|
+
```javascript
|
|
492
|
+
tx.create(Order, { id, product: 'coffee', quantity: 1 })
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
### Read
|
|
497
|
+
`tx.get()` **asynchronously** retrieves data from the database. Network traffic
|
|
498
|
+
is generated to ask the database for the data as soon as the method is called,
|
|
499
|
+
but other work can be done while waiting.
|
|
500
|
+
```javascript
|
|
501
|
+
const orderPromise = tx.get(Order, id)
|
|
502
|
+
// do some other work
|
|
503
|
+
const order = await orderPromise // block until the data has been retrieved
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`tx.get()` accepts an additional options to configure its behavior:
|
|
507
|
+
* `createIfMissing` - see [Create if Missing](#create-if-missing)
|
|
508
|
+
|
|
509
|
+
Firestore transactions require all reads to have been completed before issuing
|
|
510
|
+
write requests.
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
#### Create if Missing
|
|
514
|
+
If the doc does not exist in the database, then by default the returned value
|
|
515
|
+
will be `undefined`. You may ask for it to instead be created if it does not
|
|
516
|
+
exist. To do this, you need to supply not only the doc's key, but also the
|
|
517
|
+
data you want it to have _if_ it does not yet exist:
|
|
518
|
+
```javascript
|
|
519
|
+
const dataIfOrderIsNew = { id, product: 'coffee', quantity: 1 }
|
|
520
|
+
const order = await tx.get(Order, dataIfOrderIsNew, { createIfMissing: true })
|
|
521
|
+
if (order.isNew) { // you can check if the doc already existed or not
|
|
522
|
+
// ...
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
The `isNew` property is set when the model is instantiated (after receiving the
|
|
527
|
+
database's response to our data request). When the transaction commits, it will
|
|
528
|
+
ensure that the doc will be created if `isNew=true` (i.e., the doc
|
|
529
|
+
wasn't created by someone else in the meantime) or still exists if
|
|
530
|
+
`isNew=false` (i.e., the doc hasn't been deleted in the meantime).
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
#### Read Consistency
|
|
534
|
+
Each individual get is strongly consistent (you'll get the latest data as of
|
|
535
|
+
that moment). In transactions, all reads will be consistent with each other
|
|
536
|
+
too.
|
|
537
|
+
|
|
538
|
+
#### Batch Read
|
|
539
|
+
It is also possible to call `tx.get()` with an array of keys in order to fetch
|
|
540
|
+
many things at once:
|
|
541
|
+
```javascript
|
|
542
|
+
const [order1, order2, raceResult] = await tx.get([
|
|
543
|
+
Order.key(id),
|
|
544
|
+
Order.key(anotherID),
|
|
545
|
+
RaceResult.key({ raceID, runnerName })
|
|
546
|
+
])
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
This can also be combined with `createIfMissing`:
|
|
550
|
+
```javascript
|
|
551
|
+
const [order1, order2, raceResult] = await tx.get([
|
|
552
|
+
Order.data({ id, product: 'coffee', quantity: 1 }),
|
|
553
|
+
Order.data({ id: anotherID, product: 'spoon', quantity: 10 }),
|
|
554
|
+
RaceResult.data({ raceID, runnerName })
|
|
555
|
+
], { createIfMissing: true })
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
* Data is fetched transactionally and will be a consistent snapshot
|
|
559
|
+
(see [race conditions](#warning-race-conditions) for more about this).
|
|
560
|
+
|
|
561
|
+
### Write
|
|
562
|
+
To modify data in the database, simply modify fields on a doc created by
|
|
563
|
+
`tx.create()` or fetched by `tx.get()`. When the transaction commits, all
|
|
564
|
+
changes will be written to the database automatically.
|
|
565
|
+
|
|
566
|
+
For improved performance, data can be updated without being read from database
|
|
567
|
+
first. See details in [blind writes](#blind-writes).
|
|
568
|
+
|
|
569
|
+
### Delete
|
|
570
|
+
Documents can be deleted from the database via `tx.delete()`. The delete method
|
|
571
|
+
accepts models or keys as parameters. For example,
|
|
572
|
+
`tx.delete(model1, key1, model2, ...keys, key2)`.
|
|
573
|
+
|
|
574
|
+
For models that were read from server via `tx.get()`, if the model turns out to
|
|
575
|
+
be missing on server when the transaction commits, an exception is thrown.
|
|
576
|
+
Otherwise, deletion on missing docs will be treated as a no-op.
|
|
577
|
+
|
|
578
|
+
## Performance
|
|
579
|
+
### Blind Writes
|
|
580
|
+
Blind updates write a doc to the DB without reading it first. This is useful
|
|
581
|
+
when we wish to update them without the overhead of an unnecessary read (in
|
|
582
|
+
theory, the update can have preconditions but this isn't supported yet... just
|
|
583
|
+
do a read in that case to verify them):
|
|
584
|
+
```javascript
|
|
585
|
+
// this updates the specified order doc to quantity=2
|
|
586
|
+
tx.updateWithoutRead(Order, { id, quantity: 2 })
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Similarly, docs can be blindly created or overwritten with the
|
|
590
|
+
`createOrOverwrite` method. This is useful when we don't care about the
|
|
591
|
+
previous value (if any). For example, maybe we're tracking whether a customer
|
|
592
|
+
has used a particular feature or not. When they use it, we may just want to
|
|
593
|
+
blindly record it:
|
|
594
|
+
```javascript <!-- embed:./test/unit-test-doc.js:scope:testBlindWritesCreateOrUpdate -->
|
|
595
|
+
async testBlindWritesCreateOrUpdate () {
|
|
596
|
+
class LastUsedFeature extends db.Model {
|
|
597
|
+
static KEY = {
|
|
598
|
+
user: S.str,
|
|
599
|
+
feature: S.str
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
static FIELDS = { epoch: S.int }
|
|
603
|
+
}
|
|
604
|
+
await db.Context.run(async tx => {
|
|
605
|
+
// Overwrite the doc regardless of the content
|
|
606
|
+
const ret = tx.createOrOverwrite(LastUsedFeature,
|
|
607
|
+
{ user: 'Bob', feature: 'refer a friend', epoch: 234 })
|
|
608
|
+
expect(ret).not.toBe(undefined) // should return a modal, like create()
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
await db.Context.run(tx => {
|
|
612
|
+
tx.createOrOverwrite(LastUsedFeature,
|
|
613
|
+
// this contains the new value(s) and the doc's key; if a value is
|
|
614
|
+
// undefined then the field will be deleted (it must be optional for
|
|
615
|
+
// this to be allowed)
|
|
616
|
+
{ user: 'Bob', feature: 'refer a friend', epoch: 123 },
|
|
617
|
+
// these are the current values we expect; this call fails if the data
|
|
618
|
+
// exists AND it doesn't match these values
|
|
619
|
+
{ epoch: 234 }
|
|
620
|
+
)
|
|
621
|
+
})
|
|
622
|
+
await db.Context.run(async tx => {
|
|
623
|
+
const doc = await tx.get(LastUsedFeature,
|
|
624
|
+
{ user: 'Bob', feature: 'refer a friend' })
|
|
625
|
+
expect(doc.epoch).toBe(123)
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Both of these methods are synchronous, local methods like `tx.create()`. They
|
|
631
|
+
return immediately and do not perform any network traffic. All network traffic
|
|
632
|
+
related to these are generated as part of any writes processed when the
|
|
633
|
+
transaction commits.
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
### incrementBy()
|
|
637
|
+
To achieve higher write throughput and reduce contention, you can use
|
|
638
|
+
`incrementBy()` to mutate numeric fields. This can be used when you want to
|
|
639
|
+
increment (or decrement) a number's value but don't care about its old value:
|
|
640
|
+
```javascript
|
|
641
|
+
class WebsiteHitCounter extends db.Model {
|
|
642
|
+
static FIELDS = { count: S.int.min(0) }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function slowlyIncrement(id) {
|
|
646
|
+
const counter = await tx.get(WebsiteHitCounter, id)
|
|
647
|
+
// here we read and write the data, so the library will generate an
|
|
648
|
+
// update like "if count was N then set count to N + 1"
|
|
649
|
+
counter.count += 1
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function quicklyIncrement(id) {
|
|
653
|
+
const counter = await tx.get(WebsiteHitCounter, id)
|
|
654
|
+
// since we only increment the number and never read it, the library will
|
|
655
|
+
// generate an update like "increment quantity by 1" which will succeed no
|
|
656
|
+
// matter what the original value was
|
|
657
|
+
counter.getField('count').incrementBy(1)
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Using the `incrementBy()` only helps if you're not going to read the field
|
|
662
|
+
being incremented (though it never hurts to use it):
|
|
663
|
+
```javascript
|
|
664
|
+
async function bothAreJustAsFast(id) {
|
|
665
|
+
const counter = await tx.get(WebsiteHitCounter, id)
|
|
666
|
+
if (counter.count < 100) { // stop counting after reaching 100
|
|
667
|
+
// this is preferred here b/c it is simpler and just as fast in this case
|
|
668
|
+
// counter.count += 1
|
|
669
|
+
|
|
670
|
+
// isn't any faster because we have to generate the condition
|
|
671
|
+
// expression due to the above if condition which read the count var
|
|
672
|
+
counter.getField('count').incrementBy(1)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Using `incrementBy()` on a field whose value is `undefined` is invalid and will
|
|
678
|
+
throw an exception.
|
|
679
|
+
|
|
680
|
+
### Locking
|
|
681
|
+
|
|
682
|
+
By default, Firestore uses [pessimistic concurrency](https://cloud.google.com/datastore/docs/concepts/transactions#concurrency_modes). Review their documentation to decide which works best for your use case. Unit tests and the
|
|
683
|
+
local emulator use the default pessimistic concurrency.
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# Niche Concepts
|
|
687
|
+
|
|
688
|
+
## Key Encoding
|
|
689
|
+
Under the hood, a database key can only be a single attribute. We always store
|
|
690
|
+
that attribute as a string because Firestore only supports string keys (not integer keys, etc.). We compute this string's value by first sorting the
|
|
691
|
+
names of the components of the key. Then we compute the string representation
|
|
692
|
+
of each component's value (with `JSON.stringify()`, except for string values
|
|
693
|
+
which don't need to be encoded like that). Finally, we concatenate these values
|
|
694
|
+
(in order of their keys) and separate them with null characters. An encoded key
|
|
695
|
+
would look like this:
|
|
696
|
+
```javascript
|
|
697
|
+
const doc = tx.create(RaceResult, { raceID: 123, runnerName: 'Joe' })
|
|
698
|
+
expect(doc._id).toBe('123\0Joe')
|
|
699
|
+
|
|
700
|
+
// the encoded key is also contained in the output of Model.key():
|
|
701
|
+
const key = RaceResult.key({ runnerName: 'Mel', raceID: 123 })
|
|
702
|
+
expect(key.Cls).toBe(RaceResult)
|
|
703
|
+
expect(key.encodedKey).toBe('123\0Mel')
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
For this reason, string values cannot contain the null character. If you need
|
|
707
|
+
to store a string with this value, your best option is to probably nest it
|
|
708
|
+
inside of an object:
|
|
709
|
+
```javascript
|
|
710
|
+
class StringKeyWithNullBytes extends db.Model {
|
|
711
|
+
static KEY = { id: S.obj().prop('raw', S.str) }
|
|
712
|
+
}
|
|
713
|
+
tx.create(StringKeyWithNullBytes, {
|
|
714
|
+
id: {
|
|
715
|
+
raw: 'I can contain \0, no pr\0bl\0em!'
|
|
716
|
+
}
|
|
717
|
+
})
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## Nested Transactions are NOT Nested
|
|
721
|
+
Nested transactions like this should be avoided:
|
|
722
|
+
```javascript
|
|
723
|
+
await Context.run(async outerTx => {
|
|
724
|
+
// ...
|
|
725
|
+
await Context.run(async innerTx => {
|
|
726
|
+
// ...
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
The inner transaction, if it commits, will commit first. If the outer
|
|
731
|
+
transaction is retried, the inner transaction _will be run additional times_.
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
## Collection Creation & Persistence
|
|
735
|
+
On localhost, the data persists until you shut down the service. If you add new
|
|
736
|
+
models or change a model (particularly its key structure), you will need to
|
|
737
|
+
restart your Firestore emulator to clear out the old data.
|
|
738
|
+
|
|
739
|
+
Along the same lines, keep in mind that the localhost database is _not_ cleared
|
|
740
|
+
in between test runs. Any data added to the localhost database will remain
|
|
741
|
+
until the emulator is restarted. This can help you debug issues, but it also
|
|
742
|
+
means you should not create docs with a fixed ID as part of a unit test (use
|
|
743
|
+
`uuidv4()` to get a random ID value so it won't clash with a future run of the
|
|
744
|
+
unit tests.)
|
|
745
|
+
|
|
746
|
+
Be careful about changing your models: remember that changing the model does
|
|
747
|
+
_not_ change anything in the database. Be especially wary about changing the
|
|
748
|
+
key structure — it will probably cause serious problems.
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
## Repeated Reads
|
|
752
|
+
By default, reading a doc twice in a single transaction is treated as an
|
|
753
|
+
exception.
|
|
754
|
+
```javascript
|
|
755
|
+
await db.Context.run(async tx => {
|
|
756
|
+
await tx.get(SomeModel, "model id")
|
|
757
|
+
// await tx.get(SomeModel, "model id") // throws exception
|
|
758
|
+
})
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
In some occasions, we may need to allow the same doc to be read more than
|
|
762
|
+
once. For example, a transaction may be handling a batch of operations (action
|
|
763
|
+
pattern with batching), where individual operation might read and update the
|
|
764
|
+
same doc.
|
|
765
|
+
```javascript
|
|
766
|
+
const operation = async (tx) => {
|
|
767
|
+
const model = await tx.get(SomeModel, "some id")
|
|
768
|
+
model.intField += 1
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const operations = [operation, operation]
|
|
772
|
+
|
|
773
|
+
await db.Context.run(async tx => {
|
|
774
|
+
for (const op of operations) {
|
|
775
|
+
// Second iteration will throw
|
|
776
|
+
await op(tx)
|
|
777
|
+
}
|
|
778
|
+
})
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
To allow reading the same doc more than once, a `cacheModels` option can be
|
|
782
|
+
toggled on. In this mode, when a doc is first read, it is cached by the
|
|
783
|
+
transaction, and the transaction will return the cached model for any
|
|
784
|
+
subsequent reads.
|
|
785
|
+
```javascript
|
|
786
|
+
await db.Context.run({ cacheModels: true },async tx => {
|
|
787
|
+
// This transaction will complete ok
|
|
788
|
+
for (const op of operations) {
|
|
789
|
+
await op(tx)
|
|
790
|
+
}
|
|
791
|
+
})
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
Any modifications made to the cached doc will be stored along with the doc,
|
|
795
|
+
so subsequent reads will see the previous updates.
|
|
796
|
+
```javascript
|
|
797
|
+
await db.Context.run({ cacheModels: true },async tx => {
|
|
798
|
+
const model = await tx.get(SomeModel, "some id")
|
|
799
|
+
model.intField = 123
|
|
800
|
+
|
|
801
|
+
const cachedModel = await tx.get(SomeModel, "some id")
|
|
802
|
+
expect(cachedModel.intField).toBe(123)
|
|
803
|
+
})
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
Repeated reads can be enabled during a transaction because transactions track
|
|
807
|
+
all referenced docs. Call `enableModelCache` to turn it on.
|
|
808
|
+
```javascript
|
|
809
|
+
await db.Context.run(async tx => {
|
|
810
|
+
...
|
|
811
|
+
tx.enableModelCache()
|
|
812
|
+
...
|
|
813
|
+
})
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
If [an operation other than read](#operations) was done on the doc (e.g.
|
|
817
|
+
delete, or create, etc.), a subsequent attempt to read the doc will result in
|
|
818
|
+
an exception regardless of the cacheModels flag value.
|
|
819
|
+
|
|
820
|
+
## Key Collection
|
|
821
|
+
When duplicated keys are passed to `tx.get()`, an error will result, even if
|
|
822
|
+
[model cache](#repeated-reads) is enabled, because it is more likely to be a
|
|
823
|
+
coding error in common use cases. Keys must be de-duplicated by removing
|
|
824
|
+
repeated class and key combinations. The `db.UniqueKeyList` class
|
|
825
|
+
provides an `Array` like interface to simplify the deduplication process.
|
|
826
|
+
```javascript
|
|
827
|
+
const keys = new db.UniqueKeyList(MyModel.key('123'))
|
|
828
|
+
keys.push(MyModel.key('123'), ...[MyModel.key('123')])
|
|
829
|
+
const docs = await tx.get(keys)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
# Library Collaborator's Guide
|
|
833
|
+
|
|
834
|
+
## Transactions
|
|
835
|
+
Our `Context` class:
|
|
836
|
+
|
|
837
|
+
* Models read are tracked by the transaction context.
|
|
838
|
+
* Models mutated are written to DB on commit
|
|
839
|
+
* Context commits when the transaction context / scope is exited
|
|
840
|
+
* If a `retryable` error is thrown during transactWrite operation,
|
|
841
|
+
the transaction will be retried.
|
|
842
|
+
* If all retries fail, a `TransactionFailedError` will be thrown.
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
# Appendix
|
|
846
|
+
The samples in this readme can be found in the APIs defined for unit testing
|
|
847
|
+
this library in `test/unit-test-doc.js` in the
|
|
848
|
+
`DBReadmeTest` class.
|
|
849
|
+
|
|
850
|
+
## Useful Links
|
|
851
|
+
* Firestore NodeJS Reference Docs
|
|
852
|
+
* https://cloud.google.com/nodejs/docs/reference/firestore/latest/overview
|
|
853
|
+
* https://googleapis.dev/nodejs/firestore/latest/index.html
|
|
854
|
+
* Firestore [how-to guides](https://cloud.google.com/firestore/docs/how-to) -
|
|
855
|
+
these are very partial and lack many details, but provide a good overview
|
|
856
|
+
|
|
857
|
+
## Not Yet Implemented
|
|
858
|
+
* Preconditions for updating, etc.
|
|
859
|
+
* Indexes, filtering and scans
|
|
860
|
+
* Nested collections
|