Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit a7be2d2

Browse files
authored
fix broken init logic when there's a key prefix (#6)
1 parent ced995f commit a7be2d2

File tree

3 files changed

+103
-24
lines changed

3 files changed

+103
-24
lines changed

dynamodb_feature_store.js

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function dynamoDBFeatureStoreInternal(tableName, options) {
5959
}
6060
}
6161
cb(results);
62-
}, function (err) {
62+
}).catch(function (err) {
6363
logger.error('failed to get all ' + kind.namespace + ': ' + err);
6464
cb(null);
6565
});
@@ -70,50 +70,41 @@ function dynamoDBFeatureStoreInternal(tableName, options) {
7070
.then(function(existingItems) {
7171
var existingNamespaceKeys = {};
7272
for (var i = 0; i < existingItems.length; i++) {
73-
existingNamespaceKeys[makeNamespaceKey(existingItems[i])] = existingItems[i].version;
73+
existingNamespaceKeys[makeNamespaceKey(existingItems[i])] = true;
7474
}
7575
delete existingNamespaceKeys[makeNamespaceKey(initializedToken())];
7676

7777
// Write all initial data (without version checks).
7878
var ops = [];
7979
allData.forEach(function(collection) {
80-
var kindNamespace = collection.kind.namespace;
8180
collection.items.forEach(function(item) {
8281
var key = item.key;
83-
delete existingNamespaceKeys[kindNamespace + '$' + key];
84-
ops.push({ PutRequest: makePutRequest(collection.kind, item) });
82+
delete existingNamespaceKeys[namespaceForKind(collection.kind) + '$' + key];
83+
ops.push({ PutRequest: { Item: marshalItem(collection.kind, item) } });
8584
});
8685
});
8786

8887
// Remove existing data that is not in the new list.
8988
for (var namespaceKey in existingNamespaceKeys) {
90-
var version = existingNamespaceKeys[namespaceKey];
9189
var namespaceAndKey = namespaceKey.split('$');
92-
ops.push({ DeleteRequest: {
93-
TableName: tableName,
94-
Key: {
95-
namespace: namespaceAndKey[0],
96-
key: namespaceAndKey[1]
97-
},
98-
ConditionExpression: 'attribute_not_exists(version) OR version < :new_version',
99-
ExpressionAttributeValues: {':new_version': version }
100-
}});
90+
ops.push({ DeleteRequest: { Key: { namespace: namespaceAndKey[0], key: namespaceAndKey[1] } } });
10191
}
10292

10393
// Always write the initialized token when we initialize.
104-
ops.push({PutRequest: { TableName: tableName, Item: initializedToken() }});
94+
ops.push({ PutRequest: { Item: initializedToken() } });
10595

10696
var writePromises = helpers.batchWrite(dynamoDBClient, tableName, ops);
10797

108-
return Promise.all(writePromises).then(function() { cb && cb(); });
109-
},
110-
function (err) {
98+
return Promise.all(writePromises);
99+
})
100+
.catch(function (err) {
111101
logger.error('failed to initialize: ' + err);
112-
});
102+
})
103+
.then(function() { cb && cb(); });
113104
};
114105

115106
store.upsertInternal = function(kind, item, cb) {
116-
var params = makePutRequest(kind, item);
107+
var params = makeVersionedPutRequest(kind, item);
117108

118109
// testUpdateHook is instrumentation, used only by the unit tests
119110
var prepare = store.testUpdateHook || function(prepareCb) { prepareCb(); };
@@ -213,7 +204,7 @@ function dynamoDBFeatureStoreInternal(tableName, options) {
213204
return null;
214205
}
215206

216-
function makePutRequest(kind, item) {
207+
function makeVersionedPutRequest(kind, item) {
217208
return {
218209
TableName: tableName,
219210
Item: marshalItem(kind, item),

tests/.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
"env": {
33
"node": true,
44
"es6": true,
5-
"jasmine": true
5+
"jasmine": true,
6+
"jest": true
67
},
78
};

tests/dynamodb_feature_store-test.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
var DynamoDBFeatureStore = require('../dynamodb_feature_store');
22
var helpers = require('../dynamodb_helpers');
33
var testBase = require('ldclient-node/test/feature_store_test_base');
4+
var dataKind = require('ldclient-node/versioned_data_kind');
45
var AWS = require('aws-sdk');
56

7+
function stubLogger() {
8+
return {
9+
debug: jest.fn(),
10+
info: jest.fn(),
11+
warn: jest.fn(),
12+
error: jest.fn()
13+
};
14+
}
15+
616
describe('DynamoDBFeatureStore', function() {
717

818
AWS.config.update({
@@ -13,7 +23,7 @@ describe('DynamoDBFeatureStore', function() {
1323

1424
var dynamodb = new AWS.DynamoDB();
1525

16-
var table='test-store';
26+
var table = 'test-store';
1727

1828
beforeAll(function(done) {
1929
dynamodb.describeTable({ TableName: table }, function(err) {
@@ -94,6 +104,10 @@ describe('DynamoDBFeatureStore', function() {
94104
return new DynamoDBFeatureStore(table, {prefix: prefix, cacheTTL: 0});
95105
}
96106

107+
function makeStoreWithDefaultPrefix() {
108+
return makeStoreWithPrefix('test');
109+
}
110+
97111
function makeStoreWithHook(hook) {
98112
var store = makeStore();
99113
store.underlyingStore.testUpdateHook = hook;
@@ -108,5 +122,78 @@ describe('DynamoDBFeatureStore', function() {
108122
testBase.baseFeatureStoreTests(makeStoreWithoutCache, clearTable, false, makeStoreWithPrefix);
109123
});
110124

125+
// We run the test suite again here because in the DynamoDB implementation, the prefix is entirely
126+
// omitted by default, so we want to make sure all the logic is correct with or without one.
127+
describe('uncached with prefix', function() {
128+
testBase.baseFeatureStoreTests(makeStoreWithDefaultPrefix, clearTable, false);
129+
});
130+
111131
testBase.concurrentModificationTests(makeStore, makeStoreWithHook);
132+
133+
describe('handling errors from DynamoDB client', function() {
134+
var err = new Error('error');
135+
var client;
136+
var logger;
137+
var store;
138+
139+
beforeEach(() => {
140+
client = {};
141+
logger = stubLogger();
142+
store = new DynamoDBFeatureStore(table, { dynamoDBClient: client, logger: logger });
143+
});
144+
145+
it('error from query in init', done => {
146+
var data = { features: { flag: { key: 'flag', version: 1 } } };
147+
client.query = (params, cb) => cb(err);
148+
store.init(data, function() {
149+
expect(logger.error).toHaveBeenCalled();
150+
done();
151+
});
152+
});
153+
154+
it('error from batchWrite in init', done => {
155+
var data = { features: { flag: { key: 'flag', version: 1 } } };
156+
client.query = (params, cb) => cb(null, { Items: [] });
157+
client.batchWrite = (params, cb) => cb(err);
158+
store.init(data, function() {
159+
expect(logger.error).toHaveBeenCalled();
160+
done();
161+
});
162+
});
163+
164+
it('error from get', done => {
165+
client.get = (params, cb) => cb(err);
166+
store.get(dataKind.features, 'flag', function(result) {
167+
expect(result).toBe(null);
168+
expect(logger.error).toHaveBeenCalled();
169+
done();
170+
});
171+
});
172+
173+
it('error from get all', done => {
174+
client.query = (params, cb) => cb(err);
175+
store.all(dataKind.features, function(result) {
176+
expect(result).toBe(null);
177+
expect(logger.error).toHaveBeenCalled();
178+
done();
179+
});
180+
});
181+
182+
it('error from upsert', done => {
183+
client.put = (params, cb) => cb(err);
184+
store.upsert(dataKind.features, { key: 'flag', version: 1 }, function() {
185+
expect(logger.error).toHaveBeenCalled();
186+
done();
187+
});
188+
});
189+
190+
it('error from initialized', done => {
191+
client.get = (params, cb) => cb(err);
192+
store.initialized(function(result) {
193+
expect(result).toBe(false);
194+
expect(logger.error).toHaveBeenCalled();
195+
done();
196+
});
197+
});
198+
});
112199
});

0 commit comments

Comments
 (0)