Skip to content

Commit 8a00a9c

Browse files
committed
"Project" (select) fields
1 parent c38be6c commit 8a00a9c

File tree

6 files changed

+259
-35
lines changed

6 files changed

+259
-35
lines changed

src/client/js/services/record-handler.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ module fng.services {
558558
if (errorMessage.length > 0) {
559559
errorMessage = response.data.message + '<br /><ul>' + errorMessage + '</ul>';
560560
} else {
561-
errorMessage = response.data.message || 'Error! Sorry - No further details available.';
561+
errorMessage = response.data.message || response.data.err || 'Error! Sorry - No further details available.';
562562
}
563563
$scope.showError(errorMessage);
564564
} else {
@@ -583,26 +583,14 @@ module fng.services {
583583
SubmissionsService.readRecord($scope.modelName, $scope.id)
584584
.then(function (response) {
585585
let data: any = response.data;
586-
if (data.success === false) {
586+
handleIncomingData(data, $scope, ctrlState);
587+
}, function(error) {
588+
if (error.status === 404) {
587589
$location.path('/404');
588-
// TODO Figure out tab history updates (check for other tab-history-todos)
589-
// } else if (response.master) {
590-
//
591-
// ctrlState.allowLocationChange = false;
592-
// $scope.phase = 'ready';
593-
// $scope.record = angular.copy(response.data);
594-
// ctrlState.master = angular.copy(response.master);
595-
// if (response.changed) {
596-
// $timeout(() => {
597-
// $scope[$scope.topLevelFormName].$setDirty();
598-
// });
599-
// } else {
600-
// $timeout($scope.setPristine);
601-
// }
602590
} else {
603-
handleIncomingData(data, $scope, ctrlState);
591+
$scope.handleHttpError(error);
604592
}
605-
}, $scope.handleHttpError);
593+
});
606594
},
607595

608596
scrollTheList: function scrollTheList($scope) {

src/server/data_form.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ DataForm.prototype.internalSearch = function (req, resourcesToSearch, includeRes
417417
handleSearchResultsFromIndex(err, docs, item, cb);
418418
});
419419
} else {
420-
that.filteredFind(item.resource, req, null, searchDoc, item.resource.options.searchOrder, limit + 60, null, function(err, docs) {
420+
that.filteredFind(item.resource, req, null, searchDoc, null, item.resource.options.searchOrder, limit + 60, null, function(err, docs) {
421421
handleSearchResultsFromIndex(err, docs, item, cb);
422422
});
423423
}
@@ -951,6 +951,7 @@ DataForm.prototype.collectionGet = function () {
951951
try {
952952
const aggregationParam = req.query.a ? JSON.parse(req.query.a) : null;
953953
const findParam = req.query.f ? JSON.parse(req.query.f) : {};
954+
const projectParam = req.query.p ? JSON.parse(req.query.p) : {};
954955
const limitParam = req.query.l ? JSON.parse(req.query.l) : 0;
955956
const skipParam = req.query.s ? JSON.parse(req.query.s) : 0;
956957
const orderParam = req.query.o ? JSON.parse(req.query.o) : req.resource.options.listOrder;
@@ -961,8 +962,7 @@ DataForm.prototype.collectionGet = function () {
961962
}
962963

963964
const self = this;
964-
965-
this.filteredFind(req.resource, req, aggregationParam, findParam, orderParam, limitParam, skipParam, function (err, docs) {
965+
this.filteredFind(req.resource, req, aggregationParam, findParam, projectParam, orderParam, limitParam, skipParam, function (err, docs) {
966966
if (err) {
967967
return self.renderError(err, null, req, res, next);
968968
} else {
@@ -975,6 +975,48 @@ DataForm.prototype.collectionGet = function () {
975975
}, this);
976976
};
977977

978+
DataForm.prototype.generateProjection = function(hiddenFields, projectParam): any {
979+
980+
let type;
981+
982+
function setSelectType(typeChar, checkChar) {
983+
if (type === checkChar) {
984+
throw new Error('Cannot mix include and exclude fields in select');
985+
} else {
986+
type = typeChar;
987+
}
988+
}
989+
990+
let retVal: any = hiddenFields;
991+
if (projectParam) {
992+
let projection = Object.keys(projectParam);
993+
if (projection.length > 0) {
994+
projection.forEach(p => {
995+
if (projectParam[p] === 0) {
996+
setSelectType('E','I');
997+
} else if (projectParam[p] === 1) {
998+
setSelectType('I','E');
999+
} else {
1000+
throw new Error('Invalid projection: ' + projectParam);
1001+
}
1002+
});
1003+
if (type === 'E') {
1004+
// We are excluding fields - can just merge with hiddenFields
1005+
Object.assign(retVal, projectParam, hiddenFields);
1006+
} else {
1007+
// We are selecting fields - make sure none are hidden
1008+
retVal = projectParam;
1009+
for (let h in hiddenFields) {
1010+
if (hiddenFields.hasOwnProperty(h)) {
1011+
delete retVal[h];
1012+
}
1013+
}
1014+
}
1015+
}
1016+
}
1017+
return retVal;
1018+
};
1019+
9781020
DataForm.prototype.doFindFunc = function (req, resource, cb) {
9791021
if (resource.options.findFunc) {
9801022
resource.options.findFunc(req, cb);
@@ -983,7 +1025,7 @@ DataForm.prototype.doFindFunc = function (req, resource, cb) {
9831025
}
9841026
};
9851027

986-
DataForm.prototype.filteredFind = function (resource, req, aggregationParam, findParam, sortOrder, limit, skip, callback) {
1028+
DataForm.prototype.filteredFind = function (resource, req, aggregationParam, findParam, projectParam, sortOrder, limit, skip, callback) {
9871029

9881030
const that = this;
9891031
let hiddenFields = this.generateHiddenFields(resource, false);
@@ -1029,7 +1071,7 @@ DataForm.prototype.filteredFind = function (resource, req, aggregationParam, fin
10291071
if (findParam) {
10301072
query = query.find(findParam);
10311073
}
1032-
query = query.select(hiddenFields);
1074+
query = query.select(that.generateProjection(hiddenFields, projectParam));
10331075
if (limit) {
10341076
query = query.limit(limit);
10351077
}
@@ -1117,16 +1159,17 @@ DataForm.prototype.cleanseRequest = function (req) {
11171159
};
11181160

11191161
DataForm.prototype.generateQueryForEntity = function (req, resource, id, cb) {
1162+
let that = this;
11201163
let hiddenFields = this.generateHiddenFields(resource, false);
11211164
hiddenFields.__v = 0;
11221165

1123-
this.doFindFunc(req, resource, function (err, queryObj) {
1166+
that.doFindFunc(req, resource, function (err, queryObj) {
11241167
if (err) {
11251168
cb(err);
11261169
} else {
11271170
let idSel = {_id: id};
11281171
let crit = queryObj ? extend(queryObj, idSel) : idSel;
1129-
cb(null, resource.model.findOne(crit).select(hiddenFields));
1172+
cb(null, resource.model.findOne(crit).select(that.generateProjection(hiddenFields, req.query.p)));
11301173
}
11311174
});
11321175
};
@@ -1142,20 +1185,20 @@ DataForm.prototype.processEntity = function (req, res, next) {
11421185
}
11431186
this.generateQueryForEntity(req, req.resource, req.params.id, function (err, query) {
11441187
if (err) {
1145-
return res.send({
1188+
return res.status(500).send({
11461189
success: false,
11471190
err: util.inspect(err)
11481191
});
11491192
} else {
11501193
query.exec(function (err, doc) {
11511194
if (err) {
1152-
return res.send({
1195+
return res.status(400).send({
11531196
success: false,
11541197
err: util.inspect(err)
11551198
});
11561199
}
11571200
else if (doc == null) {
1158-
return res.send({
1201+
return res.status(404).send({
11591202
success: false,
11601203
err: 'Record not found'
11611204
});
@@ -1181,10 +1224,10 @@ DataForm.prototype.entityGet = function () {
11811224
}
11821225
if (req.resource.options.onAccess) {
11831226
req.resource.options.onAccess(req, function() {
1184-
return res.send(req.doc);
1227+
return res.status(200).send(req.doc);
11851228
});
11861229
} else {
1187-
return res.send(req.doc);
1230+
return res.status(200).send(req.doc);
11881231
}
11891232
});
11901233
}, this);

test/api/API-Spec.js

Lines changed: 193 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,120 @@ describe('API tests', function() {
166166

167167
describe('API', function() {
168168

169-
describe('data read', function() {
169+
describe.only('document read', function() {
170+
171+
var bData, status;
172+
173+
function getItem(id, query, cb) {
174+
var mockReq = {
175+
url: '/b_using_options/' + id,
176+
params: { resourceName: 'b_using_options', id: id },
177+
query: query
178+
};
179+
var mockRes = {
180+
status: function(data) {
181+
status = data;
182+
return this;
183+
},
184+
send: function(data) {
185+
cb(null, data);
186+
}
187+
};
188+
fng.entityGet()(mockReq, mockRes);
189+
}
190+
191+
describe('simple', function() {
192+
193+
before(function(done) {
194+
getItem('519a6075b320153869b175e0', {}, function(err, result) {
195+
if (err) {throw err;}
196+
bData = result;
197+
done();
198+
});
199+
});
200+
201+
it('should send a record', function() {
202+
assert(status === 200);
203+
assert(bData);
204+
});
205+
206+
it('should not send secure fields of a modified schema', function() {
207+
assert(bData.surname, 'Must send surname');
208+
assert(bData.forename, 'Must send forename');
209+
assert(!bData.login, 'Must not send secure login field');
210+
assert(!bData.passwordHash, 'Must not send secure password hash field');
211+
assert(bData.email, 'Must send email');
212+
assert(bData.weight, 'Must send weight');
213+
assert(bData.accepted, 'Must send accepted');
214+
assert(bData.interviewScore, 'Must send interview score');
215+
assert(bData.freeText, 'Must send freetext');
216+
});
217+
218+
it('should not send secure fields of a modified subschema', function() {
219+
assert(bData.address.line1, 'Must send line1');
220+
assert(bData.address.town, 'Must send town');
221+
assert(bData.address.postcode, 'Must send postcode');
222+
assert(!bData.address.surveillance, 'Must not send secure surveillance field');
223+
});
224+
225+
});
226+
227+
describe('projection', function() {
228+
229+
before(function(done) {
230+
getItem('519a6075b320153869b175e0', {p: {surname:1, forename:1, login:1, 'address.line1':1, 'address.surveillance': 1}}, function(err, result) {
231+
if (err) {throw err;}
232+
bData = result;
233+
done();
234+
});
235+
});
236+
237+
it('should send a record', function() {
238+
assert(bData);
239+
assert(status === 200);
240+
});
241+
242+
it('should not send secure fields of a modified schema', function() {
243+
assert(bData.surname, 'Must send surname');
244+
assert(bData.forename, 'Must send forename');
245+
assert(!bData.login, 'Must not send secure login field');
246+
assert(!bData.passwordHash, 'Must not send secure password hash field');
247+
assert(!bData.email, 'Must not send email');
248+
assert(!bData.weight, 'Must not send weight');
249+
assert(!bData.accepted, 'Must not send accepted');
250+
assert(!bData.interviewScore, 'Must not send interview score');
251+
assert(!bData.freeText, 'Must not send freetext');
252+
});
253+
254+
it('should not send secure fields of a modified subschema', function() {
255+
assert(bData.address.line1, 'Must send line1');
256+
assert(!bData.address.town, 'Must not send town');
257+
assert(!bData.address.postcode, 'Must not send postcode');
258+
assert(!bData.address.surveillance, 'Must not send secure surveillance field');
259+
});
260+
261+
});
262+
263+
describe('findFunc filter', function() {
264+
265+
before(function(done) {
266+
getItem('519a6075b440153869b155e0', {}, function(err, result) {
267+
if (err) {throw err;}
268+
bData = result;
269+
done();
270+
});
271+
});
272+
273+
it('should not send a record', function() {
274+
assert(!bData || !bData.success);
275+
assert(status === 404);
276+
});
277+
278+
});
279+
280+
});
281+
282+
describe('collection read', function() {
170283

171284
var aData, aPtr, bData, bPtr;
172285

@@ -248,6 +361,85 @@ describe('API tests', function() {
248361

249362
});
250363

364+
describe('collection projection', function() {
365+
366+
var aData, aPtr, bData, bPtr;
367+
368+
function getCollectionProjection(model, proj, cb) {
369+
var mockReq = {
370+
url: '/' + model,
371+
query : {p : JSON.stringify(proj)},
372+
params: { resourceName: model }
373+
};
374+
var mockRes = {
375+
send: function(data) {
376+
cb(null, data);
377+
}
378+
};
379+
fng.collectionGet()(mockReq, mockRes);
380+
}
381+
382+
before(function(done) {
383+
async.auto(
384+
{
385+
aData: function(cb) {
386+
getCollectionProjection('a_unadorned_mongoose', {forename:0, weight:0}, cb);
387+
},
388+
bData: function(cb) {
389+
getCollectionProjection('b_using_options', {surname: 1, weight: 1, login: 1, 'address.surveillance': 1, 'address.line1': 1}, cb);
390+
}
391+
},
392+
function(err, results) {
393+
if (err) {
394+
throw err;
395+
}
396+
aData = results['aData'];
397+
aPtr = aData.find(function(obj) {
398+
return obj.surname === 'TestPerson1'
399+
});
400+
bData = results['bData'];
401+
bPtr = bData.find(function(obj) {
402+
return obj.surname === 'IsAccepted1'
403+
});
404+
done();
405+
}
406+
);
407+
});
408+
409+
it('should send the right number of records', function() {
410+
assert.strictEqual(aData.length, 2);
411+
});
412+
413+
it('should suppress unselected fields', function() {
414+
assert(aPtr.surname, 'must send surname');
415+
assert(!aPtr.forename, 'must not send forename');
416+
assert(!aPtr.weight, 'must not send weight');
417+
assert(aPtr.eyeColour, 'must send eyeColour');
418+
assert(aPtr.dateOfBirth, 'must send dob');
419+
assert.strictEqual(aPtr.accepted, false, 'must send accepted');
420+
});
421+
422+
it('should send select fields unless they are secure', function() {
423+
assert(bPtr.surname, 'Must send surname');
424+
assert(!bPtr.forename, 'Must not send forename');
425+
assert(!bPtr.login, 'Must not send secure login field');
426+
assert(!bPtr.passwordHash, 'Must not send secure password hash field');
427+
assert(!bPtr.email, 'Must not send email');
428+
assert(bPtr.weight, 'Must send weight');
429+
assert(!bPtr.accepted, 'Must not send accepted');
430+
assert(!bPtr.interviewScore, 'Must not send interview score');
431+
assert(!bPtr.freeText, 'Must not send freetext');
432+
});
433+
434+
it('should not send secure fields of a modified subschema', function() {
435+
assert(bPtr.address.line1, 'Must send line1');
436+
assert(!bPtr.address.town, 'Must not send town');
437+
assert(!bPtr.address.postcode, 'Must not send postcode');
438+
assert(!bPtr.address.surveillance, 'Must not send secure surveillance field');
439+
});
440+
441+
});
442+
251443
describe('data update', function() {
252444

253445
var id;

0 commit comments

Comments
 (0)