@spine-event-engine/storage-datastore 2.0.0-snapshot.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 +201 -0
- package/README.md +222 -0
- package/REFERENCE.md +147 -0
- package/dist/datastore/column-mapping.d.ts +28 -0
- package/dist/datastore/column-mapping.d.ts.map +1 -0
- package/dist/datastore/column-mapping.js +141 -0
- package/dist/datastore/column-mapping.js.map +1 -0
- package/dist/datastore/delivery-cleanup.d.ts +31 -0
- package/dist/datastore/delivery-cleanup.d.ts.map +1 -0
- package/dist/datastore/delivery-cleanup.js +111 -0
- package/dist/datastore/delivery-cleanup.js.map +1 -0
- package/dist/datastore/entity-history.d.ts +82 -0
- package/dist/datastore/entity-history.d.ts.map +1 -0
- package/dist/datastore/entity-history.js +540 -0
- package/dist/datastore/entity-history.js.map +1 -0
- package/dist/datastore/id-column.d.ts +34 -0
- package/dist/datastore/id-column.d.ts.map +1 -0
- package/dist/datastore/id-column.js +103 -0
- package/dist/datastore/id-column.js.map +1 -0
- package/dist/datastore/namespace.d.ts +81 -0
- package/dist/datastore/namespace.d.ts.map +1 -0
- package/dist/datastore/namespace.js +123 -0
- package/dist/datastore/namespace.js.map +1 -0
- package/dist/datastore/record-storage.d.ts +223 -0
- package/dist/datastore/record-storage.d.ts.map +1 -0
- package/dist/datastore/record-storage.js +854 -0
- package/dist/datastore/record-storage.js.map +1 -0
- package/dist/datastore/storage-factory.d.ts +199 -0
- package/dist/datastore/storage-factory.d.ts.map +1 -0
- package/dist/datastore/storage-factory.js +223 -0
- package/dist/datastore/storage-factory.js.map +1 -0
- package/dist/datastore/tenant-catalog.d.ts +50 -0
- package/dist/datastore/tenant-catalog.d.ts.map +1 -0
- package/dist/datastore/tenant-catalog.js +154 -0
- package/dist/datastore/tenant-catalog.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/index.yaml +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
15
|
+
import { and, Datastore, or, PropertyFilter } from "@google-cloud/datastore";
|
|
16
|
+
import { StringifierRegistry } from "@spine-event-engine/core";
|
|
17
|
+
import { ColumnMappings, defaultQueryCandidateLimit, RecordStorage, TenantBoundary, } from "@spine-event-engine/storage";
|
|
18
|
+
import { StorageQueryValues } from "@spine-event-engine/storage/internal/query-values";
|
|
19
|
+
import { DatastoreColumnMapping } from "./column-mapping.js";
|
|
20
|
+
import { DatastoreIdColumn } from "./id-column.js";
|
|
21
|
+
import { DefaultNamespaceConverter, NamespaceAssignments, } from "./namespace.js";
|
|
22
|
+
const payloadProperty = "bytes";
|
|
23
|
+
const maxMutationsPerBatch = 500;
|
|
24
|
+
const maxCasAttempts = 3;
|
|
25
|
+
const casRetryDelayMs = 100;
|
|
26
|
+
const wrappedReadOptions = { wrapNumbers: true };
|
|
27
|
+
/**
|
|
28
|
+
* Private flat Datastore-entity codec for one record storage handle.
|
|
29
|
+
*/
|
|
30
|
+
class FlatEntityCodec {
|
|
31
|
+
recordSpec;
|
|
32
|
+
maxClientSideScan;
|
|
33
|
+
group;
|
|
34
|
+
#kind;
|
|
35
|
+
#namespace;
|
|
36
|
+
#idColumn;
|
|
37
|
+
#columnMapping;
|
|
38
|
+
constructor(context, recordSpec, client, maxClientSideScan, group, kind, namespaceConverter, stringifiers) {
|
|
39
|
+
this.recordSpec = recordSpec;
|
|
40
|
+
this.maxClientSideScan = maxClientSideScan;
|
|
41
|
+
this.group = group;
|
|
42
|
+
this.#kind =
|
|
43
|
+
kind ??
|
|
44
|
+
(group === undefined
|
|
45
|
+
? recordSpec.sourceType.typeName
|
|
46
|
+
: `${group.name}_${recordSpec.recordType.typeName.split(".").at(-1) ?? ""}`);
|
|
47
|
+
if (this.#kind.trim().length === 0 || Buffer.byteLength(this.#kind, "utf8") > 1_500)
|
|
48
|
+
throw new Error("Datastore record kind must be non-blank and at most 1,500 bytes.");
|
|
49
|
+
const boundary = TenantBoundary.of(context);
|
|
50
|
+
this.#namespace = boundary.single
|
|
51
|
+
? client.namespace
|
|
52
|
+
: namespaceConverter.toNamespace(boundary.tenantId);
|
|
53
|
+
this.#idColumn = new DatastoreIdColumn(recordSpec.idType, stringifiers);
|
|
54
|
+
this.#columnMapping = new DatastoreColumnMapping(stringifiers);
|
|
55
|
+
}
|
|
56
|
+
key(client, id) {
|
|
57
|
+
return client.key({
|
|
58
|
+
path: [this.#kind, this.#idColumn.value(id)],
|
|
59
|
+
...(this.#namespace === undefined ? {} : { namespace: this.#namespace }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
encode(record, columns) {
|
|
63
|
+
const data = {
|
|
64
|
+
[payloadProperty]: Buffer.from(toBinary(this.recordSpec.recordType, record, { writeUnknownFields: false })),
|
|
65
|
+
};
|
|
66
|
+
for (const [name, value] of columns) {
|
|
67
|
+
data[name] = this.columnValue(name, value);
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
id(entity, client) {
|
|
72
|
+
const key = datastoreKey(entity, client.KEY);
|
|
73
|
+
const name = datastoreKeyName(key);
|
|
74
|
+
if (typeof name !== "string") {
|
|
75
|
+
throw new Error("Datastore entity has no valid Spine record identifier.");
|
|
76
|
+
}
|
|
77
|
+
return this.#idColumn.read(name);
|
|
78
|
+
}
|
|
79
|
+
columns(entity) {
|
|
80
|
+
return new Map(this.recordSpec.columns.map((column) => [column.name, column.valueIn(this.decode(entity))]));
|
|
81
|
+
}
|
|
82
|
+
unindexedProperties(data) {
|
|
83
|
+
void data;
|
|
84
|
+
return [payloadProperty];
|
|
85
|
+
}
|
|
86
|
+
createQuery(client) {
|
|
87
|
+
return this.#namespace === undefined
|
|
88
|
+
? client.createQuery(this.#kind)
|
|
89
|
+
: client.createQuery(this.#namespace, this.#kind);
|
|
90
|
+
}
|
|
91
|
+
columnProperty(column) {
|
|
92
|
+
if (!this.recordSpec.columns.some((candidate) => candidate.name === column)) {
|
|
93
|
+
throw new Error(`Datastore record column "${column}" is not declared.`);
|
|
94
|
+
}
|
|
95
|
+
return column;
|
|
96
|
+
}
|
|
97
|
+
queryLimit() {
|
|
98
|
+
return this.maxClientSideScan + 1;
|
|
99
|
+
}
|
|
100
|
+
namespace() {
|
|
101
|
+
return this.#namespace;
|
|
102
|
+
}
|
|
103
|
+
columnValue(column, value) {
|
|
104
|
+
const declared = this.recordSpec.columns.find((candidate) => candidate.name === column);
|
|
105
|
+
if (declared === undefined)
|
|
106
|
+
throw new Error(`Datastore record column "${column}" is not declared.`);
|
|
107
|
+
return ColumnMappings.value(this.#columnMapping, declared.type, value);
|
|
108
|
+
}
|
|
109
|
+
pageCursor(entity, order, keySymbol) {
|
|
110
|
+
const key = datastoreKey(entity, keySymbol);
|
|
111
|
+
const record = this.decode(entity);
|
|
112
|
+
const values = order.map((item) => {
|
|
113
|
+
if (item.property === "__key__")
|
|
114
|
+
return key;
|
|
115
|
+
const column = this.recordSpec.columns.find((candidate) => candidate.name === item.property);
|
|
116
|
+
return column === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: ColumnMappings.value(this.#columnMapping, column.type, column.valueIn(record));
|
|
119
|
+
});
|
|
120
|
+
if (key === undefined || values.some((value) => value === undefined))
|
|
121
|
+
throw new Error("Datastore provider page continuation is malformed.");
|
|
122
|
+
return { values, key };
|
|
123
|
+
}
|
|
124
|
+
decode(entity) {
|
|
125
|
+
const payload = entity[payloadProperty];
|
|
126
|
+
if (!(payload instanceof Uint8Array)) {
|
|
127
|
+
throw new Error("Datastore entity cannot be decoded.");
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return fromBinary(this.recordSpec.recordType, payload, { readUnknownFields: false });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error("Datastore entity cannot be decoded.");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function datastoreKey(entity, property) {
|
|
138
|
+
return typeof property === "symbol" ? entity[property] : undefined;
|
|
139
|
+
}
|
|
140
|
+
function datastoreKeyName(key) {
|
|
141
|
+
if (typeof key !== "object" || key === null)
|
|
142
|
+
return undefined;
|
|
143
|
+
const value = key;
|
|
144
|
+
return value.name ?? (Array.isArray(value.path) ? value.path.at(-1) : undefined);
|
|
145
|
+
}
|
|
146
|
+
function keysetFilter(order, cursor, keySymbol) {
|
|
147
|
+
if (cursor.values.length !== order.length)
|
|
148
|
+
throw new Error("Datastore provider page continuation is malformed.");
|
|
149
|
+
const fields = [...order, { property: "__key__", direction: order.at(-1)?.direction ?? "asc" }];
|
|
150
|
+
const values = [...cursor.values, cursor.key];
|
|
151
|
+
const alternatives = fields.map((field, index) => {
|
|
152
|
+
const equal = fields
|
|
153
|
+
.slice(0, index)
|
|
154
|
+
.map((prior, priorIndex) => new PropertyFilter(prior.property, "=", values[priorIndex]));
|
|
155
|
+
const operator = field.direction === "asc" ? ">" : "<";
|
|
156
|
+
const current = new PropertyFilter(field.property, operator, values[index]);
|
|
157
|
+
return equal.length === 0 ? current : and([...equal, current]);
|
|
158
|
+
});
|
|
159
|
+
if (cursor.key === undefined || keySymbol === undefined)
|
|
160
|
+
throw new Error("Datastore provider page continuation is malformed.");
|
|
161
|
+
return or(alternatives);
|
|
162
|
+
}
|
|
163
|
+
function sameCursor(left, right) {
|
|
164
|
+
return (right !== undefined && JSON.stringify(left, cursorJson) === JSON.stringify(right, cursorJson));
|
|
165
|
+
}
|
|
166
|
+
function cursorJson(_key, value) {
|
|
167
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Provides one Datastore-backed storage handle for a record family.
|
|
171
|
+
*/
|
|
172
|
+
export class DatastoreRecordStorage extends RecordStorage {
|
|
173
|
+
client;
|
|
174
|
+
// prettier-ignore
|
|
175
|
+
/**
|
|
176
|
+
* Declares atomic conditional mutations for compatible Datastore handles.
|
|
177
|
+
*/
|
|
178
|
+
atomicCompareAndSet = true;
|
|
179
|
+
#codec;
|
|
180
|
+
/**
|
|
181
|
+
* Creates a Datastore-backed record storage handle.
|
|
182
|
+
*
|
|
183
|
+
* @param context Storage tenancy and naming context.
|
|
184
|
+
* @param recordSpec Schema, identifier, and column materialization contract.
|
|
185
|
+
* @param client Injected Datastore client owned by the factory.
|
|
186
|
+
* @param maxClientSideScan Maximum candidate records to materialize locally.
|
|
187
|
+
* @param group The optional generated storage group.
|
|
188
|
+
* @param kind The optional physical Datastore kind override.
|
|
189
|
+
* @param namespaceConverter Converts complete tenants to native namespaces.
|
|
190
|
+
* @param stringifiers Converts message-valued IDs and declared columns.
|
|
191
|
+
*/
|
|
192
|
+
constructor(context, recordSpec, client, maxClientSideScan, group, kind, namespaceConverter = new DefaultNamespaceConverter(), stringifiers = new StringifierRegistry()) {
|
|
193
|
+
super(context, recordSpec);
|
|
194
|
+
this.client = client;
|
|
195
|
+
this.#codec = new FlatEntityCodec(context, recordSpec, client, maxClientSideScan, group, kind, namespaceConverter instanceof NamespaceAssignments
|
|
196
|
+
? namespaceConverter
|
|
197
|
+
: new NamespaceAssignments(namespaceConverter), stringifiers);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Prepares one resolved physical row for an enclosing transaction.
|
|
201
|
+
*
|
|
202
|
+
* @param record The record to materialize.
|
|
203
|
+
* @returns The Datastore key and property map.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
transactionEntity(record) {
|
|
207
|
+
const materialized = this.recordSpec.materialize(record);
|
|
208
|
+
return {
|
|
209
|
+
key: this.#codec.key(this.client, materialized.id),
|
|
210
|
+
data: this.#codec.encode(materialized.record, materialized.columns),
|
|
211
|
+
excludeFromIndexes: [payloadProperty],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decodes one entity read by a provider-owned multi-record transaction.
|
|
216
|
+
*
|
|
217
|
+
* @param entity The raw Datastore entity returned inside that transaction.
|
|
218
|
+
* @returns The decoded record.
|
|
219
|
+
* @internal
|
|
220
|
+
*/
|
|
221
|
+
decodeTransactionEntity(entity) {
|
|
222
|
+
return this.#codec.decode(entity);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Checks whether one transaction-read entity is the exact expected record.
|
|
226
|
+
*
|
|
227
|
+
* @param entity The raw Datastore entity returned inside that transaction.
|
|
228
|
+
* @param expected The expected record snapshot.
|
|
229
|
+
* @returns Whether the persisted payload exactly matches the snapshot.
|
|
230
|
+
* @internal
|
|
231
|
+
*/
|
|
232
|
+
matchesTransactionEntity(entity, expected) {
|
|
233
|
+
const expectedData = this.transactionEntity(expected).data[payloadProperty];
|
|
234
|
+
return RecordValues.payloadEqual(entity[payloadProperty], expectedData);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Reads one bounded, fully provider-executed page.
|
|
238
|
+
*
|
|
239
|
+
* Entity history uses this internal seam for records that would otherwise
|
|
240
|
+
* exceed the public query materialization budget.
|
|
241
|
+
*
|
|
242
|
+
* @param request The provider filters, ordering, continuation, and row limit.
|
|
243
|
+
* @returns The decoded page and an explicit continuation when more rows remain.
|
|
244
|
+
* @internal
|
|
245
|
+
*/
|
|
246
|
+
async queryProviderPage(request) {
|
|
247
|
+
if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > 128)
|
|
248
|
+
throw new Error("Datastore provider page limit must be a positive integer no greater than 128.");
|
|
249
|
+
const query = this.#codec.createQuery(this.client);
|
|
250
|
+
for (const filter of request.filters ?? []) {
|
|
251
|
+
query.filter(new PropertyFilter(filter.property, filter.operator, this.#codec.columnValue(filter.property, filter.value)));
|
|
252
|
+
}
|
|
253
|
+
const order = request.order ?? [];
|
|
254
|
+
for (const item of order)
|
|
255
|
+
query.order(item.property, { descending: item.direction === "desc" });
|
|
256
|
+
if (!order.some((item) => item.property === "__key__"))
|
|
257
|
+
query.order("__key__", {
|
|
258
|
+
descending: order.at(-1)?.direction === "desc",
|
|
259
|
+
});
|
|
260
|
+
if (request.cursor !== undefined) {
|
|
261
|
+
const continuation = keysetFilter(order, request.cursor, this.client.KEY);
|
|
262
|
+
query.filter(continuation);
|
|
263
|
+
}
|
|
264
|
+
query.limit(request.limit);
|
|
265
|
+
const response = await this.provider(() => this.client.runQuery(query, wrappedReadOptions));
|
|
266
|
+
const entities = DatastoreResults.entities(response);
|
|
267
|
+
const info = DatastoreResults.queryInfo(response);
|
|
268
|
+
const entries = entities.map((entity) => ({
|
|
269
|
+
id: this.#codec.id(entity, this.client),
|
|
270
|
+
record: this.#codec.decode(entity),
|
|
271
|
+
}));
|
|
272
|
+
const cursor = entities.at(-1);
|
|
273
|
+
if (info.more && cursor === undefined)
|
|
274
|
+
throw new Error("Datastore provider page continuation is malformed.");
|
|
275
|
+
const next = cursor === undefined ? undefined : this.#codec.pageCursor(cursor, order, this.client.KEY);
|
|
276
|
+
if (info.more && request.cursor !== undefined && sameCursor(request.cursor, next))
|
|
277
|
+
throw new Error("Datastore provider page continuation did not advance.");
|
|
278
|
+
return {
|
|
279
|
+
entries,
|
|
280
|
+
cursor: info.more ? next : undefined,
|
|
281
|
+
hasMore: info.more,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Deletes a bounded group of provider entries.
|
|
286
|
+
*
|
|
287
|
+
* @param entries The entries selected by a preceding provider page.
|
|
288
|
+
* @returns Completes after the provider deletes the group.
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
async deleteProviderEntries(entries) {
|
|
292
|
+
if (entries.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
if (entries.length > 128)
|
|
295
|
+
throw new Error("Datastore provider delete group must contain no more than 128 entries.");
|
|
296
|
+
await this.provider(() => this.client.delete(entries.map((entry) => this.#codec.key(this.client, entry.id))));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Deletes the record at one storage slot.
|
|
300
|
+
* @param id The storage slot identifier.
|
|
301
|
+
* @returns Whether a record was deleted.
|
|
302
|
+
*/
|
|
303
|
+
async deleteRecord(id) {
|
|
304
|
+
const key = this.#codec.key(this.client, id);
|
|
305
|
+
const entity = DatastoreResults.first(await this.provider(() => this.client.get(key, wrappedReadOptions)));
|
|
306
|
+
if (entity === undefined) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
await this.provider(() => this.client.delete(key));
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Returns records matching a query.
|
|
314
|
+
* @param _query The record query.
|
|
315
|
+
* @returns The matching storage entries.
|
|
316
|
+
*/
|
|
317
|
+
async queryRecordEntries(_query) {
|
|
318
|
+
const query = this.#codec.createQuery(this.client);
|
|
319
|
+
const providerLimit = DatastoreRecordQuery.limit(_query, this.#codec.queryLimit());
|
|
320
|
+
DatastoreQueryPushdown.translate(query, _query, (id) => this.#codec.key(this.client, id), (column) => this.#codec.columnProperty(column), (column, value) => this.#codec.columnValue(column, value));
|
|
321
|
+
query.limit(providerLimit);
|
|
322
|
+
const entities = DatastoreResults.entities(await this.provider(() => this.client.runQuery(query, wrappedReadOptions)));
|
|
323
|
+
if (providerLimit === this.#codec.queryLimit() && entities.length >= providerLimit) {
|
|
324
|
+
throw new DatastoreQueryLimitError(providerLimit - 1);
|
|
325
|
+
}
|
|
326
|
+
const entries = entities.map((entity) => ({
|
|
327
|
+
id: this.#codec.id(entity, this.client),
|
|
328
|
+
record: this.#codec.decode(entity),
|
|
329
|
+
columns: this.#codec.columns(entity),
|
|
330
|
+
}));
|
|
331
|
+
if (DatastoreRecordQuery.isKeysetPage(_query))
|
|
332
|
+
return entries.map(({ id, record }) => ({ id, record }));
|
|
333
|
+
return LocalQueryResults.apply(entries, _query);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Returns the supported Datastore query capabilities.
|
|
337
|
+
* @returns The supported query capabilities.
|
|
338
|
+
*/
|
|
339
|
+
queryCapabilities() {
|
|
340
|
+
return {
|
|
341
|
+
comparisons: ["equal", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual"],
|
|
342
|
+
features: ["order", "mask", "limit"],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Returns candidate records for a normalized query plan.
|
|
347
|
+
* @param plan The normalized query plan.
|
|
348
|
+
* @returns The candidate storage entries.
|
|
349
|
+
*/
|
|
350
|
+
async queryPlanRecordEntries(plan) {
|
|
351
|
+
if (plan.predicate !== undefined && DatastoreQueryPushdown.legal(plan) === undefined)
|
|
352
|
+
throw new TypeError("Datastore normalized query has an illegal predicate or inequality ordering.");
|
|
353
|
+
for (const order of plan.order ?? [])
|
|
354
|
+
this.#codec.columnProperty(order.column);
|
|
355
|
+
const query = this.#codec.createQuery(this.client);
|
|
356
|
+
DatastoreQueryPushdown.plan(query, plan, (id) => this.#codec.key(this.client, id), (column) => this.#codec.columnProperty(column), (column, value) => this.#codec.columnValue(column, value));
|
|
357
|
+
const providerLimit = Math.min(this.#codec.queryLimit(), plan.limit ?? Number.POSITIVE_INFINITY, (plan.candidateLimit ?? defaultQueryCandidateLimit) + 1);
|
|
358
|
+
query.limit(providerLimit);
|
|
359
|
+
const entities = DatastoreResults.entities(await this.provider(() => this.client.runQuery(query, wrappedReadOptions)));
|
|
360
|
+
if (entities.length >= providerLimit && providerLimit === this.#codec.queryLimit()) {
|
|
361
|
+
throw new DatastoreQueryLimitError(providerLimit - 1);
|
|
362
|
+
}
|
|
363
|
+
return entities.map((entity) => ({
|
|
364
|
+
id: this.#codec.id(entity, this.client),
|
|
365
|
+
record: this.#codec.decode(entity),
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Reads the record at one storage slot.
|
|
370
|
+
* @param id The storage slot identifier.
|
|
371
|
+
* @returns The stored record, if present.
|
|
372
|
+
*/
|
|
373
|
+
async readRecord(id) {
|
|
374
|
+
const entity = DatastoreResults.first(await this.provider(() => this.client.get(this.#codec.key(this.client, id), wrappedReadOptions)));
|
|
375
|
+
return entity === undefined ? undefined : this.#codec.decode(entity);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Compares and conditionally replaces the record at one storage slot.
|
|
379
|
+
* @param id The storage slot identifier.
|
|
380
|
+
* @param expected The expected materialized record.
|
|
381
|
+
* @param next The replacement materialized record, if any.
|
|
382
|
+
* @returns Whether the conditional mutation was applied.
|
|
383
|
+
*/
|
|
384
|
+
async compareAndSetRecord(id, expected, next) {
|
|
385
|
+
for (let attempt = 0; attempt < maxCasAttempts; attempt += 1) {
|
|
386
|
+
try {
|
|
387
|
+
return await this.attemptCompareAndSet(id, expected, next);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (attempt + 1 < maxCasAttempts && DatastoreTransactions.retry(error)) {
|
|
391
|
+
await DatastoreTransactions.wait(attempt);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
throw DatastoreTransactions.redact(error);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
throw new Error("Datastore compare-and-set retry limit was reached.");
|
|
398
|
+
}
|
|
399
|
+
async attemptCompareAndSet(id, expected, next) {
|
|
400
|
+
const key = this.#codec.key(this.client, id);
|
|
401
|
+
const transaction = this.transaction();
|
|
402
|
+
try {
|
|
403
|
+
await transaction.run();
|
|
404
|
+
const entity = DatastoreResults.first(await transaction.get(key, wrappedReadOptions));
|
|
405
|
+
const expectedPayload = expected === undefined
|
|
406
|
+
? undefined
|
|
407
|
+
: this.#codec.encode(expected.record, expected.columns)[payloadProperty];
|
|
408
|
+
if (entity === undefined
|
|
409
|
+
? expected !== undefined
|
|
410
|
+
: !RecordValues.payloadEqual(entity[payloadProperty], expectedPayload)) {
|
|
411
|
+
await transaction.rollback();
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
if (next === undefined)
|
|
415
|
+
transaction.delete(key);
|
|
416
|
+
else {
|
|
417
|
+
const data = this.#codec.encode(next.record, next.columns);
|
|
418
|
+
transaction.save({ key, data, excludeFromIndexes: this.#codec.unindexedProperties(data) });
|
|
419
|
+
}
|
|
420
|
+
await transaction.commit();
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
await transaction.rollback().catch(() => undefined);
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Writes materialized records.
|
|
430
|
+
* @param records The materialized records to write.
|
|
431
|
+
* @returns Completes when the records are written.
|
|
432
|
+
*/
|
|
433
|
+
async writeAllRecords(records) {
|
|
434
|
+
for (let offset = 0; offset < records.length; offset += maxMutationsPerBatch) {
|
|
435
|
+
const rows = records.slice(offset, offset + maxMutationsPerBatch).map((record) => {
|
|
436
|
+
const data = this.#codec.encode(record.record, record.columns);
|
|
437
|
+
return {
|
|
438
|
+
key: this.#codec.key(this.client, record.id),
|
|
439
|
+
data,
|
|
440
|
+
excludeFromIndexes: this.#codec.unindexedProperties(data),
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
await this.provider(() => this.client.save(rows));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Writes one materialized record.
|
|
448
|
+
* @param record The materialized record to write.
|
|
449
|
+
* @returns Completes when the record is written.
|
|
450
|
+
*/
|
|
451
|
+
async writeRecord(record) {
|
|
452
|
+
const data = this.#codec.encode(record.record, record.columns);
|
|
453
|
+
const key = this.#codec.key(this.client, record.id);
|
|
454
|
+
await this.provider(() => this.client.save({
|
|
455
|
+
key,
|
|
456
|
+
data,
|
|
457
|
+
excludeFromIndexes: this.#codec.unindexedProperties(data),
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
async provider(operation) {
|
|
461
|
+
try {
|
|
462
|
+
return await operation();
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
throw new Error("Datastore provider operation failed.");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Creates a transaction scoped to this handle's selected namespace.
|
|
470
|
+
*
|
|
471
|
+
* @returns The namespace-scoped Datastore transaction.
|
|
472
|
+
* @internal
|
|
473
|
+
*/
|
|
474
|
+
transaction() {
|
|
475
|
+
const transaction = this.client.transaction();
|
|
476
|
+
const namespace = this.#codec.namespace();
|
|
477
|
+
if (namespace !== undefined)
|
|
478
|
+
transaction.namespace = namespace;
|
|
479
|
+
return transaction;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Converts values between Spine records and the Datastore provider representation.
|
|
484
|
+
*/
|
|
485
|
+
const RecordValues = Object.freeze(new (class {
|
|
486
|
+
// prettier-ignore
|
|
487
|
+
/**
|
|
488
|
+
* Compares canonical local values for equality.
|
|
489
|
+
*/
|
|
490
|
+
equal(left, right) {
|
|
491
|
+
return StorageQueryValues.equal(left, right);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Compares canonical local values for ordering.
|
|
495
|
+
*/
|
|
496
|
+
compare(left, right) {
|
|
497
|
+
return StorageQueryValues.compare(left, right);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Compares two persisted payload byte arrays.
|
|
501
|
+
*/
|
|
502
|
+
payloadEqual(left, right) {
|
|
503
|
+
return (left instanceof Uint8Array &&
|
|
504
|
+
right instanceof Uint8Array &&
|
|
505
|
+
left.byteLength === right.byteLength &&
|
|
506
|
+
left.every((value, index) => value === right[index]));
|
|
507
|
+
}
|
|
508
|
+
})());
|
|
509
|
+
/**
|
|
510
|
+
* Raised when a query would exceed the adapter's configured finite client-side scan budget.
|
|
511
|
+
*/
|
|
512
|
+
export class DatastoreQueryLimitError extends Error {
|
|
513
|
+
maxClientSideScan;
|
|
514
|
+
// prettier-ignore
|
|
515
|
+
/**
|
|
516
|
+
* Creates the scan-budget error.
|
|
517
|
+
*
|
|
518
|
+
* @param maxClientSideScan Configured maximum number of locally scanned candidates.
|
|
519
|
+
*/
|
|
520
|
+
constructor(maxClientSideScan) {
|
|
521
|
+
super(`Datastore query exceeded the client-side scan limit of ${String(maxClientSideScan)}.`);
|
|
522
|
+
this.maxClientSideScan = maxClientSideScan;
|
|
523
|
+
this.name = "DatastoreQueryLimitError";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Translates safe normalized record queries to Datastore query operations.
|
|
528
|
+
*/
|
|
529
|
+
class DatastoreQueryPushdownHelper {
|
|
530
|
+
// prettier-ignore
|
|
531
|
+
/**
|
|
532
|
+
* Translates the legacy record-query surface.
|
|
533
|
+
*/
|
|
534
|
+
translate(query, recordQuery, keyFor, columnProperty, columnValue) {
|
|
535
|
+
if (recordQuery.ids !== undefined && recordQuery.ids.length > 0) {
|
|
536
|
+
const keys = recordQuery.ids.map(keyFor);
|
|
537
|
+
const first = recordQuery.ids[0];
|
|
538
|
+
if (first === undefined)
|
|
539
|
+
throw new Error("Datastore record query ID is invalid.");
|
|
540
|
+
const key = keyFor(first);
|
|
541
|
+
query.filter(new PropertyFilter("__key__", keys.length === 1 ? "=" : "IN", keys.length === 1 ? key : keys));
|
|
542
|
+
}
|
|
543
|
+
for (const filter of recordQuery.filters ?? []) {
|
|
544
|
+
const values = Array.isArray(filter.value) ? filter.value : [filter.value];
|
|
545
|
+
const providerValues = filter.column === "id"
|
|
546
|
+
? values.map((value) => keyFor(value))
|
|
547
|
+
: values.map((value) => columnValue(filter.column, value));
|
|
548
|
+
query.filter(new PropertyFilter(filter.column === "id" ? "__key__" : columnProperty(filter.column), values.length === 1 ? "=" : "IN", values.length === 1 ? providerValues[0] : providerValues));
|
|
549
|
+
}
|
|
550
|
+
for (const order of recordQuery.sort ?? []) {
|
|
551
|
+
query.order(order.field === "id" ? "__key__" : columnProperty(order.field), {
|
|
552
|
+
descending: order.direction === "desc",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (!(recordQuery.sort ?? []).some((order) => order.field === "id"))
|
|
556
|
+
query.order("__key__");
|
|
557
|
+
if (recordQuery.after !== undefined && DatastoreRecordQuery.isKeysetPage(recordQuery))
|
|
558
|
+
query.filter(new PropertyFilter("__key__", ">", keyFor(recordQuery.after.id)));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Applies the all-or-nothing legal portion of a normalized query plan.
|
|
562
|
+
*/
|
|
563
|
+
plan(query, plan, keyFor, columnProperty, columnValue) {
|
|
564
|
+
const legal = this.legal(plan);
|
|
565
|
+
if (legal !== undefined) {
|
|
566
|
+
this.predicate(query, legal, keyFor, columnProperty, columnValue);
|
|
567
|
+
}
|
|
568
|
+
if (legal !== undefined || plan.predicate === undefined) {
|
|
569
|
+
for (const order of plan.order ?? []) {
|
|
570
|
+
query.order(columnProperty(order.column), { descending: order.direction === "desc" });
|
|
571
|
+
}
|
|
572
|
+
if ((plan.order?.length ?? 0) > 0)
|
|
573
|
+
query.order("__key__");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Returns the predicate only when every clause satisfies provider restrictions.
|
|
578
|
+
*/
|
|
579
|
+
legal(plan) {
|
|
580
|
+
const predicate = plan.predicate;
|
|
581
|
+
if (predicate === undefined)
|
|
582
|
+
return undefined;
|
|
583
|
+
const leaves = predicate.kind === "all" ? predicate.predicates : [predicate];
|
|
584
|
+
let idCount = 0;
|
|
585
|
+
const inequalityColumns = new Set();
|
|
586
|
+
for (const leaf of leaves) {
|
|
587
|
+
if (leaf.kind !== "ids" && leaf.kind !== "comparison")
|
|
588
|
+
return undefined;
|
|
589
|
+
if (leaf.kind === "ids") {
|
|
590
|
+
idCount += leaf.ids.length;
|
|
591
|
+
if (idCount > 30)
|
|
592
|
+
return undefined;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (leaf.operator !== "equal") {
|
|
596
|
+
inequalityColumns.add(leaf.column);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (inequalityColumns.size > 1)
|
|
600
|
+
return undefined;
|
|
601
|
+
const inequalityColumn = inequalityColumns.values().next().value;
|
|
602
|
+
if (inequalityColumn !== undefined && plan.order?.[0]?.column !== inequalityColumn) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
return predicate;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Adds one already-legal normalized predicate to the provider query.
|
|
609
|
+
*/
|
|
610
|
+
predicate(query, predicate, keyFor, columnProperty, columnValue) {
|
|
611
|
+
if (predicate.kind === "all") {
|
|
612
|
+
predicate.predicates.forEach((child) => {
|
|
613
|
+
this.predicate(query, child, keyFor, columnProperty, columnValue);
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (predicate.kind === "ids") {
|
|
618
|
+
const keys = predicate.ids.map(keyFor);
|
|
619
|
+
const firstKey = keys[0];
|
|
620
|
+
if (keys.length === 1 && firstKey !== undefined) {
|
|
621
|
+
query.filter(new PropertyFilter("__key__", "=", firstKey));
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
query.filter(new PropertyFilter("__key__", "IN", keys));
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (predicate.kind !== "comparison")
|
|
629
|
+
return;
|
|
630
|
+
const operator = predicate.operator === "equal"
|
|
631
|
+
? "="
|
|
632
|
+
: predicate.operator === "greaterThan"
|
|
633
|
+
? ">"
|
|
634
|
+
: predicate.operator === "lessThan"
|
|
635
|
+
? "<"
|
|
636
|
+
: predicate.operator === "greaterOrEqual"
|
|
637
|
+
? ">="
|
|
638
|
+
: "<=";
|
|
639
|
+
query.filter(new PropertyFilter(columnProperty(predicate.column), operator, columnValue(predicate.column, predicate.value)));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Frozen Datastore query pushdown operations.
|
|
644
|
+
*/
|
|
645
|
+
const DatastoreQueryPushdown = Object.freeze(new DatastoreQueryPushdownHelper());
|
|
646
|
+
/**
|
|
647
|
+
* Selects safe provider bounds for direct record queries.
|
|
648
|
+
*/
|
|
649
|
+
const DatastoreRecordQuery = Object.freeze(new (class {
|
|
650
|
+
// prettier-ignore
|
|
651
|
+
/**
|
|
652
|
+
* Returns a caller limit only when it cannot change local query semantics.
|
|
653
|
+
*
|
|
654
|
+
* @param query Supplies the requested record query.
|
|
655
|
+
* @param scanLimit Supplies the configured materialization limit.
|
|
656
|
+
* @returns The finite provider row bound.
|
|
657
|
+
*/
|
|
658
|
+
limit(query, scanLimit) {
|
|
659
|
+
const requestedLimit = query.limit;
|
|
660
|
+
if (requestedLimit !== undefined && this.isKeysetPage(query))
|
|
661
|
+
return requestedLimit;
|
|
662
|
+
if (query.limit !== undefined &&
|
|
663
|
+
query.ids === undefined &&
|
|
664
|
+
query.filters === undefined &&
|
|
665
|
+
query.sort === undefined &&
|
|
666
|
+
query.after === undefined &&
|
|
667
|
+
query.offset === undefined)
|
|
668
|
+
return query.limit;
|
|
669
|
+
return scanLimit;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Returns whether a legacy query is exactly an ascending identifier keyset page.
|
|
673
|
+
*
|
|
674
|
+
* @param query The record query to classify.
|
|
675
|
+
* @returns `true` when Datastore can apply its limit and continuation completely.
|
|
676
|
+
*/
|
|
677
|
+
isKeysetPage(query) {
|
|
678
|
+
const order = query.sort;
|
|
679
|
+
const continuation = query.after;
|
|
680
|
+
const continuationValue = continuation?.values[0];
|
|
681
|
+
return (query.limit !== undefined &&
|
|
682
|
+
query.ids === undefined &&
|
|
683
|
+
query.filters === undefined &&
|
|
684
|
+
query.offset === undefined &&
|
|
685
|
+
order?.length === 1 &&
|
|
686
|
+
order[0]?.field === "id" &&
|
|
687
|
+
order[0].direction !== "desc" &&
|
|
688
|
+
(continuation === undefined ||
|
|
689
|
+
(continuation.values.length === 1 &&
|
|
690
|
+
continuationValue?.field === "id" &&
|
|
691
|
+
RecordValues.equal(continuationValue.value, continuation.id))));
|
|
692
|
+
}
|
|
693
|
+
})());
|
|
694
|
+
/**
|
|
695
|
+
* Applies record-query filtering, ordering, continuation, and paging locally.
|
|
696
|
+
*/
|
|
697
|
+
class LocalQueryResultsHelper {
|
|
698
|
+
// prettier-ignore
|
|
699
|
+
/**
|
|
700
|
+
* Applies one legacy record query to materialized candidate entries.
|
|
701
|
+
*/
|
|
702
|
+
apply(entries, query) {
|
|
703
|
+
const matching = entries.filter((entry) => this.matches(entry, query));
|
|
704
|
+
const orders = query.sort ?? [];
|
|
705
|
+
const sorted = [...matching].sort((left, right) => this.order(left, right, orders));
|
|
706
|
+
const continuation = query.after;
|
|
707
|
+
const continued = continuation === undefined
|
|
708
|
+
? sorted
|
|
709
|
+
: sorted.filter((entry) => this.continuation(entry, orders, continuation) > 0);
|
|
710
|
+
const start = query.offset ?? 0;
|
|
711
|
+
const end = query.limit === undefined ? undefined : start + query.limit;
|
|
712
|
+
return continued.slice(start, end).map(({ id, record }) => ({ id, record }));
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Tests whether a candidate satisfies the ID and equality filters.
|
|
716
|
+
*/
|
|
717
|
+
matches(entry, query) {
|
|
718
|
+
return ((query.ids === undefined ||
|
|
719
|
+
query.ids.length === 0 ||
|
|
720
|
+
query.ids.some((id) => RecordValues.equal(id, entry.id))) &&
|
|
721
|
+
(query.filters ?? []).every((filter) => {
|
|
722
|
+
const actual = filter.column === "id" ? entry.id : entry.columns.get(filter.column);
|
|
723
|
+
const expected = Array.isArray(filter.value) ? filter.value : [filter.value];
|
|
724
|
+
return expected.some((value) => RecordValues.equal(value, actual));
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Orders candidates by requested sort fields and canonical ID tie-breaker.
|
|
729
|
+
*/
|
|
730
|
+
order(left, right, orders) {
|
|
731
|
+
for (const order of orders) {
|
|
732
|
+
const comparison = RecordValues.compare(this.value(left, order.field), this.value(right, order.field));
|
|
733
|
+
if (comparison !== 0)
|
|
734
|
+
return order.direction === "desc" ? comparison * -1 : comparison;
|
|
735
|
+
}
|
|
736
|
+
return RecordValues.compare(left.id, right.id);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Compares a candidate with a normalized continuation cursor.
|
|
740
|
+
*/
|
|
741
|
+
continuation(entry, orders, after) {
|
|
742
|
+
for (let index = 0; index < orders.length; index += 1) {
|
|
743
|
+
const order = orders[index];
|
|
744
|
+
if (order === undefined)
|
|
745
|
+
throw new Error("Record query continuation sort order is invalid.");
|
|
746
|
+
const comparison = RecordValues.compare(this.value(entry, order.field), after.values[index]?.value);
|
|
747
|
+
if (comparison !== 0)
|
|
748
|
+
return order.direction === "desc" ? comparison * -1 : comparison;
|
|
749
|
+
}
|
|
750
|
+
return RecordValues.compare(entry.id, after.id);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Obtains an ID or indexed column value from a candidate.
|
|
754
|
+
*/
|
|
755
|
+
value(entry, field) {
|
|
756
|
+
return field === "id" ? entry.id : entry.columns.get(field);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Frozen local record-query reconciliation operations.
|
|
761
|
+
*/
|
|
762
|
+
const LocalQueryResults = Object.freeze(new LocalQueryResultsHelper());
|
|
763
|
+
/**
|
|
764
|
+
* Protects transaction failures and coordinates bounded CAS retries.
|
|
765
|
+
*/
|
|
766
|
+
class DatastoreTransactionsHelper {
|
|
767
|
+
// prettier-ignore
|
|
768
|
+
/**
|
|
769
|
+
* Redacts credential-like errors while preserving safe provider failures.
|
|
770
|
+
*/
|
|
771
|
+
redact(error) {
|
|
772
|
+
void error;
|
|
773
|
+
return new Error("Datastore transaction failed.");
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Identifies a safe-to-retry Datastore transaction conflict.
|
|
777
|
+
*/
|
|
778
|
+
retry(error) {
|
|
779
|
+
return (error instanceof Error &&
|
|
780
|
+
!/(credential|payload|secret)/i.test(error.message) &&
|
|
781
|
+
Reflect.get(error, "code") === 10);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Waits using exponential jitter before a bounded CAS retry.
|
|
785
|
+
*/
|
|
786
|
+
async wait(attempt) {
|
|
787
|
+
const exponentialDelay = casRetryDelayMs * 2 ** attempt;
|
|
788
|
+
const jitter = Math.floor(Math.random() * exponentialDelay);
|
|
789
|
+
await new Promise((resolve) => setTimeout(resolve, exponentialDelay + jitter));
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Frozen Datastore transaction retry and redaction operations.
|
|
794
|
+
*/
|
|
795
|
+
const DatastoreTransactions = Object.freeze(new DatastoreTransactionsHelper());
|
|
796
|
+
/**
|
|
797
|
+
* Validates Datastore read and query response shapes.
|
|
798
|
+
*/
|
|
799
|
+
class DatastoreResultsHelper {
|
|
800
|
+
// prettier-ignore
|
|
801
|
+
/**
|
|
802
|
+
* Extracts the optional first entity from a Datastore read response.
|
|
803
|
+
*/
|
|
804
|
+
first(response) {
|
|
805
|
+
if (!Array.isArray(response))
|
|
806
|
+
throw new Error("Datastore returned an invalid entity response.");
|
|
807
|
+
const entity = response[0];
|
|
808
|
+
return entity === undefined ? undefined : this.entity(entity);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Extracts entity rows from a Datastore query response.
|
|
812
|
+
*/
|
|
813
|
+
entities(response) {
|
|
814
|
+
if (!Array.isArray(response) || !Array.isArray(response[0])) {
|
|
815
|
+
throw new Error("Datastore returned an invalid query response.");
|
|
816
|
+
}
|
|
817
|
+
return response[0].map((value) => this.entity(value));
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Extracts the provider continuation and the remaining-row indication.
|
|
821
|
+
*/
|
|
822
|
+
queryInfo(response) {
|
|
823
|
+
if (!Array.isArray(response) || typeof response[1] !== "object" || response[1] === null)
|
|
824
|
+
throw new Error("Datastore returned an invalid query response.");
|
|
825
|
+
const info = response[1];
|
|
826
|
+
const cursor = info.endCursor;
|
|
827
|
+
const moreResults = info.moreResults;
|
|
828
|
+
if (moreResults !== "NO_MORE_RESULTS" &&
|
|
829
|
+
moreResults !== "MORE_RESULTS_AFTER_LIMIT" &&
|
|
830
|
+
moreResults !== "MORE_RESULTS_AFTER_CURSOR")
|
|
831
|
+
throw new Error("Datastore returned an invalid query response.");
|
|
832
|
+
const more = moreResults === "MORE_RESULTS_AFTER_LIMIT" || moreResults === "MORE_RESULTS_AFTER_CURSOR";
|
|
833
|
+
if (more && !(cursor instanceof Buffer || typeof cursor === "string"))
|
|
834
|
+
throw new Error("Datastore returned an invalid query response.");
|
|
835
|
+
return {
|
|
836
|
+
cursor: cursor instanceof Buffer || typeof cursor === "string" ? cursor : undefined,
|
|
837
|
+
more,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Validates one provider entity object.
|
|
842
|
+
*/
|
|
843
|
+
entity(value) {
|
|
844
|
+
if (typeof value !== "object" || value === null) {
|
|
845
|
+
throw new Error("Datastore returned an invalid entity response.");
|
|
846
|
+
}
|
|
847
|
+
return value;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Frozen Datastore provider response parsing operations.
|
|
852
|
+
*/
|
|
853
|
+
const DatastoreResults = Object.freeze(new DatastoreResultsHelper());
|
|
854
|
+
//# sourceMappingURL=record-storage.js.map
|