@naturalcycles/datastore-lib 3.38.2 → 3.39.0

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.
@@ -6,20 +6,21 @@ import type { DatastoreDBStreamOptions } from './datastore.model';
6
6
  export declare class DatastoreStreamReadable<T = any> extends Readable implements ReadableTyped<T> {
7
7
  private q;
8
8
  private logger;
9
- private originalLimit;
9
+ private readonly originalLimit;
10
10
  private rowsRetrieved;
11
11
  private endCursor?;
12
12
  private running;
13
13
  private done;
14
14
  private lastQueryDone?;
15
15
  private totalWait;
16
- private table;
16
+ private readonly table;
17
17
  /**
18
18
  * Used to support maxWait
19
19
  */
20
20
  private lastReadTimestamp;
21
- private maxWaitInterval;
22
- private opt;
21
+ private readonly maxWaitInterval;
22
+ private readonly opt;
23
+ private dsOpt;
23
24
  constructor(q: Query, opt: DatastoreDBStreamOptions, logger: CommonLogger);
24
25
  private runNextQuery;
25
26
  /**
@@ -26,6 +26,11 @@ class DatastoreStreamReadable extends node_stream_1.Readable {
26
26
  batchSize: 1000,
27
27
  ...opt,
28
28
  };
29
+ this.dsOpt = {};
30
+ if (opt.readAt) {
31
+ // Datastore expects UnixTimestamp in milliseconds
32
+ this.dsOpt.readTime = opt.readAt * 1000;
33
+ }
29
34
  this.originalLimit = q.limitVal;
30
35
  this.table = q.kinds[0];
31
36
  logger.log(`!! using experimentalCursorStream !! ${this.table}, batchSize: ${opt.batchSize}`);
@@ -71,7 +76,7 @@ class DatastoreStreamReadable extends node_stream_1.Readable {
71
76
  let info = {};
72
77
  try {
73
78
  await (0, js_lib_1.pRetry)(async () => {
74
- const res = await q.run();
79
+ const res = await q.run(this.dsOpt);
75
80
  rows = res[0];
76
81
  info = res[1];
77
82
  }, {
@@ -1,9 +1,9 @@
1
- import type { Datastore, Key, Query } from '@google-cloud/datastore';
1
+ import type { Datastore, Key } from '@google-cloud/datastore';
2
2
  import { Transaction } from '@google-cloud/datastore';
3
- import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBSaveOptions, CommonDBSupport, CommonDBTransactionOptions, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
3
+ import { BaseCommonDB, CommonDB, CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions, CommonDBSupport, CommonDBTransactionOptions, DBQuery, DBTransaction, DBTransactionFn, RunQueryResult } from '@naturalcycles/db-lib';
4
4
  import { CommonLogger, JsonSchemaObject, JsonSchemaRootObject, ObjectWithId } from '@naturalcycles/js-lib';
5
5
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
6
- import { DatastoreDBCfg, DatastoreDBOptions, DatastoreDBSaveOptions, DatastoreDBStreamOptions, DatastorePayload, DatastorePropertyStats, DatastoreStats } from './datastore.model';
6
+ import { DatastoreDBCfg, DatastoreDBOptions, DatastoreDBReadOptions, DatastoreDBSaveOptions, DatastoreDBStreamOptions, DatastorePropertyStats, DatastoreStats } from './datastore.model';
7
7
  /**
8
8
  * Datastore API:
9
9
  * https://googlecloudplatform.github.io/google-cloud-node/#/docs/datastore/1.0.3/datastore
@@ -22,17 +22,17 @@ export declare class DatastoreDB extends BaseCommonDB implements CommonDB {
22
22
  protected KEY: symbol;
23
23
  ds(): Datastore;
24
24
  ping(): Promise<void>;
25
- getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: DatastoreDBOptions): Promise<ROW[]>;
26
- runQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, _opt?: DatastoreDBOptions): Promise<RunQueryResult<ROW>>;
27
- runQueryCount<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, _opt?: DatastoreDBOptions): Promise<number>;
28
- runDatastoreQuery<ROW extends ObjectWithId>(q: Query): Promise<RunQueryResult<ROW>>;
25
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: DatastoreDBReadOptions): Promise<ROW[]>;
26
+ runQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, opt?: DatastoreDBReadOptions): Promise<RunQueryResult<ROW>>;
27
+ runQueryCount<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, opt?: DatastoreDBReadOptions): Promise<number>;
28
+ private runDatastoreQuery;
29
29
  private runQueryStream;
30
30
  streamQuery<ROW extends ObjectWithId>(dbQuery: DBQuery<ROW>, opt?: DatastoreDBStreamOptions): ReadableTyped<ROW>;
31
31
  /**
32
32
  * Returns saved entities with generated id/updated/created (non-mutating!)
33
33
  */
34
34
  saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: DatastoreDBSaveOptions<ROW>): Promise<void>;
35
- deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: DatastoreDBOptions): Promise<number>;
35
+ deleteByQuery<ROW extends ObjectWithId>(q: DBQuery<ROW>, opt?: DatastoreDBReadOptions): Promise<number>;
36
36
  /**
37
37
  * Limitation: Datastore's delete returns void, so we always return all ids here as "deleted"
38
38
  * regardless if they were actually deleted or not.
@@ -46,17 +46,21 @@ export declare class DatastoreDB extends BaseCommonDB implements CommonDB {
46
46
  getStats(table: string): Promise<DatastoreStats | undefined>;
47
47
  getStatsCount(table: string): Promise<number | undefined>;
48
48
  getTableProperties(table: string): Promise<DatastorePropertyStats[]>;
49
- mapId<T extends ObjectWithId>(o: any, preserveKey?: boolean): T;
50
- toDatastoreEntity<T = any>(kind: string, o: T & {
51
- id?: string | number;
52
- }, excludeFromIndexes?: string[]): DatastorePayload<T>;
53
- key(kind: string, id: string | number): Key;
54
- getDsKey(o: any): Key | undefined;
49
+ private mapId;
50
+ private toDatastoreEntity;
51
+ key(kind: string, id: string): Key;
52
+ private getDsKey;
55
53
  getKey(key: Key): string | undefined;
56
54
  createTable<ROW extends ObjectWithId>(_table: string, _schema: JsonSchemaObject<ROW>): Promise<void>;
57
55
  getTables(): Promise<string[]>;
58
56
  getTableSchema<ROW extends ObjectWithId>(table: string): Promise<JsonSchemaRootObject<ROW>>;
59
57
  private getPRetryOptions;
58
+ /**
59
+ * Silently rollback the transaction.
60
+ * It may happen that transaction is already committed/rolled back, so we don't want to throw an error here.
61
+ */
62
+ private rollback;
63
+ private getRunQueryOptions;
60
64
  }
61
65
  /**
62
66
  * https://cloud.google.com/datastore/docs/concepts/transactions#datastore-datastore-transactional-update-nodejs
@@ -66,7 +70,7 @@ export declare class DatastoreDBTransaction implements DBTransaction {
66
70
  tx: Transaction;
67
71
  constructor(db: DatastoreDB, tx: Transaction);
68
72
  rollback(): Promise<void>;
69
- getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBOptions): Promise<ROW[]>;
73
+ getByIds<ROW extends ObjectWithId>(table: string, ids: string[], opt?: CommonDBReadOptions): Promise<ROW[]>;
70
74
  saveBatch<ROW extends ObjectWithId>(table: string, rows: ROW[], opt?: CommonDBSaveOptions<ROW>): Promise<void>;
71
75
  deleteByIds(table: string, ids: string[], opt?: CommonDBOptions): Promise<number>;
72
76
  }
@@ -76,10 +76,11 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
76
76
  return [];
77
77
  const keys = ids.map(id => this.key(table, id));
78
78
  let rows;
79
+ const dsOpt = this.getRunQueryOptions(opt);
79
80
  if (this.cfg.timeout) {
80
81
  // First try
81
82
  try {
82
- const r = await (0, js_lib_1.pTimeout)(() => (opt.tx?.tx || this.ds()).get(keys), {
83
+ const r = await (0, js_lib_1.pTimeout)(() => (opt.tx?.tx || this.ds()).get(keys, dsOpt), {
83
84
  timeout: this.cfg.timeout,
84
85
  name: `datastore.getByIds(${table})`,
85
86
  });
@@ -92,7 +93,7 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
92
93
  const DS = datastoreLib.Datastore;
93
94
  this.cachedDatastore = new DS(this.cfg);
94
95
  // Second try (will throw)
95
- const r = await (0, js_lib_1.pRetry)(() => (opt.tx?.tx || this.ds()).get(keys), {
96
+ const r = await (0, js_lib_1.pRetry)(() => (opt.tx?.tx || this.ds()).get(keys, dsOpt), {
96
97
  ...this.getPRetryOptions(`datastore.getByIds(${table}) second try`),
97
98
  maxAttempts: 3,
98
99
  timeout: this.cfg.timeout,
@@ -106,7 +107,7 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
106
107
  }
107
108
  else {
108
109
  rows = await (0, js_lib_1.pRetry)(async () => {
109
- return (await this.ds().get(keys))[0];
110
+ return (await this.ds().get(keys, dsOpt))[0];
110
111
  }, this.getPRetryOptions(`datastore.getByIds(${table})`));
111
112
  }
112
113
  return (rows
@@ -119,30 +120,32 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
119
120
  // if (!q?.kinds?.length) return '' // should never be the case, but
120
121
  // return q.kinds[0]!
121
122
  // }
122
- async runQuery(dbQuery, _opt) {
123
+ async runQuery(dbQuery, opt = {}) {
123
124
  const idFilter = dbQuery._filters.find(f => f.name === 'id');
124
125
  if (idFilter) {
125
126
  const ids = idFilter.op === '==' ? [idFilter.val] : idFilter.val;
126
127
  return {
127
- rows: await this.getByIds(dbQuery.table, ids),
128
+ rows: await this.getByIds(dbQuery.table, ids, opt),
128
129
  };
129
130
  }
130
131
  const q = (0, query_util_1.dbQueryToDatastoreQuery)(dbQuery, this.ds().createQuery(dbQuery.table));
131
- const qr = await this.runDatastoreQuery(q);
132
+ const dsOpt = this.getRunQueryOptions(opt);
133
+ const qr = await this.runDatastoreQuery(q, dsOpt);
132
134
  // Special case when projection query didn't specify 'id'
133
135
  if (dbQuery._selectedFieldNames && !dbQuery._selectedFieldNames.includes('id')) {
134
136
  qr.rows = qr.rows.map(r => (0, js_lib_1._omit)(r, ['id']));
135
137
  }
136
138
  return qr;
137
139
  }
138
- async runQueryCount(dbQuery, _opt) {
140
+ async runQueryCount(dbQuery, opt = {}) {
139
141
  const q = (0, query_util_1.dbQueryToDatastoreQuery)(dbQuery.select([]), this.ds().createQuery(dbQuery.table));
140
142
  const aq = this.ds().createAggregationQuery(q).count('count');
141
- const [entities] = await this.ds().runAggregationQuery(aq);
143
+ const dsOpt = this.getRunQueryOptions(opt);
144
+ const [entities] = await this.ds().runAggregationQuery(aq, dsOpt);
142
145
  return entities[0]?.count;
143
146
  }
144
- async runDatastoreQuery(q) {
145
- const [entities, queryResult] = await this.ds().runQuery(q);
147
+ async runDatastoreQuery(q, dsOpt) {
148
+ const [entities, queryResult] = await this.ds().runQuery(q, dsOpt);
146
149
  const rows = entities.map(e => this.mapId(e));
147
150
  return {
148
151
  ...queryResult,
@@ -154,9 +157,10 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
154
157
  ...this.cfg.streamOptions,
155
158
  ..._opt,
156
159
  };
160
+ const dsOpt = this.getRunQueryOptions(opt);
157
161
  return (opt.experimentalCursorStream
158
162
  ? new DatastoreStreamReadable_1.DatastoreStreamReadable(q, opt, (0, js_lib_1.commonLoggerMinLevel)(this.cfg.logger, opt.debug ? 'log' : 'warn'))
159
- : this.ds().runQueryStream(q)).map(chunk => this.mapId(chunk));
163
+ : this.ds().runQueryStream(q, dsOpt)).map(chunk => this.mapId(chunk));
160
164
  }
161
165
  streamQuery(dbQuery, opt) {
162
166
  const q = (0, query_util_1.dbQueryToDatastoreQuery)(dbQuery, this.ds().createQuery(dbQuery.table));
@@ -190,14 +194,15 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
190
194
  throw err;
191
195
  }
192
196
  }
193
- async deleteByQuery(q, opt) {
197
+ async deleteByQuery(q, opt = {}) {
194
198
  const idFilter = q._filters.find(f => f.name === 'id');
195
199
  if (idFilter) {
196
200
  const ids = idFilter.op === '==' ? [idFilter.val] : idFilter.val;
197
201
  return await this.deleteByIds(q.table, ids, opt);
198
202
  }
199
203
  const datastoreQuery = (0, query_util_1.dbQueryToDatastoreQuery)(q.select([]), this.ds().createQuery(q.table));
200
- const { rows } = await this.runDatastoreQuery(datastoreQuery);
204
+ const dsOpt = this.getRunQueryOptions(opt);
205
+ const { rows } = await this.runDatastoreQuery(datastoreQuery, dsOpt);
201
206
  return await this.deleteByIds(q.table, rows.map(obj => obj.id), opt);
202
207
  }
203
208
  /**
@@ -223,7 +228,7 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
223
228
  await datastoreTx.commit();
224
229
  }
225
230
  catch (err) {
226
- await datastoreTx.rollback();
231
+ await this.rollback(datastoreTx);
227
232
  throw err;
228
233
  }
229
234
  }
@@ -282,7 +287,7 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
282
287
  }
283
288
  key(kind, id) {
284
289
  (0, js_lib_1._assert)(id, `Cannot save "${kind}" entity without "id"`);
285
- return this.ds().key([kind, String(id)]);
290
+ return this.ds().key([kind, id]);
286
291
  }
287
292
  getDsKey(o) {
288
293
  return o?.[this.KEY];
@@ -379,6 +384,27 @@ class DatastoreDB extends db_lib_1.BaseCommonDB {
379
384
  },
380
385
  };
381
386
  }
387
+ /**
388
+ * Silently rollback the transaction.
389
+ * It may happen that transaction is already committed/rolled back, so we don't want to throw an error here.
390
+ */
391
+ async rollback(datastoreTx) {
392
+ try {
393
+ await datastoreTx.rollback();
394
+ }
395
+ catch (err) {
396
+ // log the error, but don't re-throw, as this should be a graceful rollback
397
+ this.cfg.logger.error(err);
398
+ }
399
+ }
400
+ getRunQueryOptions(opt) {
401
+ if (!opt.readAt)
402
+ return {};
403
+ return {
404
+ // Datastore expects UnixTimestamp in milliseconds
405
+ readTime: opt.readAt * 1000,
406
+ };
407
+ }
382
408
  }
383
409
  exports.DatastoreDB = DatastoreDB;
384
410
  /**
@@ -1,5 +1,5 @@
1
1
  import type { DatastoreOptions, Key } from '@google-cloud/datastore';
2
- import { CommonDBOptions, CommonDBSaveOptions } from '@naturalcycles/db-lib';
2
+ import { CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions } from '@naturalcycles/db-lib';
3
3
  import { CommonLogger, NumberOfSeconds, ObjectWithId } from '@naturalcycles/js-lib';
4
4
  export interface DatastorePayload<T = any> {
5
5
  key: Key;
@@ -47,7 +47,7 @@ export interface DatastoreCredentials {
47
47
  client_secret?: string;
48
48
  refresh_token?: string;
49
49
  }
50
- export interface DatastoreDBStreamOptions extends DatastoreDBOptions {
50
+ export interface DatastoreDBStreamOptions extends DatastoreDBReadOptions {
51
51
  /**
52
52
  * Set to `true` to stream via experimental "cursor-query based stream".
53
53
  *
@@ -101,6 +101,8 @@ export interface DatastoreDBStreamOptions extends DatastoreDBOptions {
101
101
  }
102
102
  export interface DatastoreDBOptions extends CommonDBOptions {
103
103
  }
104
+ export interface DatastoreDBReadOptions extends CommonDBReadOptions {
105
+ }
104
106
  export interface DatastoreDBSaveOptions<ROW extends ObjectWithId> extends CommonDBSaveOptions<ROW> {
105
107
  }
106
108
  export interface DatastoreStats {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/datastore-lib",
3
- "version": "3.38.2",
3
+ "version": "3.39.0",
4
4
  "description": "Opinionated library to work with Google Datastore",
5
5
  "scripts": {
6
6
  "prepare": "husky",
@@ -1,26 +1,27 @@
1
1
  import { Readable } from 'node:stream'
2
2
  import { Query } from '@google-cloud/datastore'
3
- import type { RunQueryInfo } from '@google-cloud/datastore/build/src/query'
3
+ import type { RunQueryInfo, RunQueryOptions } from '@google-cloud/datastore/build/src/query'
4
4
  import { _ms, CommonLogger, pRetry, UnixTimestampMillisNumber } from '@naturalcycles/js-lib'
5
5
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib'
6
6
  import type { DatastoreDBStreamOptions } from './datastore.model'
7
7
 
8
8
  export class DatastoreStreamReadable<T = any> extends Readable implements ReadableTyped<T> {
9
- private originalLimit: number
9
+ private readonly originalLimit: number
10
10
  private rowsRetrieved = 0
11
11
  private endCursor?: string
12
12
  private running = false
13
13
  private done = false
14
14
  private lastQueryDone?: number
15
15
  private totalWait = 0
16
- private table: string
16
+ private readonly table: string
17
17
  /**
18
18
  * Used to support maxWait
19
19
  */
20
20
  private lastReadTimestamp: UnixTimestampMillisNumber = 0
21
- private maxWaitInterval: NodeJS.Timeout | undefined
21
+ private readonly maxWaitInterval: NodeJS.Timeout | undefined
22
22
 
23
- private opt: DatastoreDBStreamOptions & { batchSize: number }
23
+ private readonly opt: DatastoreDBStreamOptions & { batchSize: number }
24
+ private dsOpt: RunQueryOptions
24
25
 
25
26
  constructor(
26
27
  private q: Query,
@@ -34,6 +35,11 @@ export class DatastoreStreamReadable<T = any> extends Readable implements Readab
34
35
  batchSize: 1000,
35
36
  ...opt,
36
37
  }
38
+ this.dsOpt = {}
39
+ if (opt.readAt) {
40
+ // Datastore expects UnixTimestamp in milliseconds
41
+ this.dsOpt.readTime = opt.readAt * 1000
42
+ }
37
43
 
38
44
  this.originalLimit = q.limitVal
39
45
  this.table = q.kinds[0]!
@@ -99,7 +105,7 @@ export class DatastoreStreamReadable<T = any> extends Readable implements Readab
99
105
  try {
100
106
  await pRetry(
101
107
  async () => {
102
- const res = await q.run()
108
+ const res = await q.run(this.dsOpt)
103
109
  rows = res[0]
104
110
  info = res[1]
105
111
  },
@@ -1,10 +1,12 @@
1
1
  import type { Datastore, Key, Query } from '@google-cloud/datastore'
2
2
  import { PropertyFilter, Transaction } from '@google-cloud/datastore'
3
+ import { type RunQueryOptions } from '@google-cloud/datastore/build/src/query'
3
4
  import {
4
5
  BaseCommonDB,
5
6
  CommonDB,
6
7
  commonDBFullSupport,
7
8
  CommonDBOptions,
9
+ CommonDBReadOptions,
8
10
  CommonDBSaveMethod,
9
11
  CommonDBSaveOptions,
10
12
  CommonDBSupport,
@@ -38,6 +40,7 @@ import { boldWhite, ReadableTyped } from '@naturalcycles/nodejs-lib'
38
40
  import {
39
41
  DatastoreDBCfg,
40
42
  DatastoreDBOptions,
43
+ DatastoreDBReadOptions,
41
44
  DatastoreDBSaveOptions,
42
45
  DatastoreDBStreamOptions,
43
46
  DatastorePayload,
@@ -138,17 +141,19 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
138
141
  override async getByIds<ROW extends ObjectWithId>(
139
142
  table: string,
140
143
  ids: string[],
141
- opt: DatastoreDBOptions = {},
144
+ opt: DatastoreDBReadOptions = {},
142
145
  ): Promise<ROW[]> {
143
146
  if (!ids.length) return []
144
147
  const keys = ids.map(id => this.key(table, id))
145
148
  let rows: any[]
146
149
 
150
+ const dsOpt = this.getRunQueryOptions(opt)
151
+
147
152
  if (this.cfg.timeout) {
148
153
  // First try
149
154
  try {
150
155
  const r = await pTimeout(
151
- () => ((opt.tx as DatastoreDBTransaction)?.tx || this.ds()).get(keys),
156
+ () => ((opt.tx as DatastoreDBTransaction)?.tx || this.ds()).get(keys, dsOpt),
152
157
  {
153
158
  timeout: this.cfg.timeout,
154
159
  name: `datastore.getByIds(${table})`,
@@ -165,7 +170,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
165
170
 
166
171
  // Second try (will throw)
167
172
  const r = await pRetry(
168
- () => ((opt.tx as DatastoreDBTransaction)?.tx || this.ds()).get(keys),
173
+ () => ((opt.tx as DatastoreDBTransaction)?.tx || this.ds()).get(keys, dsOpt),
169
174
  {
170
175
  ...this.getPRetryOptions(`datastore.getByIds(${table}) second try`),
171
176
  maxAttempts: 3,
@@ -181,7 +186,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
181
186
  } else {
182
187
  rows = await pRetry(
183
188
  async () => {
184
- return (await this.ds().get(keys))[0]
189
+ return (await this.ds().get(keys, dsOpt))[0]
185
190
  },
186
191
  this.getPRetryOptions(`datastore.getByIds(${table})`),
187
192
  )
@@ -203,19 +208,20 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
203
208
 
204
209
  override async runQuery<ROW extends ObjectWithId>(
205
210
  dbQuery: DBQuery<ROW>,
206
- _opt?: DatastoreDBOptions,
211
+ opt: DatastoreDBReadOptions = {},
207
212
  ): Promise<RunQueryResult<ROW>> {
208
213
  const idFilter = dbQuery._filters.find(f => f.name === 'id')
209
214
  if (idFilter) {
210
215
  const ids: string[] = idFilter.op === '==' ? [idFilter.val] : idFilter.val
211
216
 
212
217
  return {
213
- rows: await this.getByIds(dbQuery.table, ids),
218
+ rows: await this.getByIds(dbQuery.table, ids, opt),
214
219
  }
215
220
  }
216
221
 
217
222
  const q = dbQueryToDatastoreQuery(dbQuery, this.ds().createQuery(dbQuery.table))
218
- const qr = await this.runDatastoreQuery<ROW>(q)
223
+ const dsOpt = this.getRunQueryOptions(opt)
224
+ const qr = await this.runDatastoreQuery<ROW>(q, dsOpt)
219
225
 
220
226
  // Special case when projection query didn't specify 'id'
221
227
  if (dbQuery._selectedFieldNames && !dbQuery._selectedFieldNames.includes('id')) {
@@ -227,16 +233,20 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
227
233
 
228
234
  override async runQueryCount<ROW extends ObjectWithId>(
229
235
  dbQuery: DBQuery<ROW>,
230
- _opt?: DatastoreDBOptions,
236
+ opt: DatastoreDBReadOptions = {},
231
237
  ): Promise<number> {
232
238
  const q = dbQueryToDatastoreQuery(dbQuery.select([]), this.ds().createQuery(dbQuery.table))
233
239
  const aq = this.ds().createAggregationQuery(q).count('count')
234
- const [entities] = await this.ds().runAggregationQuery(aq)
240
+ const dsOpt = this.getRunQueryOptions(opt)
241
+ const [entities] = await this.ds().runAggregationQuery(aq, dsOpt)
235
242
  return entities[0]?.count
236
243
  }
237
244
 
238
- async runDatastoreQuery<ROW extends ObjectWithId>(q: Query): Promise<RunQueryResult<ROW>> {
239
- const [entities, queryResult] = await this.ds().runQuery(q)
245
+ private async runDatastoreQuery<ROW extends ObjectWithId>(
246
+ q: Query,
247
+ dsOpt: RunQueryOptions,
248
+ ): Promise<RunQueryResult<ROW>> {
249
+ const [entities, queryResult] = await this.ds().runQuery(q, dsOpt)
240
250
 
241
251
  const rows = entities.map(e => this.mapId<ROW>(e))
242
252
 
@@ -254,6 +264,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
254
264
  ...this.cfg.streamOptions,
255
265
  ..._opt,
256
266
  }
267
+ const dsOpt = this.getRunQueryOptions(opt)
257
268
 
258
269
  return (
259
270
  opt.experimentalCursorStream
@@ -262,7 +273,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
262
273
  opt,
263
274
  commonLoggerMinLevel(this.cfg.logger, opt.debug ? 'log' : 'warn'),
264
275
  )
265
- : (this.ds().runQueryStream(q) as ReadableTyped<ROW>)
276
+ : (this.ds().runQueryStream(q, dsOpt) as ReadableTyped<ROW>)
266
277
  ).map(chunk => this.mapId(chunk))
267
278
  }
268
279
 
@@ -320,7 +331,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
320
331
 
321
332
  override async deleteByQuery<ROW extends ObjectWithId>(
322
333
  q: DBQuery<ROW>,
323
- opt?: DatastoreDBOptions,
334
+ opt: DatastoreDBReadOptions = {},
324
335
  ): Promise<number> {
325
336
  const idFilter = q._filters.find(f => f.name === 'id')
326
337
  if (idFilter) {
@@ -329,7 +340,8 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
329
340
  }
330
341
 
331
342
  const datastoreQuery = dbQueryToDatastoreQuery(q.select([]), this.ds().createQuery(q.table))
332
- const { rows } = await this.runDatastoreQuery<ROW>(datastoreQuery)
343
+ const dsOpt = this.getRunQueryOptions(opt)
344
+ const { rows } = await this.runDatastoreQuery<ROW>(datastoreQuery, dsOpt)
333
345
  return await this.deleteByIds(
334
346
  q.table,
335
347
  rows.map(obj => obj.id),
@@ -374,7 +386,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
374
386
  await fn(tx)
375
387
  await datastoreTx.commit()
376
388
  } catch (err) {
377
- await datastoreTx.rollback()
389
+ await this.rollback(datastoreTx)
378
390
  throw err
379
391
  }
380
392
  }
@@ -413,7 +425,7 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
413
425
  return stats
414
426
  }
415
427
 
416
- mapId<T extends ObjectWithId>(o: any, preserveKey = false): T {
428
+ private mapId<T extends ObjectWithId>(o: any, preserveKey = false): T {
417
429
  if (!o) return o
418
430
  const r = {
419
431
  ...o,
@@ -424,12 +436,12 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
424
436
  }
425
437
 
426
438
  // if key field exists on entity, it will be used as key (prevent to duplication of numeric keyed entities)
427
- toDatastoreEntity<T = any>(
439
+ private toDatastoreEntity<T extends ObjectWithId>(
428
440
  kind: string,
429
- o: T & { id?: string | number },
441
+ o: T,
430
442
  excludeFromIndexes: string[] = [],
431
443
  ): DatastorePayload<T> {
432
- const key = this.getDsKey(o) || this.key(kind, o.id!)
444
+ const key = this.getDsKey(o) || this.key(kind, o.id)
433
445
  const data = Object.assign({}, o) as any
434
446
  delete data.id
435
447
  delete data[this.KEY]
@@ -441,12 +453,12 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
441
453
  }
442
454
  }
443
455
 
444
- key(kind: string, id: string | number): Key {
456
+ key(kind: string, id: string): Key {
445
457
  _assert(id, `Cannot save "${kind}" entity without "id"`)
446
- return this.ds().key([kind, String(id)])
458
+ return this.ds().key([kind, id])
447
459
  }
448
460
 
449
- getDsKey(o: any): Key | undefined {
461
+ private getDsKey(o: any): Key | undefined {
450
462
  return o?.[this.KEY]
451
463
  }
452
464
 
@@ -549,6 +561,28 @@ export class DatastoreDB extends BaseCommonDB implements CommonDB {
549
561
  },
550
562
  }
551
563
  }
564
+
565
+ /**
566
+ * Silently rollback the transaction.
567
+ * It may happen that transaction is already committed/rolled back, so we don't want to throw an error here.
568
+ */
569
+ private async rollback(datastoreTx: Transaction): Promise<void> {
570
+ try {
571
+ await datastoreTx.rollback()
572
+ } catch (err) {
573
+ // log the error, but don't re-throw, as this should be a graceful rollback
574
+ this.cfg.logger.error(err)
575
+ }
576
+ }
577
+
578
+ private getRunQueryOptions(opt: DatastoreDBReadOptions): RunQueryOptions {
579
+ if (!opt.readAt) return {}
580
+
581
+ return {
582
+ // Datastore expects UnixTimestamp in milliseconds
583
+ readTime: opt.readAt * 1000,
584
+ }
585
+ }
552
586
  }
553
587
 
554
588
  /**
@@ -572,7 +606,7 @@ export class DatastoreDBTransaction implements DBTransaction {
572
606
  async getByIds<ROW extends ObjectWithId>(
573
607
  table: string,
574
608
  ids: string[],
575
- opt?: CommonDBOptions,
609
+ opt?: CommonDBReadOptions,
576
610
  ): Promise<ROW[]> {
577
611
  return await this.db.getByIds(table, ids, { ...opt, tx: this })
578
612
  }
@@ -1,5 +1,5 @@
1
1
  import type { DatastoreOptions, Key } from '@google-cloud/datastore'
2
- import { CommonDBOptions, CommonDBSaveOptions } from '@naturalcycles/db-lib'
2
+ import { CommonDBOptions, CommonDBReadOptions, CommonDBSaveOptions } from '@naturalcycles/db-lib'
3
3
  import { CommonLogger, NumberOfSeconds, ObjectWithId } from '@naturalcycles/js-lib'
4
4
 
5
5
  export interface DatastorePayload<T = any> {
@@ -56,7 +56,7 @@ export interface DatastoreCredentials {
56
56
  refresh_token?: string
57
57
  }
58
58
 
59
- export interface DatastoreDBStreamOptions extends DatastoreDBOptions {
59
+ export interface DatastoreDBStreamOptions extends DatastoreDBReadOptions {
60
60
  /**
61
61
  * Set to `true` to stream via experimental "cursor-query based stream".
62
62
  *
@@ -116,6 +116,8 @@ export interface DatastoreDBStreamOptions extends DatastoreDBOptions {
116
116
 
117
117
  export interface DatastoreDBOptions extends CommonDBOptions {}
118
118
 
119
+ export interface DatastoreDBReadOptions extends CommonDBReadOptions {}
120
+
119
121
  export interface DatastoreDBSaveOptions<ROW extends ObjectWithId>
120
122
  extends CommonDBSaveOptions<ROW> {}
121
123