Skip to content

Commit 8e026af

Browse files
praveenqlogic01stephenplusplus
authored andcommitted
feat: add merge method for merging an object into an existing entity (#452)
1 parent f266e0a commit 8e026af

File tree

4 files changed

+243
-0
lines changed

4 files changed

+243
-0
lines changed

samples/tasks.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,24 @@ async function markDone(taskId) {
119119
}
120120
// [END datastore_update_entity]
121121

122+
// [START datastore_merge_entity]
123+
async function merge(taskId, description) {
124+
const taskKey = datastore.key(['Task', taskId]);
125+
const task = {
126+
description,
127+
};
128+
try {
129+
await datastore.merge({
130+
key: taskKey,
131+
data: task,
132+
});
133+
console.log(`Task ${taskId} description updated successfully.`);
134+
} catch (err) {
135+
console.error('ERROR:', err);
136+
}
137+
}
138+
// [END datastore_merge_entity]
139+
122140
// [START datastore_retrieve_entities]
123141
async function listTasks() {
124142
const query = datastore.createQuery('Task').order('created');
@@ -151,12 +169,19 @@ require(`yargs`) // eslint-disable-line
151169
.command(`done <taskId>`, `Marks the specified task as done.`, {}, opts =>
152170
markDone(opts.taskId)
153171
)
172+
.command(`merge <taskId>`, `Marks the specified task as done.`, {}, opts =>
173+
merge(opts.taskId, opts.description)
174+
)
154175
.command(`list`, `Lists all tasks ordered by creation time.`, {}, listTasks)
155176
.command(`delete <taskId>`, `Deletes a task.`, {}, opts =>
156177
deleteTask(opts.taskId)
157178
)
158179
.example(`node $0 new "Buy milk"`, `Adds a task with description "Buy milk".`)
159180
.example(`node $0 done 12345`, `Marks task 12345 as Done.`)
181+
.example(
182+
`node $0 merge 12345`,
183+
`update task 12345 with description "Buy food".`
184+
)
160185
.example(`node $0 list`, `Lists all tasks ordered by creation time`)
161186
.example(`node $0 delete 12345`, `Deletes task 12345.`)
162187
.wrap(120)

src/request.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
RunQueryCallback,
4949
} from './query';
5050
import {Datastore} from '.';
51+
import {ServiceError} from '@grpc/grpc-js';
5152

5253
/**
5354
* A map of read consistency values to proto codes.
@@ -1235,6 +1236,65 @@ class DatastoreRequest {
12351236
this.save(entities, callback!);
12361237
}
12371238

1239+
merge(entities: Entities): Promise<CommitResponse>;
1240+
merge(entities: Entities, callback: SaveCallback): void;
1241+
/**
1242+
* Merge the specified object(s). If a key is incomplete, its associated object
1243+
* is inserted and the original Key object is updated to contain the generated ID.
1244+
* For example, if you provide an incomplete key (one without an ID),
1245+
* the request will create a new entity and have its ID automatically assigned.
1246+
* If you provide a complete key, the entity will be get the data from datastore
1247+
* and merge with the data specified.
1248+
* By default, all properties are indexed. To prevent a property from being
1249+
* included in *all* indexes, you must supply an `excludeFromIndexes` array.
1250+
*
1251+
* Maps to {@link Datastore#save}, forcing the method to be `merge`.
1252+
*
1253+
* @param {object|object[]} entities Datastore key object(s).
1254+
* @param {Key} entities.key Datastore key object.
1255+
* @param {string[]} [entities.excludeFromIndexes] Exclude properties from
1256+
* indexing using a simple JSON path notation. See the examples in
1257+
* {@link Datastore#save} to see how to target properties at different
1258+
* levels of nesting within your entity.
1259+
* @param {object} entities.data Data to merge to the same for provided key.
1260+
* @param {function} callback The callback function.
1261+
* @param {?error} callback.err An error returned while making this request
1262+
* @param {object} callback.apiResponse The full API response.
1263+
*/
1264+
merge(
1265+
entities: Entities,
1266+
callback?: SaveCallback
1267+
): void | Promise<CommitResponse> {
1268+
const transaction = this.datastore.transaction();
1269+
transaction.run(async err => {
1270+
if (err) {
1271+
transaction.rollback();
1272+
callback!(err);
1273+
return;
1274+
}
1275+
try {
1276+
await Promise.all(
1277+
arrify(entities).map(async (objEntity: Entity) => {
1278+
const obj: Entity = DatastoreRequest.prepareEntityObject_(
1279+
objEntity
1280+
);
1281+
const [data] = await transaction.get(obj.key);
1282+
obj.method = 'upsert';
1283+
obj.data = Object.assign({}, data, obj.data);
1284+
transaction.save(obj);
1285+
})
1286+
);
1287+
1288+
const [response] = await transaction.commit();
1289+
callback!(null, response);
1290+
} catch (err) {
1291+
transaction.rollback();
1292+
callback!(err);
1293+
}
1294+
});
1295+
}
1296+
1297+
request_(config: RequestConfig, callback: RequestCallback): void;
12381298
/**
12391299
* Make a request to the API endpoint. Properties to indicate a transactional
12401300
* or non-transactional operation are added automatically.

system-test/datastore.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,29 @@ describe('Datastore', () => {
305305
await datastore.delete(postKey);
306306
});
307307

308+
it('should save/get/merge', async () => {
309+
const postKey = datastore.key(['Post', 1]);
310+
const originalData = {
311+
key: postKey,
312+
data: {
313+
title: 'Original',
314+
status: 'neat',
315+
},
316+
};
317+
await datastore.save(originalData);
318+
const updatedData = {
319+
key: postKey,
320+
data: {
321+
title: 'Updated',
322+
},
323+
};
324+
await datastore.merge(updatedData);
325+
const [entity] = await datastore.get(postKey);
326+
assert.strictEqual(entity.title, updatedData.data.title);
327+
assert.strictEqual(entity.status, originalData.data.status);
328+
await datastore.delete(postKey);
329+
});
330+
308331
it('should save and get with a string ID', async () => {
309332
const longIdKey = datastore.key([
310333
'Post',

test/request.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import {
3131
AllocateIdsResponse,
3232
RequestConfig,
3333
RequestOptions,
34+
PrepareEntityObjectResponse,
35+
CommitResponse,
36+
GetResponse,
3437
} from '../src/request';
3538

3639
// tslint:disable-next-line no-any
@@ -1847,6 +1850,138 @@ describe('Request', () => {
18471850
});
18481851
});
18491852

1853+
describe('merge', () => {
1854+
// tslint:disable-next-line: variable-name
1855+
let Transaction: typeof ds.Transaction;
1856+
let transaction: ds.Transaction;
1857+
const PROJECT_ID = 'project-id';
1858+
const NAMESPACE = 'a-namespace';
1859+
1860+
const DATASTORE = ({
1861+
request_() {},
1862+
projectId: PROJECT_ID,
1863+
namespace: NAMESPACE,
1864+
} as {}) as ds.Datastore;
1865+
1866+
const key = {
1867+
namespace: 'ns',
1868+
kind: 'Company',
1869+
path: ['Company', null],
1870+
};
1871+
const entityObject = {};
1872+
1873+
before(() => {
1874+
Transaction = proxyquire('../src/transaction.js', {
1875+
'@google-cloud/promisify': fakePfy,
1876+
}).Transaction;
1877+
});
1878+
1879+
beforeEach(() => {
1880+
transaction = new Transaction(DATASTORE);
1881+
1882+
transaction.request_ = () => {};
1883+
1884+
transaction.commit = async () => {
1885+
return [{}] as CommitResponse;
1886+
};
1887+
request.datastore = {
1888+
transaction: () => transaction,
1889+
};
1890+
// tslint:disable-next-line: no-any
1891+
(transaction as any).run = (callback?: Function) => {
1892+
callback!(null);
1893+
};
1894+
1895+
transaction.get = async () => {
1896+
return [entityObject] as GetResponse;
1897+
};
1898+
1899+
transaction.commit = async () => {
1900+
return [{}] as CommitResponse;
1901+
};
1902+
});
1903+
1904+
afterEach(() => sandbox.restore());
1905+
1906+
it('should return merge object for entity', done => {
1907+
const updatedEntityObject = {
1908+
status: 'merged',
1909+
};
1910+
1911+
transaction.save = (modifiedData: PrepareEntityObjectResponse) => {
1912+
assert.deepStrictEqual(
1913+
modifiedData.data,
1914+
Object.assign({}, entityObject, updatedEntityObject)
1915+
);
1916+
};
1917+
1918+
request.merge({key, data: updatedEntityObject}, done);
1919+
});
1920+
1921+
it('should return merge objects for entities', done => {
1922+
const updatedEntityObject = [
1923+
{
1924+
id: 1,
1925+
status: 'merged',
1926+
},
1927+
{
1928+
id: 2,
1929+
status: 'merged',
1930+
},
1931+
];
1932+
1933+
transaction.commit = async () => {
1934+
transaction.modifiedEntities_.forEach((entity, index) => {
1935+
assert.deepStrictEqual(
1936+
entity.args[0].data,
1937+
Object.assign({}, entityObject, updatedEntityObject[index])
1938+
);
1939+
});
1940+
return [{}] as CommitResponse;
1941+
};
1942+
1943+
request.merge(
1944+
[
1945+
{key, data: updatedEntityObject[0]},
1946+
{key, data: updatedEntityObject[1]},
1947+
],
1948+
done
1949+
);
1950+
});
1951+
1952+
it('transaction should rollback if error on transaction run!', done => {
1953+
sandbox
1954+
.stub(transaction, 'run')
1955+
.callsFake((gaxOption, callback?: Function) => {
1956+
callback = typeof gaxOption === 'function' ? gaxOption : callback!;
1957+
callback(new Error('Error'));
1958+
});
1959+
1960+
request.merge({key, data: null}, (err: Error) => {
1961+
assert.strictEqual(err.message, 'Error');
1962+
done();
1963+
});
1964+
});
1965+
1966+
it('transaction should rollback if error for for transaction get!', done => {
1967+
sandbox.stub(transaction, 'get').rejects(new Error('Error'));
1968+
1969+
request.merge({key, data: null}, (err: Error) => {
1970+
assert.strictEqual(err.message, 'Error');
1971+
done();
1972+
});
1973+
});
1974+
1975+
it('transaction should rollback if error for for transaction commit!', done => {
1976+
sandbox.stub(transaction, 'commit').rejects(new Error('Error'));
1977+
1978+
request.merge({key, data: null}, (err: Error) => {
1979+
assert.strictEqual(err.message, 'Error');
1980+
done();
1981+
});
1982+
});
1983+
});
1984+
18501985
describe('request_', () => {
18511986
const CONFIG = {
18521987
client: 'FakeClient', // name set at top of file

0 commit comments

Comments
 (0)