datastore-api 6.1.0 → 6.2.1
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/README.md +2 -1
- package/dist/datastore-api.cjs.development.js +728 -496
- package/dist/datastore-api.cjs.development.js.map +1 -1
- package/dist/datastore-api.cjs.production.min.js +1 -1
- package/dist/datastore-api.cjs.production.min.js.map +1 -1
- package/dist/datastore-api.esm.js +729 -497
- package/dist/datastore-api.esm.js.map +1 -1
- package/dist/lib/assert.d.ts +1 -1
- package/dist/lib/dstore-api.d.ts +49 -2
- package/package.json +100 -102
- package/src/lib/assert.ts +1 -1
- package/src/lib/dstore-api-cloud.spec.ts +123 -123
- package/src/lib/dstore-api.ts +299 -193
package/src/lib/dstore-api.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* dstore.ts - Datastore Compatibility layer
|
|
3
|
-
* Try to get a smoother
|
|
3
|
+
* Try to get a smoother API for transactions and such.
|
|
4
4
|
* A little bit inspired by the Python2 ndb interface.
|
|
5
5
|
* But without the ORM bits.
|
|
6
6
|
*
|
|
7
7
|
* In future https://github.com/graphql/dataloader might be used for batching.
|
|
8
8
|
*
|
|
9
9
|
* Created by Dr. Maximillian Dornseif 2021-12-05 in huwawi3backend 11.10.0
|
|
10
|
-
* Copyright (c) 2021, 2022, 2023 Dr. Maximillian Dornseif
|
|
10
|
+
* Copyright (c) 2021, 2022, 2023, 2025 Dr. Maximillian Dornseif
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { AsyncLocalStorage } from 'async_hooks'
|
|
14
|
-
import { setImmediate } from 'timers/promises'
|
|
13
|
+
import { AsyncLocalStorage } from 'async_hooks'
|
|
14
|
+
import { setImmediate } from 'timers/promises'
|
|
15
15
|
|
|
16
|
-
import { Datastore, Key, PathType, Query, Transaction, PropertyFilter } from '@google-cloud/datastore'
|
|
17
|
-
import { Entity, entity } from '@google-cloud/datastore/build/src/entity'
|
|
18
|
-
import { Operator, RunQueryInfo, RunQueryResponse } from '@google-cloud/datastore/build/src/query'
|
|
19
|
-
import { CommitResponse } from '@google-cloud/datastore/build/src/request'
|
|
16
|
+
import { Datastore, Key, PathType, Query, Transaction, PropertyFilter } from '@google-cloud/datastore'
|
|
17
|
+
import { Entity, entity } from '@google-cloud/datastore/build/src/entity'
|
|
18
|
+
import { Operator, RunQueryInfo, RunQueryResponse } from '@google-cloud/datastore/build/src/query'
|
|
19
|
+
import { CommitResponse } from '@google-cloud/datastore/build/src/request'
|
|
20
20
|
import {
|
|
21
21
|
assert,
|
|
22
22
|
assertIsArray,
|
|
@@ -24,19 +24,20 @@ import {
|
|
|
24
24
|
assertIsNumber,
|
|
25
25
|
assertIsObject,
|
|
26
26
|
assertIsString,
|
|
27
|
-
} from 'assertate-debug'
|
|
28
|
-
import Debug from 'debug'
|
|
29
|
-
import promClient from 'prom-client'
|
|
30
|
-
import { Writable } from 'ts-essentials'
|
|
27
|
+
} from 'assertate-debug'
|
|
28
|
+
import Debug from 'debug'
|
|
29
|
+
import promClient from 'prom-client'
|
|
30
|
+
import { Writable } from 'ts-essentials'
|
|
31
|
+
import { assertIsKey } from './assert'
|
|
31
32
|
|
|
32
33
|
/** @ignore */
|
|
33
|
-
export { Datastore, Key, PathType, Query, Transaction } from '@google-cloud/datastore'
|
|
34
|
+
export { Datastore, Key, PathType, Query, Transaction } from '@google-cloud/datastore'
|
|
34
35
|
|
|
35
36
|
/** @ignore */
|
|
36
|
-
const debug = Debug('ds:api')
|
|
37
|
+
const debug = Debug('ds:api')
|
|
37
38
|
|
|
38
39
|
/** @ignore */
|
|
39
|
-
const transactionAsyncLocalStorage = new AsyncLocalStorage()
|
|
40
|
+
const transactionAsyncLocalStorage = new AsyncLocalStorage()
|
|
40
41
|
|
|
41
42
|
// for HMR
|
|
42
43
|
promClient.register.removeSingleMetric('dstore_requests_seconds')
|
|
@@ -46,25 +47,25 @@ const metricHistogram = new promClient.Histogram({
|
|
|
46
47
|
name: 'dstore_requests_seconds',
|
|
47
48
|
help: 'How long did Datastore operations take?',
|
|
48
49
|
labelNames: ['operation'],
|
|
49
|
-
})
|
|
50
|
+
})
|
|
50
51
|
const metricFailureCounter = new promClient.Counter({
|
|
51
52
|
name: 'dstore_failures_total',
|
|
52
53
|
help: 'How many Datastore operations failed?',
|
|
53
54
|
labelNames: ['operation'],
|
|
54
|
-
})
|
|
55
|
+
})
|
|
55
56
|
|
|
56
57
|
/** Use instead of Datastore.KEY
|
|
57
58
|
*
|
|
58
59
|
* Even better: use `_key` instead.
|
|
59
60
|
*/
|
|
60
|
-
export const KEYSYM = Datastore.KEY
|
|
61
|
+
export const KEYSYM = Datastore.KEY
|
|
61
62
|
|
|
62
|
-
export type IGqlFilterTypes = boolean | string | number
|
|
63
|
+
export type IGqlFilterTypes = boolean | string | number
|
|
63
64
|
|
|
64
65
|
export type IGqlFilterSpec = {
|
|
65
|
-
readonly eq: IGqlFilterTypes
|
|
66
|
-
}
|
|
67
|
-
export type TGqlFilterList = Array<[string, Operator, DstorePropertyValues]
|
|
66
|
+
readonly eq: IGqlFilterTypes
|
|
67
|
+
}
|
|
68
|
+
export type TGqlFilterList = Array<[string, Operator, DstorePropertyValues]>
|
|
68
69
|
|
|
69
70
|
/** Define what can be written into the Datastore */
|
|
70
71
|
export type DstorePropertyValues =
|
|
@@ -77,11 +78,11 @@ export type DstorePropertyValues =
|
|
|
77
78
|
| Buffer
|
|
78
79
|
| Key
|
|
79
80
|
| DstorePropertyValues[]
|
|
80
|
-
| { [key: string]: DstorePropertyValues }
|
|
81
|
+
| { [key: string]: DstorePropertyValues }
|
|
81
82
|
|
|
82
83
|
export interface IDstoreEntryWithoutKey {
|
|
83
84
|
/** All User Data stored in the Datastore */
|
|
84
|
-
[key: string]: DstorePropertyValues
|
|
85
|
+
[key: string]: DstorePropertyValues
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/** Represents what is actually stored inside the Datastore, called "Entity" by Google
|
|
@@ -89,39 +90,56 @@ export interface IDstoreEntryWithoutKey {
|
|
|
89
90
|
*/
|
|
90
91
|
export interface IDstoreEntry extends IDstoreEntryWithoutKey {
|
|
91
92
|
/* Datastore Key provided by [@google-cloud/datastore](https://github.com/googleapis/nodejs-datastore#readme) */
|
|
92
|
-
readonly [Datastore.KEY]?: Key
|
|
93
|
+
readonly [Datastore.KEY]?: Key
|
|
94
|
+
/** [Datastore.KEY] key */
|
|
95
|
+
_keyStr: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Represents what is actually stored inside the Datastore, called "Entity" by Google
|
|
99
|
+
*/
|
|
100
|
+
export interface IDstoreEntryWithKey extends IDstoreEntry {
|
|
101
|
+
/* Datastore Key provided by [@google-cloud/datastore](https://github.com/googleapis/nodejs-datastore#readme) */
|
|
102
|
+
readonly [Datastore.KEY]: Key
|
|
93
103
|
/** [Datastore.KEY] key */
|
|
94
|
-
_keyStr: string
|
|
104
|
+
_keyStr: string
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
/** Represents the thing you pass to the save method. Also called "Entity" by Google */
|
|
98
108
|
export type DstoreSaveEntity = {
|
|
99
|
-
key: Key
|
|
109
|
+
key: Key
|
|
100
110
|
data: Omit<IDstoreEntry, '_keyStr' | Datastore['KEY']> &
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
method?: 'insert' | 'update' | 'upsert'
|
|
106
|
-
excludeLargeProperties?: boolean
|
|
107
|
-
excludeFromIndexes?: readonly string[]
|
|
108
|
-
}
|
|
111
|
+
Partial<{
|
|
112
|
+
_keyStr: string | undefined;
|
|
113
|
+
[Datastore.KEY]: Key
|
|
114
|
+
}>
|
|
115
|
+
method?: 'insert' | 'update' | 'upsert'
|
|
116
|
+
excludeLargeProperties?: boolean
|
|
117
|
+
excludeFromIndexes?: readonly string[]
|
|
118
|
+
}
|
|
109
119
|
|
|
120
|
+
export interface IIterateParams {
|
|
121
|
+
kindName: string,
|
|
122
|
+
filters?: TGqlFilterList,
|
|
123
|
+
limit?: number,
|
|
124
|
+
ordering?: readonly string[],
|
|
125
|
+
selection?: readonly string[],
|
|
126
|
+
}
|
|
110
127
|
type IDstore = {
|
|
111
128
|
/** Accessible by Users of the library. Keep in mind that you will access outside transactions created by [[runInTransaction]]. */
|
|
112
|
-
readonly datastore: Datastore
|
|
113
|
-
key: (path: ReadonlyArray<PathType>) => Key
|
|
114
|
-
keyFromSerialized: (text: string) => Key
|
|
115
|
-
keySerialize: (key: Key) => string
|
|
116
|
-
readKey: (entry: IDstoreEntry) => Key
|
|
117
|
-
get: (key: Key) => Promise<IDstoreEntry | null
|
|
118
|
-
getMulti: (keys: ReadonlyArray<Key>) => Promise<ReadonlyArray<IDstoreEntry | null
|
|
119
|
-
set: (key: Key, entry: IDstoreEntry) => Promise<Key
|
|
120
|
-
save: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined
|
|
121
|
-
insert: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined
|
|
122
|
-
update: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined
|
|
123
|
-
delete: (keys: readonly Key[]) => Promise<CommitResponse | undefined
|
|
124
|
-
createQuery: (kind: string) => Query
|
|
129
|
+
readonly datastore: Datastore
|
|
130
|
+
key: (path: ReadonlyArray<PathType>) => Key
|
|
131
|
+
keyFromSerialized: (text: string) => Key
|
|
132
|
+
keySerialize: (key: Key) => string
|
|
133
|
+
readKey: (entry: IDstoreEntry) => Key
|
|
134
|
+
get: (key: Key) => Promise<IDstoreEntry | null>
|
|
135
|
+
getMulti: (keys: ReadonlyArray<Key>) => Promise<ReadonlyArray<IDstoreEntry | null>>
|
|
136
|
+
set: (key: Key, entry: IDstoreEntry) => Promise<Key>
|
|
137
|
+
save: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined>
|
|
138
|
+
insert: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined>
|
|
139
|
+
update: (entities: readonly DstoreSaveEntity[]) => Promise<CommitResponse | undefined>
|
|
140
|
+
delete: (keys: readonly Key[]) => Promise<CommitResponse | undefined>
|
|
141
|
+
createQuery: (kind: string) => Query
|
|
142
|
+
runQuery: (query: Query | Omit<Query, 'run'>) => Promise<RunQueryResponse>
|
|
125
143
|
query: (
|
|
126
144
|
kind: string,
|
|
127
145
|
filters?: TGqlFilterList,
|
|
@@ -129,11 +147,11 @@ type IDstore = {
|
|
|
129
147
|
ordering?: readonly string[],
|
|
130
148
|
selection?: readonly string[],
|
|
131
149
|
cursor?: string
|
|
132
|
-
) => Promise<RunQueryResponse
|
|
133
|
-
|
|
134
|
-
allocateOneId: (kindName: string) => Promise<string
|
|
135
|
-
runInTransaction: <T>(func: { (): Promise<T>; (): T }) => Promise<T
|
|
136
|
-
}
|
|
150
|
+
) => Promise<RunQueryResponse>
|
|
151
|
+
iterate: (options: IIterateParams) => AsyncIterable<IDstoreEntry>
|
|
152
|
+
allocateOneId: (kindName: string) => Promise<string>
|
|
153
|
+
runInTransaction: <T>(func: { (): Promise<T>; (): T }) => Promise<T>
|
|
154
|
+
}
|
|
137
155
|
|
|
138
156
|
/** Dstore implements a slightly more accessible version of the [Google Cloud Datastore: Node.js Client](https://cloud.google.com/nodejs/docs/reference/datastore/latest)
|
|
139
157
|
|
|
@@ -179,13 +197,13 @@ export class Dstore implements IDstore {
|
|
|
179
197
|
```
|
|
180
198
|
*/
|
|
181
199
|
constructor(readonly datastore: Datastore, readonly projectId?: string, readonly logger?: string) {
|
|
182
|
-
assertIsObject(datastore)
|
|
183
|
-
this.engines.push(this.engine)
|
|
200
|
+
assertIsObject(datastore)
|
|
201
|
+
this.engines.push(this.engine)
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
/** Gets the Datastore or the current Transaction. */
|
|
187
205
|
private getDoT(): Transaction | Datastore {
|
|
188
|
-
return (transactionAsyncLocalStorage.getStore() as Transaction) || this.datastore
|
|
206
|
+
return (transactionAsyncLocalStorage.getStore() as Transaction) || this.datastore
|
|
189
207
|
}
|
|
190
208
|
|
|
191
209
|
/** `key()` creates a [[Key]] Object from a path.
|
|
@@ -197,7 +215,7 @@ export class Dstore implements IDstore {
|
|
|
197
215
|
* @category Datastore Drop-In
|
|
198
216
|
*/
|
|
199
217
|
key(path: readonly PathType[]): Key {
|
|
200
|
-
return this.datastore.key(path as Writable<typeof path>)
|
|
218
|
+
return this.datastore.key(path as Writable<typeof path>)
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
/** `keyFromSerialized()` serializes [[Key]] to a string.
|
|
@@ -209,7 +227,7 @@ export class Dstore implements IDstore {
|
|
|
209
227
|
* @category Datastore Drop-In
|
|
210
228
|
*/
|
|
211
229
|
keySerialize(key: Key): string {
|
|
212
|
-
return key ? this.urlSaveKey.legacyEncode(this.projectId ?? '', key) : ''
|
|
230
|
+
return key ? this.urlSaveKey.legacyEncode(this.projectId ?? '', key) : ''
|
|
213
231
|
}
|
|
214
232
|
|
|
215
233
|
/** `keyFromSerialized()` deserializes a string created with [[keySerialize]] to a [[Key]].
|
|
@@ -219,7 +237,7 @@ export class Dstore implements IDstore {
|
|
|
219
237
|
* @category Datastore Drop-In
|
|
220
238
|
*/
|
|
221
239
|
keyFromSerialized(text: string): Key {
|
|
222
|
-
return this.urlSaveKey.legacyDecode(text)
|
|
240
|
+
return this.urlSaveKey.legacyDecode(text)
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
/** `readKey()` extracts the [[Key]] from an [[IDstoreEntry]].
|
|
@@ -230,20 +248,20 @@ export class Dstore implements IDstore {
|
|
|
230
248
|
* @category Additional
|
|
231
249
|
*/
|
|
232
250
|
readKey(ent: IDstoreEntry): Key {
|
|
233
|
-
assertIsObject(ent)
|
|
234
|
-
let ret = ent[Datastore.KEY]
|
|
251
|
+
assertIsObject(ent)
|
|
252
|
+
let ret = ent[Datastore.KEY]
|
|
235
253
|
if (ent._keyStr && !ret) {
|
|
236
|
-
ret = this.keyFromSerialized(ent._keyStr)
|
|
254
|
+
ret = this.keyFromSerialized(ent._keyStr)
|
|
237
255
|
}
|
|
238
256
|
assertIsObject(
|
|
239
257
|
ret,
|
|
240
258
|
'entity[Datastore.KEY]/entity._keyStr',
|
|
241
259
|
`Entity is missing the datastore Key: ${JSON.stringify(ent)}`
|
|
242
|
-
)
|
|
243
|
-
return ret
|
|
260
|
+
)
|
|
261
|
+
return ret
|
|
244
262
|
}
|
|
245
263
|
|
|
246
|
-
/** `fixKeys()` is called for all [[IDstoreEntry]]
|
|
264
|
+
/** `fixKeys()` is called for all [[IDstoreEntry]] returned from [[Dstore]].
|
|
247
265
|
*
|
|
248
266
|
* Is ensures that besides `entity[Datastore.KEY]` there is `_keyStr` to be leveraged by [[readKey]].
|
|
249
267
|
*
|
|
@@ -254,13 +272,36 @@ export class Dstore implements IDstore {
|
|
|
254
272
|
): Array<IDstoreEntry | undefined> {
|
|
255
273
|
entities.forEach((x) => {
|
|
256
274
|
if (!!x?.[Datastore.KEY] && x[Datastore.KEY]) {
|
|
257
|
-
assertIsDefined(x[Datastore.KEY])
|
|
258
|
-
assertIsObject(x[Datastore.KEY])
|
|
275
|
+
assertIsDefined(x[Datastore.KEY])
|
|
276
|
+
assertIsObject(x[Datastore.KEY])
|
|
259
277
|
// Old TypesScript has problems with symbols as a property
|
|
260
|
-
x._keyStr = this.keySerialize(x[Datastore.KEY] as Key)
|
|
278
|
+
x._keyStr = this.keySerialize(x[Datastore.KEY] as Key)
|
|
261
279
|
}
|
|
262
|
-
})
|
|
263
|
-
return entities as Array<IDstoreEntry | undefined
|
|
280
|
+
})
|
|
281
|
+
return entities as Array<IDstoreEntry | undefined>
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** this is for save, insert, update and upsert and ensures _kkeyStr() handling.
|
|
285
|
+
*
|
|
286
|
+
*/
|
|
287
|
+
private prepareEntitiesForDatastore(entities: readonly DstoreSaveEntity[]) {
|
|
288
|
+
for (const e of entities) {
|
|
289
|
+
assertIsObject(e.key)
|
|
290
|
+
assertIsObject(e.data)
|
|
291
|
+
this.fixKeys([e.data])
|
|
292
|
+
e.excludeLargeProperties = e.excludeLargeProperties === undefined ? true : e.excludeLargeProperties
|
|
293
|
+
e.data = { ...e.data, _keyStr: undefined }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** this is for save, insert, update and upsert and ensures _kkeyStr() handling.
|
|
298
|
+
*
|
|
299
|
+
*/
|
|
300
|
+
private prepareEntitiesFromDatastore(entities: readonly DstoreSaveEntity[] & unknown[]) {
|
|
301
|
+
for (const e of entities) {
|
|
302
|
+
e.data[Datastore.KEY] = e.key
|
|
303
|
+
this.fixKeys([e.data])
|
|
304
|
+
}
|
|
264
305
|
}
|
|
265
306
|
|
|
266
307
|
/** `get()` reads a [[IDstoreEntry]] from the Datastore.
|
|
@@ -279,11 +320,11 @@ export class Dstore implements IDstore {
|
|
|
279
320
|
* @category Datastore Drop-In
|
|
280
321
|
*/
|
|
281
322
|
async get(key: Key): Promise<IDstoreEntry | null> {
|
|
282
|
-
assertIsObject(key)
|
|
283
|
-
assert(!Array.isArray(key))
|
|
284
|
-
assert(key.path.length % 2 == 0, `key.path must be complete: ${JSON.stringify(key.path)}`)
|
|
285
|
-
const result = await this.getMulti([key])
|
|
286
|
-
return result?.[0] || null
|
|
323
|
+
assertIsObject(key)
|
|
324
|
+
assert(!Array.isArray(key))
|
|
325
|
+
assert(key.path.length % 2 == 0, `key.path must be complete: ${JSON.stringify(key.path)}`)
|
|
326
|
+
const result = await this.getMulti([key])
|
|
327
|
+
return result?.[0] || null
|
|
287
328
|
}
|
|
288
329
|
|
|
289
330
|
/** `getMulti()` reads several [[IDstoreEntry]]s from the Datastore.
|
|
@@ -305,28 +346,28 @@ export class Dstore implements IDstore {
|
|
|
305
346
|
*/
|
|
306
347
|
async getMulti(keys: readonly Key[]): Promise<Array<IDstoreEntry | null>> {
|
|
307
348
|
// assertIsArray(keys);
|
|
308
|
-
let results: (IDstoreEntry | null | undefined)[]
|
|
309
|
-
const metricEnd = metricHistogram.startTimer()
|
|
349
|
+
let results: (IDstoreEntry | null | undefined)[]
|
|
350
|
+
const metricEnd = metricHistogram.startTimer()
|
|
310
351
|
try {
|
|
311
352
|
results = this.fixKeys(
|
|
312
353
|
keys.length > 0 ? (await this.getDoT().get(keys as Writable<typeof keys>))?.[0] : []
|
|
313
|
-
)
|
|
354
|
+
)
|
|
314
355
|
} catch (error) {
|
|
315
|
-
metricFailureCounter.inc({ operation: 'get' })
|
|
316
|
-
await setImmediate()
|
|
317
|
-
throw new DstoreError('datastore.getMulti error', error as Error, { keys })
|
|
356
|
+
metricFailureCounter.inc({ operation: 'get' })
|
|
357
|
+
await setImmediate()
|
|
358
|
+
throw new DstoreError('datastore.getMulti error', error as Error, { keys })
|
|
318
359
|
} finally {
|
|
319
|
-
metricEnd({ operation: 'get' })
|
|
360
|
+
metricEnd({ operation: 'get' })
|
|
320
361
|
}
|
|
321
362
|
|
|
322
363
|
// Sort resulting entities by the keys they were requested with.
|
|
323
|
-
assertIsArray(results)
|
|
324
|
-
const entities = results as IDstoreEntry[]
|
|
325
|
-
const entitiesByKey: Record<string, IDstoreEntry> = {}
|
|
364
|
+
assertIsArray(results)
|
|
365
|
+
const entities = results as IDstoreEntry[]
|
|
366
|
+
const entitiesByKey: Record<string, IDstoreEntry> = {}
|
|
326
367
|
entities.forEach((entity) => {
|
|
327
|
-
entitiesByKey[JSON.stringify(entity[Datastore.KEY])] = entity
|
|
328
|
-
})
|
|
329
|
-
return keys.map((key) => entitiesByKey[JSON.stringify(key)] || null)
|
|
368
|
+
entitiesByKey[JSON.stringify(entity[Datastore.KEY])] = entity
|
|
369
|
+
})
|
|
370
|
+
return keys.map((key) => entitiesByKey[JSON.stringify(key)] || null)
|
|
330
371
|
}
|
|
331
372
|
|
|
332
373
|
/** `set()` is addition to [[Datastore]]. It provides a classic Key-value Interface.
|
|
@@ -346,11 +387,11 @@ export class Dstore implements IDstore {
|
|
|
346
387
|
* @category Additional
|
|
347
388
|
*/
|
|
348
389
|
async set(key: Key, data: IDstoreEntryWithoutKey): Promise<Key> {
|
|
349
|
-
assertIsObject(key)
|
|
350
|
-
assertIsObject(data)
|
|
351
|
-
const saveEntity = { key, data: { ...data, _keyStr: undefined } }
|
|
352
|
-
await this.save([saveEntity])
|
|
353
|
-
return saveEntity.key
|
|
390
|
+
assertIsObject(key)
|
|
391
|
+
assertIsObject(data)
|
|
392
|
+
const saveEntity = { key, data: { ...data, _keyStr: undefined } }
|
|
393
|
+
await this.save([saveEntity])
|
|
394
|
+
return saveEntity.key
|
|
354
395
|
}
|
|
355
396
|
|
|
356
397
|
/** `save()` is compatible to [Datastore.save()](https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/datastore#_google_cloud_datastore_Datastore_save_member_1_).
|
|
@@ -387,34 +428,27 @@ export class Dstore implements IDstore {
|
|
|
387
428
|
* @category Datastore Drop-In
|
|
388
429
|
*/
|
|
389
430
|
async save(entities: readonly DstoreSaveEntity[]): Promise<CommitResponse | undefined> {
|
|
390
|
-
assertIsArray(entities)
|
|
391
|
-
let ret: CommitResponse | undefined
|
|
392
|
-
const metricEnd = metricHistogram.startTimer()
|
|
431
|
+
assertIsArray(entities)
|
|
432
|
+
let ret: CommitResponse | undefined
|
|
433
|
+
const metricEnd = metricHistogram.startTimer()
|
|
393
434
|
try {
|
|
435
|
+
this.prepareEntitiesForDatastore(entities)
|
|
394
436
|
// Within Transaction we don't get any answer here!
|
|
395
437
|
// [ { mutationResults: [ [Object], [Object] ], indexUpdates: 51 } ]
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
assertIsObject(e.data);
|
|
399
|
-
this.fixKeys([e.data]);
|
|
400
|
-
e.excludeLargeProperties = e.excludeLargeProperties === undefined ? true : e.excludeLargeProperties;
|
|
401
|
-
e.data = { ...e.data, _keyStr: undefined };
|
|
402
|
-
}
|
|
403
|
-
ret = (await this.getDoT().save(entities)) || undefined;
|
|
404
|
-
for (const e of entities) {
|
|
405
|
-
e.data[Datastore.KEY] = e.key;
|
|
406
|
-
this.fixKeys([e.data]);
|
|
407
|
-
}
|
|
438
|
+
ret = (await this.getDoT().save(entities)) || undefined
|
|
439
|
+
this.prepareEntitiesFromDatastore(entities)
|
|
408
440
|
} catch (error) {
|
|
409
|
-
metricFailureCounter.inc({ operation: 'save' })
|
|
410
|
-
await setImmediate()
|
|
411
|
-
throw new DstoreError('datastore.save error', error as Error)
|
|
441
|
+
metricFailureCounter.inc({ operation: 'save' })
|
|
442
|
+
await setImmediate()
|
|
443
|
+
throw new DstoreError('datastore.save error', error as Error)
|
|
412
444
|
} finally {
|
|
413
|
-
metricEnd({ operation: 'save' })
|
|
445
|
+
metricEnd({ operation: 'save' })
|
|
414
446
|
}
|
|
415
|
-
return ret
|
|
447
|
+
return ret
|
|
416
448
|
}
|
|
417
449
|
|
|
450
|
+
|
|
451
|
+
|
|
418
452
|
/** `insert()` is compatible to [Datastore.insert()](https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/datastore#_google_cloud_datastore_Datastore_insert_member_1_).
|
|
419
453
|
*
|
|
420
454
|
* The single Parameter is a list of [[DstoreSaveEntity]]s.
|
|
@@ -434,20 +468,22 @@ export class Dstore implements IDstore {
|
|
|
434
468
|
* @category Datastore Drop-In
|
|
435
469
|
*/
|
|
436
470
|
async insert(entities: readonly DstoreSaveEntity[]): Promise<CommitResponse | undefined> {
|
|
437
|
-
assertIsArray(entities)
|
|
438
|
-
let ret: CommitResponse | undefined
|
|
439
|
-
const metricEnd = metricHistogram.startTimer()
|
|
471
|
+
assertIsArray(entities)
|
|
472
|
+
let ret: CommitResponse | undefined
|
|
473
|
+
const metricEnd = metricHistogram.startTimer()
|
|
440
474
|
try {
|
|
441
|
-
|
|
475
|
+
this.prepareEntitiesForDatastore(entities)
|
|
476
|
+
ret = (await this.getDoT().insert(entities)) || undefined
|
|
477
|
+
this.prepareEntitiesFromDatastore(entities)
|
|
442
478
|
} catch (error) {
|
|
443
479
|
// console.error(error)
|
|
444
|
-
metricFailureCounter.inc({ operation: 'insert' })
|
|
445
|
-
await setImmediate()
|
|
446
|
-
throw new DstoreError('datastore.insert error', error as Error)
|
|
480
|
+
metricFailureCounter.inc({ operation: 'insert' })
|
|
481
|
+
await setImmediate()
|
|
482
|
+
throw new DstoreError('datastore.insert error', error as Error)
|
|
447
483
|
} finally {
|
|
448
|
-
metricEnd({ operation: 'insert' })
|
|
484
|
+
metricEnd({ operation: 'insert' })
|
|
449
485
|
}
|
|
450
|
-
return ret
|
|
486
|
+
return ret
|
|
451
487
|
}
|
|
452
488
|
|
|
453
489
|
/** `update()` is compatible to [Datastore.update()](https://cloud.google.com/nodejs/docs/reference/datastore/latest/datastore/datastore#_google_cloud_datastore_Datastore_update_member_1_).
|
|
@@ -469,29 +505,31 @@ export class Dstore implements IDstore {
|
|
|
469
505
|
* @category Datastore Drop-In
|
|
470
506
|
*/
|
|
471
507
|
async update(entities: readonly DstoreSaveEntity[]): Promise<CommitResponse | undefined> {
|
|
472
|
-
assertIsArray(entities)
|
|
508
|
+
assertIsArray(entities)
|
|
473
509
|
|
|
474
|
-
entities.forEach((entity) => assertIsObject(entity.key))
|
|
510
|
+
entities.forEach((entity) => assertIsObject(entity.key))
|
|
475
511
|
entities.forEach((entity) =>
|
|
476
512
|
assert(
|
|
477
513
|
entity.key.path.length % 2 == 0,
|
|
478
514
|
`entity.key.path must be complete: ${JSON.stringify([entity.key.path, entity])}`
|
|
479
515
|
)
|
|
480
|
-
)
|
|
481
|
-
let ret: CommitResponse | undefined
|
|
482
|
-
const metricEnd = metricHistogram.startTimer()
|
|
516
|
+
)
|
|
517
|
+
let ret: CommitResponse | undefined
|
|
518
|
+
const metricEnd = metricHistogram.startTimer()
|
|
483
519
|
|
|
484
520
|
try {
|
|
485
|
-
|
|
521
|
+
this.prepareEntitiesForDatastore(entities)
|
|
522
|
+
ret = (await this.getDoT().update(entities)) || undefined
|
|
523
|
+
this.prepareEntitiesFromDatastore(entities)
|
|
486
524
|
} catch (error) {
|
|
487
525
|
// console.error(error)
|
|
488
|
-
metricFailureCounter.inc({ operation: 'update' })
|
|
489
|
-
await setImmediate()
|
|
490
|
-
throw new DstoreError('datastore.update error', error as Error)
|
|
526
|
+
metricFailureCounter.inc({ operation: 'update' })
|
|
527
|
+
await setImmediate()
|
|
528
|
+
throw new DstoreError('datastore.update error', error as Error)
|
|
491
529
|
} finally {
|
|
492
|
-
metricEnd({ operation: 'update' })
|
|
530
|
+
metricEnd({ operation: 'update' })
|
|
493
531
|
}
|
|
494
|
-
return ret
|
|
532
|
+
return ret
|
|
495
533
|
}
|
|
496
534
|
|
|
497
535
|
/** `delete()` is compatible to [Datastore.delete()].
|
|
@@ -508,23 +546,23 @@ export class Dstore implements IDstore {
|
|
|
508
546
|
* @category Datastore Drop-In
|
|
509
547
|
*/
|
|
510
548
|
async delete(keys: readonly Key[]): Promise<CommitResponse | undefined> {
|
|
511
|
-
assertIsArray(keys)
|
|
512
|
-
keys.forEach((key) => assertIsObject(key))
|
|
549
|
+
assertIsArray(keys)
|
|
550
|
+
keys.forEach((key) => assertIsObject(key))
|
|
513
551
|
keys.forEach((key) =>
|
|
514
552
|
assert(key.path.length % 2 == 0, `key.path must be complete: ${JSON.stringify(key.path)}`)
|
|
515
|
-
)
|
|
516
|
-
let ret
|
|
517
|
-
const metricEnd = metricHistogram.startTimer()
|
|
553
|
+
)
|
|
554
|
+
let ret
|
|
555
|
+
const metricEnd = metricHistogram.startTimer()
|
|
518
556
|
try {
|
|
519
|
-
ret = (await this.getDoT().delete(keys)) || undefined
|
|
557
|
+
ret = (await this.getDoT().delete(keys)) || undefined
|
|
520
558
|
} catch (error) {
|
|
521
|
-
metricFailureCounter.inc({ operation: 'delete' })
|
|
522
|
-
await setImmediate()
|
|
523
|
-
throw new DstoreError('datastore.delete error', error as Error)
|
|
559
|
+
metricFailureCounter.inc({ operation: 'delete' })
|
|
560
|
+
await setImmediate()
|
|
561
|
+
throw new DstoreError('datastore.delete error', error as Error)
|
|
524
562
|
} finally {
|
|
525
|
-
metricEnd({ operation: 'delete' })
|
|
563
|
+
metricEnd({ operation: 'delete' })
|
|
526
564
|
}
|
|
527
|
-
return ret
|
|
565
|
+
return ret
|
|
528
566
|
}
|
|
529
567
|
|
|
530
568
|
/** `createQuery()` creates an "empty" [[Query]] Object.
|
|
@@ -537,25 +575,25 @@ export class Dstore implements IDstore {
|
|
|
537
575
|
*/
|
|
538
576
|
createQuery(kindName: string): Query {
|
|
539
577
|
try {
|
|
540
|
-
return this.getDoT().createQuery(kindName)
|
|
578
|
+
return this.getDoT().createQuery(kindName)
|
|
541
579
|
} catch (error) {
|
|
542
|
-
throw new DstoreError('datastore.createQuery error', error as Error)
|
|
580
|
+
throw new DstoreError('datastore.createQuery error', error as Error)
|
|
543
581
|
}
|
|
544
582
|
}
|
|
545
583
|
|
|
546
584
|
async runQuery(query: Query | Omit<Query, 'run'>): Promise<RunQueryResponse> {
|
|
547
|
-
let ret
|
|
548
|
-
const metricEnd = metricHistogram.startTimer()
|
|
585
|
+
let ret
|
|
586
|
+
const metricEnd = metricHistogram.startTimer()
|
|
549
587
|
try {
|
|
550
|
-
const [entities, info]: [Entity[], RunQueryInfo] = await this.getDoT().runQuery(query as Query)
|
|
551
|
-
ret = [this.fixKeys(entities), info]
|
|
588
|
+
const [entities, info]: [Entity[], RunQueryInfo] = await this.getDoT().runQuery(query as Query)
|
|
589
|
+
ret = [this.fixKeys(entities), info]
|
|
552
590
|
} catch (error) {
|
|
553
|
-
await setImmediate()
|
|
554
|
-
throw new DstoreError('datastore.runQuery error', error as Error)
|
|
591
|
+
await setImmediate()
|
|
592
|
+
throw new DstoreError('datastore.runQuery error', error as Error)
|
|
555
593
|
} finally {
|
|
556
|
-
metricEnd({ operation: 'query' })
|
|
594
|
+
metricEnd({ operation: 'query' })
|
|
557
595
|
}
|
|
558
|
-
return ret as RunQueryResponse
|
|
596
|
+
return ret as RunQueryResponse
|
|
559
597
|
}
|
|
560
598
|
|
|
561
599
|
/** `query()` combined [[createQuery]] and [[runQuery]] in a single call.
|
|
@@ -568,6 +606,7 @@ export class Dstore implements IDstore {
|
|
|
568
606
|
* @param selection selectionList of [[Query]] select() calls.
|
|
569
607
|
* @param cursor unsupported so far.
|
|
570
608
|
*
|
|
609
|
+
* @throws [[DstoreError]]
|
|
571
610
|
* @category Datastore Drop-In
|
|
572
611
|
*/
|
|
573
612
|
async query(
|
|
@@ -578,34 +617,101 @@ export class Dstore implements IDstore {
|
|
|
578
617
|
selection: readonly string[] = [],
|
|
579
618
|
cursor?: string
|
|
580
619
|
): Promise<RunQueryResponse> {
|
|
581
|
-
assertIsString(kindName)
|
|
582
|
-
assertIsArray(filters)
|
|
583
|
-
assertIsNumber(limit)
|
|
620
|
+
assertIsString(kindName)
|
|
621
|
+
assertIsArray(filters)
|
|
622
|
+
assertIsNumber(limit)
|
|
584
623
|
try {
|
|
585
|
-
const q = this.createQuery(kindName)
|
|
624
|
+
const q = this.createQuery(kindName)
|
|
586
625
|
for (const filterSpec of filters) {
|
|
587
|
-
assertIsObject(filterSpec)
|
|
626
|
+
assertIsObject(filterSpec)
|
|
588
627
|
// @ts-ignore
|
|
589
|
-
q.filter(new PropertyFilter(...filterSpec))
|
|
628
|
+
q.filter(new PropertyFilter(...filterSpec))
|
|
590
629
|
}
|
|
591
630
|
for (const orderField of ordering) {
|
|
592
|
-
q.order(orderField)
|
|
631
|
+
q.order(orderField)
|
|
593
632
|
}
|
|
594
633
|
if (limit > 0) {
|
|
595
|
-
q.limit(limit)
|
|
634
|
+
q.limit(limit)
|
|
596
635
|
}
|
|
597
636
|
if (selection.length > 0) {
|
|
598
|
-
q.select(selection as any)
|
|
637
|
+
q.select(selection as any)
|
|
638
|
+
}
|
|
639
|
+
const ret = await this.getDoT().runQuery(q)
|
|
640
|
+
return [this.fixKeys(ret[0]), ret[1]]
|
|
641
|
+
} catch (error) {
|
|
642
|
+
await setImmediate()
|
|
643
|
+
throw new DstoreError('datastore.query error', error as Error, {
|
|
644
|
+
kindName,
|
|
645
|
+
filters,
|
|
646
|
+
limit,
|
|
647
|
+
ordering,
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
/** `iterate()` is a modernized version of `query()`.
|
|
654
|
+
*
|
|
655
|
+
* It takes a Parameter object and returns an AsyncIterable.
|
|
656
|
+
* Entities returned have been processed by `fixKeys()`.
|
|
657
|
+
*
|
|
658
|
+
* Can be used with `for await` loops like this:
|
|
659
|
+
*
|
|
660
|
+
* ```typescript
|
|
661
|
+
* for await (const entity of dstore.iterate({ kindName: 'p_ReservierungsAbruf', filters: [['verbucht', '=', true]]})) {
|
|
662
|
+
* console.log(entity)
|
|
663
|
+
* }
|
|
664
|
+
* ```
|
|
665
|
+
*
|
|
666
|
+
* @param kindName Name of the [[Datastore]][Kind](https://cloud.google.com/datastore/docs/concepts/entities#kinds_and_identifiers) ("Table") which should be searched.
|
|
667
|
+
* @param filters List of [[Query]] filter() calls.
|
|
668
|
+
* @param limit Maximum Number of Results to return.
|
|
669
|
+
* @param ordering List of [[Query]] order() calls.
|
|
670
|
+
* @param selection selectionList of [[Query]] select() calls.
|
|
671
|
+
*
|
|
672
|
+
* @throws [[DstoreError]]
|
|
673
|
+
* @category Additional
|
|
674
|
+
*/
|
|
675
|
+
async * iterate({
|
|
676
|
+
kindName,
|
|
677
|
+
filters = [],
|
|
678
|
+
limit = 2500,
|
|
679
|
+
ordering = [],
|
|
680
|
+
selection = [],
|
|
681
|
+
}: IIterateParams): AsyncIterable<IDstoreEntry> {
|
|
682
|
+
assertIsString(kindName)
|
|
683
|
+
assertIsArray(filters)
|
|
684
|
+
assertIsNumber(limit)
|
|
685
|
+
try {
|
|
686
|
+
const q = this.getDoT().createQuery(kindName)
|
|
687
|
+
for (const filterSpec of filters) {
|
|
688
|
+
assertIsObject(filterSpec)
|
|
689
|
+
// @ts-ignore
|
|
690
|
+
q.filter(new PropertyFilter(...filterSpec))
|
|
691
|
+
}
|
|
692
|
+
for (const orderField of ordering) {
|
|
693
|
+
q.order(orderField)
|
|
694
|
+
}
|
|
695
|
+
if (limit > 0) {
|
|
696
|
+
q.limit(limit)
|
|
697
|
+
}
|
|
698
|
+
if (selection.length > 0) {
|
|
699
|
+
q.select(selection as any)
|
|
700
|
+
}
|
|
701
|
+
for await (const entity of q.runStream()) {
|
|
702
|
+
const ret = this.fixKeys([entity])[0]
|
|
703
|
+
assertIsDefined(ret, 'datastore.iterate: entity is undefined')
|
|
704
|
+
assertIsKey(ret[Datastore.KEY])
|
|
705
|
+
yield ret as IDstoreEntryWithKey
|
|
599
706
|
}
|
|
600
|
-
return await this.runQuery(q);
|
|
601
707
|
} catch (error) {
|
|
602
|
-
await setImmediate()
|
|
708
|
+
await setImmediate()
|
|
603
709
|
throw new DstoreError('datastore.query error', error as Error, {
|
|
604
710
|
kindName,
|
|
605
711
|
filters,
|
|
606
712
|
limit,
|
|
607
713
|
ordering,
|
|
608
|
-
})
|
|
714
|
+
})
|
|
609
715
|
}
|
|
610
716
|
}
|
|
611
717
|
|
|
@@ -621,10 +727,10 @@ export class Dstore implements IDstore {
|
|
|
621
727
|
* In fact the generated ID is namespaced via an incomplete [[Key]] of the given Kind.
|
|
622
728
|
*/
|
|
623
729
|
async allocateOneId(kindName = 'Numbering'): Promise<string> {
|
|
624
|
-
assertIsString(kindName)
|
|
625
|
-
const ret = (await this.datastore.allocateIds(this.key([kindName]), 1))[0][0].id
|
|
626
|
-
assertIsString(ret)
|
|
627
|
-
return ret
|
|
730
|
+
assertIsString(kindName)
|
|
731
|
+
const ret = (await this.datastore.allocateIds(this.key([kindName]), 1))[0][0].id
|
|
732
|
+
assertIsString(ret)
|
|
733
|
+
return ret
|
|
628
734
|
}
|
|
629
735
|
|
|
630
736
|
/** This tries to give high level access to transactions.
|
|
@@ -642,27 +748,27 @@ export class Dstore implements IDstore {
|
|
|
642
748
|
Most Applications today are running on "Firestore in Datastore Mode". Beware that the Datastore-Emulator fails with `error: 3 INVALID_ARGUMENT: Only ancestor queries are allowed inside transactions.` during [[runQuery]] while the Datastore on Google Infrastructure does not have such an restriction anymore as of 2022.
|
|
643
749
|
*/
|
|
644
750
|
async runInTransaction<T>(func: () => Promise<T>): Promise<T> {
|
|
645
|
-
let ret
|
|
646
|
-
const transaction: Transaction = this.datastore.transaction()
|
|
751
|
+
let ret
|
|
752
|
+
const transaction: Transaction = this.datastore.transaction()
|
|
647
753
|
await transactionAsyncLocalStorage.run(transaction, async () => {
|
|
648
|
-
const [transactionInfo, transactionRunApiResponse] = await transaction.run()
|
|
649
|
-
let commitApiResponse
|
|
754
|
+
const [transactionInfo, transactionRunApiResponse] = await transaction.run()
|
|
755
|
+
let commitApiResponse
|
|
650
756
|
try {
|
|
651
|
-
ret = await func()
|
|
757
|
+
ret = await func()
|
|
652
758
|
} catch (error) {
|
|
653
|
-
const rollbackInfo = await transaction.rollback()
|
|
759
|
+
const rollbackInfo = await transaction.rollback()
|
|
654
760
|
debug(
|
|
655
761
|
'Transaction failed, rollback initiated: %O %O %O %O',
|
|
656
762
|
transactionInfo,
|
|
657
763
|
transactionRunApiResponse,
|
|
658
764
|
rollbackInfo,
|
|
659
765
|
error
|
|
660
|
-
)
|
|
661
|
-
await setImmediate()
|
|
662
|
-
throw new DstoreError('datastore.transaction execution error', error as Error)
|
|
766
|
+
)
|
|
767
|
+
await setImmediate()
|
|
768
|
+
throw new DstoreError('datastore.transaction execution error', error as Error)
|
|
663
769
|
}
|
|
664
770
|
try {
|
|
665
|
-
commitApiResponse = (await transaction.commit())[0]
|
|
771
|
+
commitApiResponse = (await transaction.commit())[0]
|
|
666
772
|
} catch (error) {
|
|
667
773
|
debug(
|
|
668
774
|
'Transaction commit failed: %O %O %O %O ret: %O',
|
|
@@ -671,36 +777,36 @@ export class Dstore implements IDstore {
|
|
|
671
777
|
commitApiResponse,
|
|
672
778
|
error,
|
|
673
779
|
ret
|
|
674
|
-
)
|
|
675
|
-
await setImmediate()
|
|
676
|
-
throw new DstoreError('datastore.transaction execution error', error as Error)
|
|
780
|
+
)
|
|
781
|
+
await setImmediate()
|
|
782
|
+
throw new DstoreError('datastore.transaction execution error', error as Error)
|
|
677
783
|
}
|
|
678
|
-
})
|
|
679
|
-
return ret as T
|
|
784
|
+
})
|
|
785
|
+
return ret as T
|
|
680
786
|
}
|
|
681
787
|
}
|
|
682
788
|
|
|
683
789
|
export class DstoreError extends Error {
|
|
684
|
-
public readonly extensions: Record<string, unknown
|
|
685
|
-
public readonly originalError: Error | undefined
|
|
686
|
-
readonly [key: string]: unknown
|
|
790
|
+
public readonly extensions: Record<string, unknown>
|
|
791
|
+
public readonly originalError: Error | undefined
|
|
792
|
+
readonly [key: string]: unknown
|
|
687
793
|
|
|
688
794
|
constructor(message: string, originalError: Error | undefined, extensions?: Record<string, unknown>) {
|
|
689
|
-
super(`${message}: ${originalError?.message}`)
|
|
795
|
+
super(`${message}: ${originalError?.message}`)
|
|
690
796
|
|
|
691
797
|
// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
|
|
692
798
|
if (!this.name) {
|
|
693
|
-
Object.defineProperty(this, 'name', { value: 'DstoreError' })
|
|
799
|
+
Object.defineProperty(this, 'name', { value: 'DstoreError' })
|
|
694
800
|
}
|
|
695
801
|
// metadata: Metadata { internalRepr: Map(0) {}, options: {} },
|
|
696
|
-
this.originalError = originalError
|
|
697
|
-
this.extensions = { ...extensions }
|
|
802
|
+
this.originalError = originalError
|
|
803
|
+
this.extensions = { ...extensions }
|
|
698
804
|
this.stack =
|
|
699
805
|
(this.stack?.split('\n')[0] || '') +
|
|
700
806
|
'\n' +
|
|
701
807
|
(originalError?.stack?.split('\n')?.slice(1)?.join('\n') || '') +
|
|
702
808
|
'\n' +
|
|
703
|
-
(this.stack?.split('\n')?.slice(1)?.join('\n') || '')
|
|
809
|
+
(this.stack?.split('\n')?.slice(1)?.join('\n') || '')
|
|
704
810
|
|
|
705
811
|
// These are usually present on Datastore Errors
|
|
706
812
|
// logger.error({ err: originalError, extensions }, message);
|