Skip to content

Commit 8ab6209

Browse files
authored
feat: Add datastore mode data transforms (#1369)
* Work in progress property transforms * Change entity back to any * Add the new interfaces and middleware code * Pull the build property transform into separate fn * Add arrays to the build transform function * Add a test for property transforms * Correct test comments * More modular build function * Update the test to check for the final result * Add the request spy * Fix the bug making property always increment * Fix the test based on the most recent source code change * Refactor code for the array transforms * Add a guard in case the casted type doesn’t exist * Remove only * Add a header * Remove TODO * Remove TODO * Parameterize the test * Ran linter * Run the linter * Add a comment to buildPropertyTransforms * Add comments for building the property transforms * Add two more test cases * Build the 3 test cases * should process string inputs for max/min/increment * Produce test cases for different situations * Delete invalid test cases and run linter * Correct the order of operations test * Remove only
1 parent 167fc8e commit 8ab6209

File tree

4 files changed

+948
-117
lines changed

4 files changed

+948
-117
lines changed

src/entity.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1452,8 +1452,28 @@ export interface EntityProto {
14521452
excludeFromIndexes?: boolean;
14531453
}
14541454

1455+
/*
1456+
* This is the interface the user would provide transform operations in before
1457+
* they are converted to the google.datastore.v1.IPropertyTransform
1458+
* interface.
1459+
*
1460+
*/
1461+
export type PropertyTransform = {
1462+
property: string;
1463+
setToServerValue: boolean;
1464+
increment: any;
1465+
maximum: any;
1466+
minimum: any;
1467+
appendMissingElements: any[];
1468+
removeAllFromArray: any[];
1469+
};
1470+
1471+
interface EntityWithTransforms {
1472+
transforms?: PropertyTransform[];
1473+
}
1474+
14551475
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1456-
export type Entity = any;
1476+
export type Entity = any & EntityWithTransforms;
14571477
export type Entities = Entity | Entity[];
14581478

14591479
interface KeyProtoPathElement extends google.datastore.v1.Key.IPathElement {

src/index.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,16 @@ import {
3737
ServiceError,
3838
} from 'google-gax';
3939
import * as is from 'is';
40-
import {Transform, pipeline} from 'stream';
40+
import {pipeline, Transform} from 'stream';
4141

42-
import {entity, Entities, Entity, EntityProto, ValueProto} from './entity';
42+
import {
43+
entity,
44+
Entities,
45+
Entity,
46+
EntityProto,
47+
ValueProto,
48+
PropertyTransform,
49+
} from './entity';
4350
import {AggregateField} from './aggregate';
4451
import Key = entity.Key;
4552
export {Entity, Key, AggregateField};
@@ -70,6 +77,10 @@ import {AggregateQuery} from './aggregate';
7077
import {SaveEntity} from './interfaces/save';
7178
import {extendExcludeFromIndexes} from './utils/entity/extendExcludeFromIndexes';
7279
import {buildEntityProto} from './utils/entity/buildEntityProto';
80+
import IValue = google.datastore.v1.IValue;
81+
import IEntity = google.datastore.v1.IEntity;
82+
import ServerValue = google.datastore.v1.PropertyTransform.ServerValue;
83+
import {buildPropertyTransforms} from './utils/entity/buildPropertyTransforms';
7384

7485
const {grpc} = new GrpcClient();
7586

@@ -1098,7 +1109,8 @@ class Datastore extends DatastoreRequest {
10981109
entities
10991110
.map(DatastoreRequest.prepareEntityObject_)
11001111
.forEach((entityObject: Entity, index: number) => {
1101-
const mutation: Mutation = {};
1112+
const mutation: google.datastore.v1.IMutation = {};
1113+
11021114
let method = 'upsert';
11031115

11041116
if (entityObject.method) {
@@ -1120,7 +1132,15 @@ class Datastore extends DatastoreRequest {
11201132

11211133
entityProto.key = entity.keyToKeyProto(entityObject.key);
11221134

1123-
mutation[method] = entityProto;
1135+
mutation[method as 'upsert' | 'update' | 'insert' | 'delete'] =
1136+
entityProto as IEntity;
1137+
1138+
// We built the entityProto, now we should add the data transforms:
1139+
if (entityObject.transforms) {
1140+
mutation.propertyTransforms = buildPropertyTransforms(
1141+
entityObject.transforms,
1142+
);
1143+
}
11241144
mutations.push(mutation);
11251145
});
11261146

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {entity, PropertyTransform} from '../../entity';
16+
import {google} from '../../../protos/protos';
17+
import IValue = google.datastore.v1.IValue;
18+
import ServerValue = google.datastore.v1.PropertyTransform.ServerValue;
19+
20+
/**
21+
* This method takes the user supplied transforms object and returns a list of
22+
* corresponding transforms that are instead in the shape of the protos so that
23+
* they can be included in the grpc request to the service directly.
24+
*
25+
* @param transforms The transforms supplied by the user that the user intends
26+
* to apply.
27+
*/
28+
export function buildPropertyTransforms(transforms: PropertyTransform[]) {
29+
const propertyTransforms: google.datastore.v1.IPropertyTransform[] = [];
30+
transforms.forEach((transform: PropertyTransform) => {
31+
const property = transform.property;
32+
// If the user's transform has a setToServerValue property then ensure the
33+
// propertyTransforms sent in the request have a setToServerValue transform.
34+
if (transform.setToServerValue) {
35+
propertyTransforms.push({
36+
property,
37+
setToServerValue: ServerValue.REQUEST_TIME,
38+
});
39+
}
40+
// If the transform has an 'increment', 'maximum' or 'minimum' property then
41+
// add the corresponding property transform to the propertyTransforms in the
42+
// request.
43+
['increment', 'maximum', 'minimum'].forEach(type => {
44+
const castedType = type as 'increment' | 'maximum' | 'minimum';
45+
if (transform[castedType]) {
46+
propertyTransforms.push({
47+
property,
48+
[castedType]: entity.encodeValue(
49+
parseFloat(transform[castedType]),
50+
property,
51+
) as IValue,
52+
});
53+
}
54+
});
55+
// If the transform has an 'appendMissingElements' or 'removeAllFromArray'
56+
// property then add the corresponding property transform to the
57+
// propertyTransforms in the request.
58+
['appendMissingElements', 'removeAllFromArray'].forEach(type => {
59+
const castedType = type as 'appendMissingElements' | 'removeAllFromArray';
60+
if (transform[castedType]) {
61+
propertyTransforms.push({
62+
property,
63+
[castedType]: {
64+
values: transform[castedType].map(element => {
65+
return entity.encodeValue(element, property) as IValue;
66+
}),
67+
},
68+
});
69+
}
70+
});
71+
});
72+
return propertyTransforms;
73+
}

0 commit comments

Comments
 (0)