meadow-endpoints 2.0.12 → 3.0.0
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/Dockerfile +32 -0
- package/README.md +30 -1
- package/{DebugHarness.js → debug/Harness.js} +0 -0
- package/package.json +27 -7
- package/source/Meadow-Authenticator.js +21 -11
- package/source/Meadow-Authorizers.js +23 -7
- package/source/Meadow-CommonServices.js +20 -15
- package/source/Meadow-Endpoints.js +58 -45
- package/source/Meadow-MarshallSessionData.js +64 -0
- package/source/crud/Meadow-Endpoint-Count.js +1 -1
- package/source/crud/Meadow-Endpoint-ReadDistinctList.js +146 -0
- package/source/crud/Meadow-Endpoint-ReadLiteList.js +8 -2
- package/source/crud/Meadow-Endpoint-ReadSelectList.js +6 -1
- package/source/crud/Meadow-Endpoint-Reads.js +6 -1
- package/source/crud/Meadow-Endpoint-ReadsBy.js +5 -0
- package/source/crud/Meadow-Marshal-DistinctList.js +55 -0
- package/test/MeadowEndpoints_basic_tests.js +80 -4
- package/test/MeadowEndpoints_disabledAuth_tests.js +1325 -0
- package/test/MeadowEndpoints_trustedSession_tests.js +1731 -0
- package/.travis.yml +0 -21
- package/0001-Don-t-call-response.send-on-huge-record-sets.-Use-JS.patch +0 -209
- package/0001-Register-wildcard-matches-that-have-parameter-count-.patch +0 -38
- package/0002-Bump-package-version.patch +0 -24
- package/0002-Fix-indent.patch +0 -47
- package/0003-Bump-package-version-for-publication.patch +0 -24
- package/cloud9setup.sh +0 -25
- package/source/crud/Meadow-Filter-Parse.js +0 -206
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Endpoint - Read a list of Records with a specified set of columns, distinct by those columns.
|
|
3
|
+
*
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @author Alex Decker <alex.decker@headlight.com>
|
|
7
|
+
* @module Meadow
|
|
8
|
+
*/
|
|
9
|
+
const libAsync = require('async');
|
|
10
|
+
const meadowFilterParser = require('meadow-filter').parse;
|
|
11
|
+
const marshalDistinctList = require('./Meadow-Marshal-DistinctList.js');
|
|
12
|
+
const streamRecordsToResponse = require('./Meadow-StreamRecordArray');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a set of records from a DAL.
|
|
16
|
+
*/
|
|
17
|
+
const doAPIReadDistinctEndpoint = function(pRequest, pResponse, fNext)
|
|
18
|
+
{
|
|
19
|
+
// This state is the requirement for the UserRoleIndex value in the UserSession object... processed by default as >=
|
|
20
|
+
// The default here is that any authenticated user can use this endpoint.
|
|
21
|
+
pRequest.EndpointAuthorizationRequirement = pRequest.EndpointAuthorizationLevels.Reads;
|
|
22
|
+
|
|
23
|
+
// INJECT: Pre authorization (for instance to change the authorization level)
|
|
24
|
+
|
|
25
|
+
if (pRequest.CommonServices.authorizeEndpoint(pRequest, pResponse, fNext) === false)
|
|
26
|
+
{
|
|
27
|
+
// If this endpoint fails, it's sent an error automatically.
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let tmpDistinctColumns;
|
|
32
|
+
libAsync.waterfall(
|
|
33
|
+
[
|
|
34
|
+
// 1a. Get the records
|
|
35
|
+
function (fStageComplete)
|
|
36
|
+
{
|
|
37
|
+
pRequest.Query = pRequest.DAL.query.setDistinct(true);
|
|
38
|
+
// TODO: Limit the query to the columns we need for the templated expression
|
|
39
|
+
|
|
40
|
+
let tmpCap = false;
|
|
41
|
+
let tmpBegin = false;
|
|
42
|
+
if (typeof(pRequest.params.Begin) === 'string' ||
|
|
43
|
+
typeof(pRequest.params.Begin) === 'number')
|
|
44
|
+
{
|
|
45
|
+
tmpBegin = parseInt(pRequest.params.Begin, 10);
|
|
46
|
+
}
|
|
47
|
+
if (typeof(pRequest.params.Cap) === 'string' ||
|
|
48
|
+
typeof(pRequest.params.Cap) === 'number')
|
|
49
|
+
{
|
|
50
|
+
tmpCap = parseInt(pRequest.params.Cap, 10);
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
{
|
|
54
|
+
//maximum number of records to return by default on Read queries. Override via "MeadowDefaultMaxCap" fable setting.
|
|
55
|
+
tmpCap = pRequest.DEFAULT_MAX_CAP;
|
|
56
|
+
}
|
|
57
|
+
pRequest.Query.setCap(tmpCap).setBegin(tmpBegin);
|
|
58
|
+
if (typeof(pRequest.params.Filter) === 'string')
|
|
59
|
+
{
|
|
60
|
+
// If a filter has been passed in, parse it and add the values to the query.
|
|
61
|
+
meadowFilterParser(pRequest.params.Filter, pRequest.Query);
|
|
62
|
+
}
|
|
63
|
+
else if (pRequest.params.Filter)
|
|
64
|
+
{
|
|
65
|
+
pRequest.Query.setFilter(pRequest.params.Filter);
|
|
66
|
+
}
|
|
67
|
+
if (typeof(pRequest.params.Columns) === 'string')
|
|
68
|
+
{
|
|
69
|
+
tmpDistinctColumns = pRequest.params.Columns.split(',');
|
|
70
|
+
if (!tmpDistinctColumns)
|
|
71
|
+
{
|
|
72
|
+
return fStageComplete({Code:400,Message:'Columns to distinct on must be provided.'});
|
|
73
|
+
}
|
|
74
|
+
pRequest.Query.setDataElements(tmpDistinctColumns);
|
|
75
|
+
}
|
|
76
|
+
fStageComplete(false);
|
|
77
|
+
},
|
|
78
|
+
// 1b. INJECT: Query configuration
|
|
79
|
+
function (fStageComplete)
|
|
80
|
+
{
|
|
81
|
+
pRequest.BehaviorModifications.runBehavior('Reads-QueryConfiguration', pRequest, fStageComplete);
|
|
82
|
+
},
|
|
83
|
+
// 1b2. INJECT: Query pre-authorization behavior (ex. if authorizer needs fields to be included, it can add them)
|
|
84
|
+
function (fStageComplete)
|
|
85
|
+
{
|
|
86
|
+
pRequest.BehaviorModifications.runBehavior('Reads-PreAuth', pRequest, fStageComplete);
|
|
87
|
+
},
|
|
88
|
+
// 1c. Do the record read
|
|
89
|
+
function (fStageComplete)
|
|
90
|
+
{
|
|
91
|
+
pRequest.DAL.doReads(pRequest.Query, fStageComplete);
|
|
92
|
+
},
|
|
93
|
+
// 2. Post processing of the records
|
|
94
|
+
function (pQuery, pRecords, fStageComplete)
|
|
95
|
+
{
|
|
96
|
+
if (pRecords.length < 1)
|
|
97
|
+
{
|
|
98
|
+
pRecords = [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pRequest.Records = pRecords;
|
|
102
|
+
|
|
103
|
+
// Complete the waterfall operation
|
|
104
|
+
fStageComplete(false);
|
|
105
|
+
},
|
|
106
|
+
// 2.5: Check if there is an authorizer set for this endpoint and user role combination, and authorize based on that
|
|
107
|
+
function (fStageComplete)
|
|
108
|
+
{
|
|
109
|
+
// shared permission with reads
|
|
110
|
+
pRequest.Authorizers.authorizeRequest('Reads', pRequest, fStageComplete);
|
|
111
|
+
},
|
|
112
|
+
// 2.6: Check if authorization or post processing denied security access to the record
|
|
113
|
+
function (fStageComplete)
|
|
114
|
+
{
|
|
115
|
+
if (pRequest.MeadowAuthorization)
|
|
116
|
+
{
|
|
117
|
+
return fStageComplete(false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// It looks like this record was not authorized. Send an error.
|
|
121
|
+
return fStageComplete({Code:405,Message:'UNAUTHORIZED ACCESS IS NOT ALLOWED'});
|
|
122
|
+
},
|
|
123
|
+
// 3. Marshalling of records into the hash list, using underscore templates.
|
|
124
|
+
function (fStageComplete)
|
|
125
|
+
{
|
|
126
|
+
fStageComplete(false, marshalDistinctList(pRequest.Records, pRequest, tmpDistinctColumns));
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
// 3. Return the results to the user
|
|
130
|
+
function(pError, pResultRecords)
|
|
131
|
+
{
|
|
132
|
+
// Remove 'Records' object from pRequest, instead return template results (pResultRecords) for the records
|
|
133
|
+
delete pRequest['Records'];
|
|
134
|
+
|
|
135
|
+
if (pError)
|
|
136
|
+
{
|
|
137
|
+
return pRequest.CommonServices.sendCodedError('Error retreiving a recordset.', pError, pRequest, pResponse, fNext);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pRequest.CommonServices.log.info('Read a recordset lite list with '+pResultRecords.length+' results.', {SessionID:pRequest.UserSession.SessionID, RequestID:pRequest.RequestUUID, RequestURL:pRequest.url, Action:pRequest.DAL.scope+'-ReadDistinct'}, pRequest);
|
|
141
|
+
return streamRecordsToResponse(pResponse, pResultRecords, fNext);
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports = doAPIReadDistinctEndpoint;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module Meadow
|
|
8
8
|
*/
|
|
9
9
|
var libAsync = require('async');
|
|
10
|
-
|
|
10
|
+
const meadowFilterParser = require('meadow-filter').parse;
|
|
11
11
|
var marshalLiteList = require('./Meadow-Marshal-LiteList.js');
|
|
12
12
|
const streamRecordsToResponse = require('./Meadow-StreamRecordArray');
|
|
13
13
|
|
|
@@ -71,6 +71,11 @@ var doAPIReadLiteEndpoint = function(pRequest, pResponse, fNext)
|
|
|
71
71
|
{
|
|
72
72
|
pRequest.BehaviorModifications.runBehavior('Reads-QueryConfiguration', pRequest, fStageComplete);
|
|
73
73
|
},
|
|
74
|
+
// 1b2. INJECT: Query pre-authorization behavior (ex. if authorizer needs fields to be included, it can add them)
|
|
75
|
+
function (fStageComplete)
|
|
76
|
+
{
|
|
77
|
+
pRequest.BehaviorModifications.runBehavior('Reads-PreAuth', pRequest, fStageComplete);
|
|
78
|
+
},
|
|
74
79
|
// 1c. Do the record read
|
|
75
80
|
function (fStageComplete)
|
|
76
81
|
{
|
|
@@ -92,7 +97,8 @@ var doAPIReadLiteEndpoint = function(pRequest, pResponse, fNext)
|
|
|
92
97
|
// 2.5: Check if there is an authorizer set for this endpoint and user role combination, and authorize based on that
|
|
93
98
|
function (fStageComplete)
|
|
94
99
|
{
|
|
95
|
-
|
|
100
|
+
// shared permission with reads
|
|
101
|
+
pRequest.Authorizers.authorizeRequest('Reads', pRequest, fStageComplete);
|
|
96
102
|
},
|
|
97
103
|
// 2.6: Check if authorization or post processing denied security access to the record
|
|
98
104
|
function (fStageComplete)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module Meadow
|
|
8
8
|
*/
|
|
9
9
|
var libAsync = require('async');
|
|
10
|
-
|
|
10
|
+
const meadowFilterParser = require('meadow-filter').parse;
|
|
11
11
|
const streamRecordsToResponse = require('./Meadow-StreamRecordArray');
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -66,6 +66,11 @@ var doAPIReadSelectListEndpoint = function(pRequest, pResponse, fNext)
|
|
|
66
66
|
{
|
|
67
67
|
pRequest.BehaviorModifications.runBehavior('Reads-QueryConfiguration', pRequest, fStageComplete);
|
|
68
68
|
},
|
|
69
|
+
// 1b2. INJECT: Query pre-authorization behavior (ex. if authorizer needs fields to be included, it can add them)
|
|
70
|
+
function (fStageComplete)
|
|
71
|
+
{
|
|
72
|
+
pRequest.BehaviorModifications.runBehavior('Reads-PreAuth', pRequest, fStageComplete);
|
|
73
|
+
},
|
|
69
74
|
// 1c. Do the record read
|
|
70
75
|
function (fStageComplete)
|
|
71
76
|
{
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module Meadow
|
|
8
8
|
*/
|
|
9
9
|
var libAsync = require('async');
|
|
10
|
-
|
|
10
|
+
const meadowFilterParser = require('meadow-filter').parse;
|
|
11
11
|
const streamRecordsToResponse = require('./Meadow-StreamRecordArray');
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -67,6 +67,11 @@ var doAPIReadsEndpoint = function(pRequest, pResponse, fNext)
|
|
|
67
67
|
{
|
|
68
68
|
pRequest.BehaviorModifications.runBehavior('Reads-QueryConfiguration', pRequest, fStageComplete);
|
|
69
69
|
},
|
|
70
|
+
// 2b. INJECT: Query pre-authorization behavior (ex. if authorizer needs fields to be included, it can add them)
|
|
71
|
+
function (fStageComplete)
|
|
72
|
+
{
|
|
73
|
+
pRequest.BehaviorModifications.runBehavior('Reads-PreAuth', pRequest, fStageComplete);
|
|
74
|
+
},
|
|
70
75
|
// 3. Execute the query
|
|
71
76
|
function (fStageComplete)
|
|
72
77
|
{
|
|
@@ -94,6 +94,11 @@ var doAPIReadsByEndpoint = function(pRequest, pResponse, fNext)
|
|
|
94
94
|
{
|
|
95
95
|
pRequest.BehaviorModifications.runBehavior('Reads-QueryConfiguration', pRequest, fStageComplete);
|
|
96
96
|
},
|
|
97
|
+
// 3b. INJECT: Query pre-authorization behavior (ex. if authorizer needs fields to be included, it can add them)
|
|
98
|
+
function (fStageComplete)
|
|
99
|
+
{
|
|
100
|
+
pRequest.BehaviorModifications.runBehavior('Reads-PreAuth', pRequest, fStageComplete);
|
|
101
|
+
},
|
|
97
102
|
// 4. Execute the query
|
|
98
103
|
function (fStageComplete)
|
|
99
104
|
{
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Operation - Marshal an array of records into a lite list
|
|
3
|
+
*
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
7
|
+
* @module Meadow
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Shared record marshaling code
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var marshalDistinctList = (pRecords, pRequest, pFieldList) =>
|
|
14
|
+
{
|
|
15
|
+
if (pRecords.length < 1)
|
|
16
|
+
return [];
|
|
17
|
+
|
|
18
|
+
let tmpDistinctList = [];
|
|
19
|
+
// Allow the caller to pass in a list of fields.
|
|
20
|
+
let tmpFieldList = (typeof(pFieldList) !== 'undefined') ? pFieldList : [];
|
|
21
|
+
|
|
22
|
+
// See if this record has a GUID in the schema
|
|
23
|
+
let tmpGUID = (pRequest.DAL.defaultGUIdentifier && pRequest.DAL.defaultGUIdentifier.length > 0) ? pRequest.DAL.defaultGUIdentifier : false;
|
|
24
|
+
// Peek at the first record to check for updatedate
|
|
25
|
+
let tmpHasUpdateDate = (pRecords[0].hasOwnProperty('UpdateDate')) ? true : false;
|
|
26
|
+
//Include all GUID and ID fields on the record
|
|
27
|
+
let tmpRecordFields = Object.keys(pRecords[0]);
|
|
28
|
+
|
|
29
|
+
let h = 0;
|
|
30
|
+
while (h < tmpFieldList.length)
|
|
31
|
+
{
|
|
32
|
+
// Remove any fields in the list that aren't in the first record.
|
|
33
|
+
if (!pRecords[0].hasOwnProperty(tmpFieldList[0]))
|
|
34
|
+
tmpFieldList.splice(h, 1);
|
|
35
|
+
else
|
|
36
|
+
h++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < pRecords.length; i++)
|
|
40
|
+
{
|
|
41
|
+
let tmpDistinctRecord = { };
|
|
42
|
+
|
|
43
|
+
tmpFieldList.forEach(
|
|
44
|
+
(pField) =>
|
|
45
|
+
{
|
|
46
|
+
tmpDistinctRecord[pField] = pRecords[i][pField];
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
tmpDistinctList.push(tmpDistinctRecord);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return tmpDistinctList;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
module.exports = marshalDistinctList;
|
|
@@ -22,6 +22,9 @@ var tmpFableSettings = (
|
|
|
22
22
|
|
|
23
23
|
"UnauthorizedRequestDelay": 10,
|
|
24
24
|
|
|
25
|
+
MeadowAuthenticationMode: 'LoggedIn',
|
|
26
|
+
MeadowAuthorizationMode: 'SimpleOwnership',
|
|
27
|
+
|
|
25
28
|
APIServerPort: 9080,
|
|
26
29
|
|
|
27
30
|
MySQL:
|
|
@@ -40,7 +43,6 @@ var tmpFableSettings = (
|
|
|
40
43
|
var libFable = require('fable').new(tmpFableSettings);
|
|
41
44
|
tmpFableSettings = libFable.settings;
|
|
42
45
|
|
|
43
|
-
var libMySQL = require('mysql2');
|
|
44
46
|
libFable.MeadowMySQLConnectionPool = libMySQL.createPool
|
|
45
47
|
(
|
|
46
48
|
{
|
|
@@ -807,7 +809,6 @@ suite
|
|
|
807
809
|
function(fDone)
|
|
808
810
|
{
|
|
809
811
|
libSuperTest('http://localhost:9080/')
|
|
810
|
-
// Get page 2, 2 records per page.
|
|
811
812
|
.get('1.0/FableTests/FilteredTo/FBV~Type~EQ~Frog')
|
|
812
813
|
.end(
|
|
813
814
|
function (pError, pResponse)
|
|
@@ -819,13 +820,88 @@ suite
|
|
|
819
820
|
}
|
|
820
821
|
);
|
|
821
822
|
}
|
|
822
|
-
);
|
|
823
|
+
);
|
|
824
|
+
test
|
|
825
|
+
(
|
|
826
|
+
'reads: get distinct values for a column',
|
|
827
|
+
function(fDone)
|
|
828
|
+
{
|
|
829
|
+
libSuperTest('http://localhost:9080/')
|
|
830
|
+
.get('1.0/FableTests/Distinct/Type')
|
|
831
|
+
.end(
|
|
832
|
+
function (pError, pResponse)
|
|
833
|
+
{
|
|
834
|
+
var tmpResults = JSON.parse(pResponse.text);
|
|
835
|
+
Expect(tmpResults.length).to.equal(5);
|
|
836
|
+
const types = tmpResults.map((r) => r.Type);
|
|
837
|
+
Expect(types).to.have.members(['Bunny', 'Girl', 'Dog', 'Frog', 'Mammoth']);
|
|
838
|
+
fDone();
|
|
839
|
+
}
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
test
|
|
844
|
+
(
|
|
845
|
+
'reads: get distinct values for a column with filter',
|
|
846
|
+
function(fDone)
|
|
847
|
+
{
|
|
848
|
+
libSuperTest('http://localhost:9080/')
|
|
849
|
+
.get('1.0/FableTests/Distinct/Type/FilteredTo/FBV~IDAnimal~LT~3')
|
|
850
|
+
.end(
|
|
851
|
+
function (pError, pResponse)
|
|
852
|
+
{
|
|
853
|
+
var tmpResults = JSON.parse(pResponse.text);
|
|
854
|
+
Expect(tmpResults.length).to.equal(2);
|
|
855
|
+
const types = new Set(tmpResults.map((r) => r.Type));
|
|
856
|
+
Expect(types.size).to.equal(2);
|
|
857
|
+
fDone();
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
);
|
|
862
|
+
test
|
|
863
|
+
(
|
|
864
|
+
'reads: get distinct values for a column with filter and pagination',
|
|
865
|
+
function(fDone)
|
|
866
|
+
{
|
|
867
|
+
libSuperTest('http://localhost:9080/')
|
|
868
|
+
.get('1.0/FableTests/Distinct/Type/FilteredTo/FBV~IDAnimal~LT~3/0/1')
|
|
869
|
+
.end(
|
|
870
|
+
function (pError, pResponse)
|
|
871
|
+
{
|
|
872
|
+
var tmpResults = JSON.parse(pResponse.text);
|
|
873
|
+
Expect(tmpResults.length).to.equal(1);
|
|
874
|
+
fDone();
|
|
875
|
+
}
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
test
|
|
880
|
+
(
|
|
881
|
+
'reads: get distinct values for a column with pagination',
|
|
882
|
+
function(fDone)
|
|
883
|
+
{
|
|
884
|
+
libSuperTest('http://localhost:9080/')
|
|
885
|
+
.get('1.0/FableTests/Distinct/Type/2/2')
|
|
886
|
+
.end(
|
|
887
|
+
function (pError, pResponse)
|
|
888
|
+
{
|
|
889
|
+
var tmpResults = JSON.parse(pResponse.text);
|
|
890
|
+
Expect(tmpResults.length).to.equal(2);
|
|
891
|
+
const types = new Set(tmpResults.map((r) => r.Type));
|
|
892
|
+
Expect(types.size).to.equal(2);
|
|
893
|
+
fDone();
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
test
|
|
823
899
|
(
|
|
824
900
|
'reads: get a filtered paged set of records',
|
|
825
901
|
function(fDone)
|
|
826
902
|
{
|
|
827
903
|
libSuperTest('http://localhost:9080/')
|
|
828
|
-
//
|
|
904
|
+
// Skip one record, 2 records per page.
|
|
829
905
|
.get('1.0/FableTests/FilteredTo/FBV~Type~EQ~Dog/1/2')
|
|
830
906
|
.end(
|
|
831
907
|
function (pError, pResponse)
|