soajs.controller 4.3.18 → 4.3.20

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.
Files changed (33) hide show
  1. package/.github/workflows/ci.yml +4 -1
  2. package/.github/workflows/release.yml +4 -1
  3. package/docs/maintenance-routes.md +31 -0
  4. package/docs/middleware/cache.md +74 -1
  5. package/docs/middleware/idempotency.md +31 -2
  6. package/mw/cache/index.js +24 -5
  7. package/mw/cache/invalidate.js +71 -0
  8. package/mw/cache/model/mongo.js +95 -1
  9. package/mw/gotoService/redirectToService.js +21 -9
  10. package/mw/idempotency/index.js +42 -4
  11. package/mw/idempotency/model/memory.js +12 -2
  12. package/mw/idempotency/model/mongo.js +10 -3
  13. package/package.json +1 -1
  14. package/server/maintenance.js +41 -0
  15. package/test/data/provision/customRegistry/gateway.js +47 -0
  16. package/test/data/provision/environment/dev.js +9 -0
  17. package/test/data/provision/hosts/idempotent.js +12 -0
  18. package/test/data/provision/hosts/spaces.js +12 -0
  19. package/test/data/provision/marketplace/idempotent.js +37 -0
  20. package/test/data/provision/marketplace/spaces.js +36 -0
  21. package/test/data/provision/oauth/token/second_owner_token.js +72 -0
  22. package/test/data/provision/products/dsbrd.js +49 -0
  23. package/test/data/provision/urac/users/second_owner.js +61 -0
  24. package/test/integration/UC6-idempotency/index.js +197 -0
  25. package/test/integration/UC7-cache-invalidate/index.js +209 -0
  26. package/test/integration/index.js +8 -0
  27. package/test/unit/index.js +3 -0
  28. package/test/unit/mw/cache/index.js +122 -0
  29. package/test/unit/mw/cache/invalidate.js +99 -0
  30. package/test/unit/mw/cache/mongo.js +104 -0
  31. package/test/unit/mw/idempotency/index.js +122 -0
  32. package/test/unit/mw/idempotency/memory.js +36 -4
  33. package/test/unit/mw/idempotency/mongo.js +62 -0
@@ -75,7 +75,7 @@ let model = {
75
75
  return entry;
76
76
  },
77
77
 
78
- 'lock': (key, ttl) => {
78
+ 'lock': (key, ttl, lockId) => {
79
79
  if (!dataHolder[key.l1]) {
80
80
  dataHolder[key.l1] = {};
81
81
  }
@@ -85,6 +85,7 @@ let model = {
85
85
  }
86
86
  dataHolder[key.l1][key.l2] = {
87
87
  'status': 'in_flight',
88
+ 'lockId': lockId || null,
88
89
  'lockedAt': Date.now(),
89
90
  'expiresAt': Date.now() + ttl
90
91
  };
@@ -103,8 +104,17 @@ let model = {
103
104
  };
104
105
  },
105
106
 
106
- 'unlock': (key) => {
107
+ //NOTE: only an entry still in flight is removed, never a completed one, and with a lockId only
108
+ // the lock of that request, not one taken by a retry after the ttl
109
+ 'unlock': (key, lockId) => {
107
110
  if (dataHolder[key.l1] && dataHolder[key.l1][key.l2]) {
111
+ const entry = dataHolder[key.l1][key.l2];
112
+ if (entry.status !== 'in_flight') {
113
+ return;
114
+ }
115
+ if (lockId && entry.lockId !== lockId) {
116
+ return;
117
+ }
108
118
  delete dataHolder[key.l1][key.l2];
109
119
  }
110
120
  },
@@ -58,7 +58,7 @@ let model = {
58
58
  }
59
59
  },
60
60
 
61
- 'lock': async (key, ttl) => {
61
+ 'lock': async (key, ttl, lockId) => {
62
62
  let condition = {
63
63
  'l1': key.l1,
64
64
  'l2': key.l2
@@ -73,6 +73,7 @@ let model = {
73
73
  'l1': key.l1,
74
74
  'l2': key.l2,
75
75
  'status': 'in_flight',
76
+ 'lockId': lockId || null,
76
77
  'lockedAt': new Date(),
77
78
  'expiresAt': new Date(Date.now() + ttl)
78
79
  };
@@ -108,11 +109,17 @@ let model = {
108
109
  }
109
110
  },
110
111
 
111
- 'unlock': async (key) => {
112
+ //NOTE: only a record still in flight is removed, never a completed one, and with a lockId only
113
+ // the lock of that request, not one taken by a retry after the ttl
114
+ 'unlock': async (key, lockId) => {
112
115
  let condition = {
113
116
  'l1': key.l1,
114
- 'l2': key.l2
117
+ 'l2': key.l2,
118
+ 'status': 'in_flight'
115
119
  };
120
+ if (lockId) {
121
+ condition.lockId = lockId;
122
+ }
116
123
  try {
117
124
  await model.mongo.deleteOne(idempotency_store, condition);
118
125
  } catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soajs.controller",
3
3
  "description": "soajs multi tenant API gateway",
4
- "version": "4.3.18",
4
+ "version": "4.3.20",
5
5
  "author": {
6
6
  "name": "soajs team",
7
7
  "email": "team@soajs.org"
@@ -11,6 +11,7 @@
11
11
  const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
12
12
 
13
13
  const registryModule = require("./../modules/registry");
14
+ const cacheInvalidate = require("./../mw/cache/invalidate.js");
14
15
 
15
16
  const httpProxy = require('http-proxy');
16
17
  const http = require('http');
@@ -311,6 +312,46 @@ let Maintenance = (core, log, param, serviceIp, regEnvironment, awareness_mw, so
311
312
  };
312
313
  proxy.web(req, res, {target: haTarget});
313
314
  }
315
+ else if (parsedUrl.pathname === '/cache/invalidate') {
316
+ let response = maintenanceResponse(parsedUrl, param);
317
+ let reply = (status, extra) => {
318
+ res.writeHead(status, {'Content-Type': 'application/json'});
319
+ return res.end(JSON.stringify(Object.assign(response, extra)));
320
+ };
321
+ if (req.method !== 'POST') {
322
+ return reply(405, {"error": "Use POST."});
323
+ }
324
+ let body = "";
325
+ let tooLarge = false;
326
+ req.on('data', function (chunk) {
327
+ body += chunk;
328
+ //NOTE: the body only names what to remove, anything larger is not a valid request
329
+ if (body.length > 16384) {
330
+ tooLarge = true;
331
+ req.destroy();
332
+ }
333
+ });
334
+ req.on('end', function () {
335
+ if (tooLarge) {
336
+ return;
337
+ }
338
+ let input = null;
339
+ try {
340
+ input = body ? JSON.parse(body) : null;
341
+ } catch (e) {
342
+ return reply(400, {"error": "The body is not valid JSON."});
343
+ }
344
+ cacheInvalidate(registryModule.get(), input).then((outcome) => {
345
+ if (outcome.status !== 200) {
346
+ return reply(outcome.status, {"error": outcome.error});
347
+ }
348
+ log.info("Cache invalidated for tenant [" + input.tenant + "] service [" + input.service + "]" +
349
+ (input.api ? " api [" + input.api + "]" : "") + (input.user ? " user [" + input.user + "]" : "") +
350
+ ", removed [" + outcome.removed + "]");
351
+ return reply(200, {"result": true, "data": {"removed": outcome.removed}});
352
+ });
353
+ });
354
+ }
314
355
  else if (parsedUrl.pathname === '/loadProvision') {
315
356
  provision.loadProvision(function (loaded) {
316
357
  let response = maintenanceResponse(parsedUrl, param);
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ let doc = {
3
+ "_id": "5d7f8e43edf21900fed08cf7",
4
+ "name": "gateway",
5
+ "locked": false,
6
+ "plugged": true,
7
+ "shared": false,
8
+ "value": {
9
+ "idempotency": {
10
+ "model": "mongo",
11
+ "services": {
12
+ "idempotent": {
13
+ "enabled": true,
14
+ "ttl": 60000,
15
+ "apis": ["POST /ok", "POST /fail", "POST /bad", "POST /slow", "POST /throttled"]
16
+ }
17
+ }
18
+ },
19
+ "cache": {
20
+ "model": "mongo",
21
+ "defaultTTL": 60000,
22
+ "services": {
23
+ "spaces": {
24
+ "enabled": true,
25
+ "apis": {
26
+ "GET /spaces": { "enabled": true, "scope": "tenant_user" },
27
+ "GET /space/:id": { "enabled": true, "scope": "tenant_user" },
28
+ "GET /slow": { "enabled": true, "scope": "tenant_user" },
29
+ "GET /catalog": { "enabled": true, "scope": "tenant" }
30
+ }
31
+ }
32
+ }
33
+ },
34
+ "traffic": {
35
+ "throttling": {
36
+ "idempotent": {
37
+ "publicAPIStrategy": "idempotent_tight",
38
+ "apis": ["/throttled"]
39
+ }
40
+ }
41
+ }
42
+ },
43
+ "created": "DEV",
44
+ "author": "owner"
45
+ };
46
+
47
+ module.exports = doc;
@@ -115,6 +115,15 @@ let env = {
115
115
  "accessTokenLifetime": 7200,
116
116
  "refreshTokenLifetime": 1209600
117
117
  },
118
+ "throttling": {
119
+ "idempotent_tight": {
120
+ "type": 1,
121
+ "window": 60000,
122
+ "limit": 1,
123
+ "retries": 0,
124
+ "delay": 100
125
+ }
126
+ },
118
127
  "ports": {
119
128
  "controller": 4000,
120
129
  "maintenanceInc": 1001,
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ let host = {
4
+ _id: "5da078e22357684b57effcf3",
5
+ env: "dev",
6
+ ip: "127.0.0.1",
7
+ name: "idempotent",
8
+ version: "1",
9
+ port: 4014
10
+ };
11
+
12
+ module.exports = host;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ let host = {
4
+ _id: "5da078e22357684b57effcf5",
5
+ env: "dev",
6
+ ip: "127.0.0.1",
7
+ name: "spaces",
8
+ version: "1",
9
+ port: 4015
10
+ };
11
+
12
+ module.exports = host;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ let service = {
4
+ _id: "5da078c92357684b57effcf2",
5
+ "type": "service",
6
+ name: "idempotent",
7
+ configuration: {
8
+ group: "SOAJS test",
9
+ port: 4014,
10
+ requestTimeout: 30,
11
+ requestTimeoutRenewal: 5
12
+ },
13
+ versions: [{
14
+ "version": "1",
15
+ maintenance: {
16
+ "port": {
17
+ "type": "maintenance"
18
+ },
19
+ "readiness": "/heartbeat"
20
+ },
21
+ oauth: true,
22
+ extKeyRequired: true,
23
+ urac: true,
24
+ urac_Profile: false,
25
+ urac_ACL: false,
26
+ provision_ACL: false,
27
+ apis: [
28
+ { l: "Answers 200", v: "/ok", m: "post", group: "Integration test" },
29
+ { l: "Answers 503 the first time", v: "/fail", m: "post", group: "Integration test" },
30
+ { l: "Answers 400", v: "/bad", m: "post", group: "Integration test" },
31
+ { l: "Answers 200 after a delay", v: "/slow", m: "post", group: "Integration test" },
32
+ { l: "Answers 200, throttled", v: "/throttled", m: "post", group: "Integration test" }
33
+ ]
34
+ }
35
+ ]
36
+ };
37
+ module.exports = service;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ let service = {
4
+ _id: "5da078c92357684b57effcf4",
5
+ "type": "service",
6
+ name: "spaces",
7
+ configuration: {
8
+ group: "SOAJS test",
9
+ port: 4015,
10
+ requestTimeout: 30,
11
+ requestTimeoutRenewal: 5
12
+ },
13
+ versions: [{
14
+ "version": "1",
15
+ maintenance: {
16
+ "port": {
17
+ "type": "maintenance"
18
+ },
19
+ "readiness": "/heartbeat"
20
+ },
21
+ oauth: true,
22
+ extKeyRequired: true,
23
+ urac: true,
24
+ urac_Profile: false,
25
+ urac_ACL: false,
26
+ provision_ACL: false,
27
+ apis: [
28
+ { l: "Spaces of the user", v: "/spaces", m: "get", group: "Integration test" },
29
+ { l: "One space", v: "/space/:id", m: "get", group: "Integration test" },
30
+ { l: "Spaces after a delay", v: "/slow", m: "get", group: "Integration test" },
31
+ { l: "Same for the whole tenant", v: "/catalog", m: "get", group: "Integration test" }
32
+ ]
33
+ }
34
+ ]
35
+ };
36
+ module.exports = service;
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ let token = {
4
+ _id: "5cdc190fd52c82e0ddb1dcd8",
5
+ type: "accessToken",
6
+ token: "c4c4e5e5f6f6a7a7b8b8c9c9d0d0e1e1f2f2a301",
7
+ clientId: "5c0e74ba9acc3c5a84a51259",
8
+ user: {
9
+ _id: "5c8d0c505653de3985aa2001",
10
+ locked: true,
11
+ username: "second_owner",
12
+ firstName: "owner3'sèį",
13
+ lastName: "owner",
14
+ email: "me@localhost.com",
15
+ "ts": new Date().getTime(),
16
+ status: "active",
17
+ profile: {},
18
+ groups: [
19
+ "owner"
20
+ ],
21
+ config: {
22
+ packages: {},
23
+ keys: {},
24
+ allowedTenants: [
25
+ {
26
+ tenant: {
27
+ id: "THYME_tID",
28
+ code: "THYME_CODE",
29
+ pin: {
30
+ code: "5678",
31
+ allowed: true
32
+ }
33
+ },
34
+ groups: [
35
+ "waiter"
36
+ ]
37
+ },
38
+ {
39
+ tenant: {
40
+ id: "ELVIRA_tID",
41
+ code: "ELVIRA_CODE"
42
+ },
43
+ groups: [
44
+ "manager"
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ tenant: {
50
+ id: "5c0e74ba9acc3c5a84a51259",
51
+ code: "DBTN",
52
+ pin: {
53
+ code: "1235",
54
+ allowed: true
55
+ }
56
+ },
57
+ lastLogin: new Date().getTime(),
58
+ groupsConfig: {
59
+ allowedPackages: {
60
+ DSBRD: [
61
+ "DSBRD_OWNER"
62
+ ]
63
+ },
64
+ allowedEnvironments: {}
65
+ },
66
+ loginMode: "urac",
67
+ id: "5c8d0c505653de3985aa2001"
68
+ },
69
+ env: "dev",
70
+ expires: new Date((new Date().getFullYear()) + 2, 0, 1)
71
+ };
72
+ module.exports = token;
@@ -9,6 +9,39 @@ let prod = {
9
9
  "scope": {
10
10
  "acl": {
11
11
  "dev": {
12
+ "spaces": {
13
+ "1": {
14
+ "access": true,
15
+ "get": [
16
+ {
17
+ "group": "Spaces group",
18
+ "apis": {
19
+ "/spaces": {},
20
+ "/space/:id": {},
21
+ "/slow": {},
22
+ "/catalog": {}
23
+ }
24
+ }
25
+ ]
26
+ }
27
+ },
28
+ "idempotent": {
29
+ "1": {
30
+ "access": true,
31
+ "post": [
32
+ {
33
+ "group": "Idempotent group",
34
+ "apis": {
35
+ "/ok": {},
36
+ "/fail": {},
37
+ "/bad": {},
38
+ "/slow": {},
39
+ "/throttled": {}
40
+ }
41
+ }
42
+ ]
43
+ }
44
+ },
12
45
  "httpmethods": {
13
46
  "1": {
14
47
  "access": true,
@@ -200,6 +233,22 @@ let prod = {
200
233
  "locked": true,
201
234
  "acl": {
202
235
  "dev": {
236
+ "spaces": [
237
+ {
238
+ "version": "1",
239
+ "get": [
240
+ "Spaces group"
241
+ ]
242
+ }
243
+ ],
244
+ "idempotent": [
245
+ {
246
+ "version": "1",
247
+ "post": [
248
+ "Idempotent group"
249
+ ]
250
+ }
251
+ ],
203
252
  "httpmethods": [
204
253
  {
205
254
  "version": "1",
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+ let user = {
3
+ _id: "5c8d0c505653de3985aa2001",
4
+ locked: true,
5
+ username: "second_owner",
6
+ password: "$2a$12$geJJfv33wkYIXEAlDkeeuOgiQ6y6MjP/YxbqLdHdDSK7LDG.7n7Pq",
7
+ firstName: "owner3'sèį",
8
+ lastName: "owner",
9
+ email: "second_owner@localhost.com",
10
+ ts: 1552747600152,
11
+ status: "active",
12
+ profile: {
13
+
14
+ },
15
+ groups: [
16
+ "owner"
17
+ ],
18
+ config: {
19
+ packages: {
20
+
21
+ },
22
+ keys: {
23
+
24
+ },
25
+ allowedTenants: [
26
+ {
27
+ tenant: {
28
+ id: "THYME_tID",
29
+ code: "THYME_CODE",
30
+ pin: {
31
+ code: "5678",
32
+ allowed: true
33
+ }
34
+ },
35
+ groups: [
36
+ "waiter"
37
+ ]
38
+ },
39
+ {
40
+ tenant: {
41
+ id: "ELVIRA_tID",
42
+ code: "ELVIRA_CODE"
43
+ },
44
+ groups: [
45
+ "manager"
46
+ ]
47
+ }
48
+ ]
49
+ },
50
+ tenant: {
51
+ id: "5c0e74ba9acc3c5a84a51259",
52
+ code: "DBTN",
53
+ pin: {
54
+ code: "1235",
55
+ allowed: true
56
+ }
57
+ },
58
+ lastLogin: 1559836949372
59
+ };
60
+
61
+ module.exports = user;