jinaga-server 2.4.2 → 2.6.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/dist/authorization/authorization-keystore.d.ts +5 -4
- package/dist/authorization/authorization-keystore.js +16 -13
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/http/router.d.ts +5 -3
- package/dist/http/router.js +125 -52
- package/dist/http/router.js.map +1 -1
- package/dist/http/stream.d.ts +10 -0
- package/dist/http/stream.js +35 -0
- package/dist/http/stream.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/jinaga-server.js +7 -8
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/connection.js +1 -3
- package/dist/postgres/connection.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +4 -4
- package/dist/postgres/postgres-store.js +2 -2
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/query-description.d.ts +75 -0
- package/dist/postgres/query-description.js +283 -0
- package/dist/postgres/query-description.js.map +1 -0
- package/dist/postgres/specification-result-sql.d.ts +2 -2
- package/dist/postgres/specification-result-sql.js +39 -324
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.d.ts +2 -2
- package/dist/postgres/specification-sql.js +60 -163
- package/dist/postgres/specification-sql.js.map +1 -1
- package/http/sans-auth/.env.local +1 -0
- package/http/sans-auth/Create post sans auth.http +27 -0
- package/http/sans-auth/Create site sans auth.http +11 -0
- package/http/sans-auth/Feed sites.http +1 -0
- package/http/sans-auth/Feeds sites.http +12 -0
- package/http/sans-auth/Get posts sans auth.http +35 -0
- package/http/sans-auth/Get sites sans auth.http +12 -0
- package/http/sans-auth/stream.sh +2 -0
- package/integration-test/jinaga-test/Dockerfile +1 -1
- package/integration-test/jinaga-test/package-lock.json +917 -748
- package/integration-test/jinaga-test/package.json +3 -3
- package/package.json +2 -2
- package/replicator/package-lock.json +16 -16
- package/replicator/package.json +1 -1
- package/src/authorization/authorization-keystore.ts +23 -14
- package/src/http/router.ts +154 -64
- package/src/http/stream.ts +32 -0
- package/src/index.ts +3 -4
- package/src/jinaga-server.ts +8 -8
- package/src/postgres/connection.ts +1 -3
- package/src/postgres/postgres-store.ts +5 -8
- package/src/postgres/query-description.ts +435 -0
- package/src/postgres/specification-result-sql.ts +56 -489
- package/src/postgres/specification-sql.ts +71 -232
- package/test/authorization/authorizationSpec.ts +12 -2
- package/test/postgres/postgresSpecificationSpec.ts +15 -15
- package/dist/http/feed-cache.d.ts +0 -12
- package/dist/http/feed-cache.js +0 -3
- package/dist/http/feed-cache.js.map +0 -1
- package/dist/memory/memory-feed-cache.d.ts +0 -6
- package/dist/memory/memory-feed-cache.js +0 -17
- package/dist/memory/memory-feed-cache.js.map +0 -1
- package/src/http/feed-cache.ts +0 -14
- package/src/memory/memory-feed-cache.ts +0 -14
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@stablelib/base64": "^1.0.1",
|
|
17
17
|
"express": "^4.17.2",
|
|
18
|
-
"jest": "^
|
|
18
|
+
"jest": "^29.7.0",
|
|
19
19
|
"jest-compact-reporter": "^1.2.9",
|
|
20
|
-
"jinaga": "^5.
|
|
20
|
+
"jinaga": "^5.6.0",
|
|
21
21
|
"node-forge": "^1.3.0",
|
|
22
22
|
"passport": "^0.6.0",
|
|
23
|
-
"pg": "^8.11.
|
|
23
|
+
"pg": "^8.11.1",
|
|
24
24
|
"tweetnacl": "^1.0.3",
|
|
25
25
|
"tweetnacl-util": "^0.15.1"
|
|
26
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jinaga-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Data management for web and mobile applications.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"express": "^4.18.2",
|
|
51
|
-
"jinaga": "^5.
|
|
51
|
+
"jinaga": "^5.6.0",
|
|
52
52
|
"node-forge": "^1.3.1",
|
|
53
53
|
"passport": "^0.6.0",
|
|
54
54
|
"pg": "^8.11.0"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"cors": "^2.8.5",
|
|
13
13
|
"express": "^4.18.1",
|
|
14
|
-
"pg": "8.11"
|
|
14
|
+
"pg": "^8.11.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/cors": "^2.8.12",
|
|
@@ -530,14 +530,14 @@
|
|
|
530
530
|
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
|
531
531
|
},
|
|
532
532
|
"node_modules/pg": {
|
|
533
|
-
"version": "8.11.
|
|
534
|
-
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.
|
|
535
|
-
"integrity": "sha512
|
|
533
|
+
"version": "8.11.3",
|
|
534
|
+
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
|
|
535
|
+
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
|
|
536
536
|
"dependencies": {
|
|
537
537
|
"buffer-writer": "2.0.0",
|
|
538
538
|
"packet-reader": "1.0.0",
|
|
539
|
-
"pg-connection-string": "^2.6.
|
|
540
|
-
"pg-pool": "^3.6.
|
|
539
|
+
"pg-connection-string": "^2.6.2",
|
|
540
|
+
"pg-pool": "^3.6.1",
|
|
541
541
|
"pg-protocol": "^1.6.0",
|
|
542
542
|
"pg-types": "^2.1.0",
|
|
543
543
|
"pgpass": "1.x"
|
|
@@ -546,7 +546,7 @@
|
|
|
546
546
|
"node": ">= 8.0.0"
|
|
547
547
|
},
|
|
548
548
|
"optionalDependencies": {
|
|
549
|
-
"pg-cloudflare": "^1.1.
|
|
549
|
+
"pg-cloudflare": "^1.1.1"
|
|
550
550
|
},
|
|
551
551
|
"peerDependencies": {
|
|
552
552
|
"pg-native": ">=3.0.1"
|
|
@@ -558,15 +558,15 @@
|
|
|
558
558
|
}
|
|
559
559
|
},
|
|
560
560
|
"node_modules/pg-cloudflare": {
|
|
561
|
-
"version": "1.1.
|
|
562
|
-
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.
|
|
563
|
-
"integrity": "sha512-
|
|
561
|
+
"version": "1.1.1",
|
|
562
|
+
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
|
563
|
+
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
|
|
564
564
|
"optional": true
|
|
565
565
|
},
|
|
566
566
|
"node_modules/pg-connection-string": {
|
|
567
|
-
"version": "2.6.
|
|
568
|
-
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.
|
|
569
|
-
"integrity": "sha512-
|
|
567
|
+
"version": "2.6.2",
|
|
568
|
+
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
|
|
569
|
+
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
|
|
570
570
|
},
|
|
571
571
|
"node_modules/pg-int8": {
|
|
572
572
|
"version": "1.0.1",
|
|
@@ -577,9 +577,9 @@
|
|
|
577
577
|
}
|
|
578
578
|
},
|
|
579
579
|
"node_modules/pg-pool": {
|
|
580
|
-
"version": "3.6.
|
|
581
|
-
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.
|
|
582
|
-
"integrity": "sha512-
|
|
580
|
+
"version": "3.6.1",
|
|
581
|
+
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
|
|
582
|
+
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
|
|
583
583
|
"peerDependencies": {
|
|
584
584
|
"pg": ">=8.0"
|
|
585
585
|
}
|
package/replicator/package.json
CHANGED
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
DistributionRules,
|
|
7
7
|
FactEnvelope,
|
|
8
8
|
FactFeed,
|
|
9
|
+
FactManager,
|
|
9
10
|
FactRecord,
|
|
10
11
|
FactReference,
|
|
11
|
-
Feed,
|
|
12
12
|
Forbidden,
|
|
13
13
|
Query,
|
|
14
|
+
ReferencesByName,
|
|
14
15
|
Specification,
|
|
15
16
|
Storage,
|
|
16
17
|
UserIdentity,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
} from "jinaga";
|
|
20
21
|
|
|
21
22
|
import { Keystore } from "../keystore";
|
|
22
|
-
import { distinct } from "../util/fn";
|
|
23
23
|
import { DistributedFactCache } from "./distributed-fact-cache";
|
|
24
24
|
|
|
25
25
|
export class AuthorizationKeystore implements Authorization {
|
|
@@ -28,6 +28,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
28
28
|
private distributedFacts: DistributedFactCache = new DistributedFactCache();
|
|
29
29
|
|
|
30
30
|
constructor(
|
|
31
|
+
private factManager: FactManager,
|
|
31
32
|
private store: Storage,
|
|
32
33
|
private keystore: Keystore,
|
|
33
34
|
authorizationRules: AuthorizationRules | null,
|
|
@@ -52,7 +53,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
query(userIdentity: UserIdentity | null, start: FactReference, query: Query) {
|
|
55
|
-
return this.
|
|
56
|
+
return this.factManager.query(start, query);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async read(userIdentity: UserIdentity | null, start: FactReference[], specification: Specification) {
|
|
@@ -60,26 +61,34 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
60
61
|
const userReference: FactReference | null = userIdentity
|
|
61
62
|
? await this.keystore.getUserFact(userIdentity)
|
|
62
63
|
: null;
|
|
64
|
+
const namedStart = specification.given.reduce((map, label, index) => ({
|
|
65
|
+
...map,
|
|
66
|
+
[label.name]: start[index]
|
|
67
|
+
}), {} as ReferencesByName);
|
|
63
68
|
// Break the specification into feeds and check distribution.
|
|
64
69
|
const feeds = buildFeeds(specification);
|
|
65
|
-
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds,
|
|
70
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, namedStart, userReference);
|
|
66
71
|
if (canDistribute.type === "failure") {
|
|
67
72
|
throw new Forbidden(canDistribute.reason);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
|
-
return await this.
|
|
75
|
+
return await this.factManager.read(start, specification);
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
async feed(userIdentity: UserIdentity | null,
|
|
78
|
+
async feed(userIdentity: UserIdentity | null, specification: Specification, start: FactReference[], bookmark: string): Promise<FactFeed> {
|
|
74
79
|
if (this.distributionEngine) {
|
|
75
80
|
const userReference: FactReference | null = userIdentity
|
|
76
81
|
? await this.keystore.getUserFact(userIdentity)
|
|
77
82
|
: null;
|
|
78
|
-
const
|
|
83
|
+
const namedStart = specification.given.reduce((map, label, index) => ({
|
|
84
|
+
...map,
|
|
85
|
+
[label.name]: start[index]
|
|
86
|
+
}), {} as ReferencesByName);
|
|
87
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll([specification], namedStart, userReference);
|
|
79
88
|
if (canDistribute.type === "failure") {
|
|
80
89
|
throw new Forbidden(canDistribute.reason);
|
|
81
90
|
}
|
|
82
|
-
const factFeed = await this.store.feed(
|
|
91
|
+
const factFeed = await this.store.feed(specification, start, bookmark);
|
|
83
92
|
const factReferences = factFeed.tuples
|
|
84
93
|
.flatMap(tuple => tuple.facts)
|
|
85
94
|
.filter((value, index, self) => self.findIndex(factReferenceEquals(value)) === index);
|
|
@@ -87,7 +96,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
87
96
|
return factFeed;
|
|
88
97
|
}
|
|
89
98
|
else {
|
|
90
|
-
return await this.store.feed(
|
|
99
|
+
return await this.store.feed(specification, start, bookmark);
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
|
|
@@ -99,12 +108,12 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
99
108
|
throw new Forbidden("Unauthorized");
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
|
-
return await this.
|
|
111
|
+
return await this.factManager.load(references);
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
async save(userIdentity: UserIdentity | null, facts: FactRecord[]) {
|
|
106
115
|
if (!this.authorizationEngine) {
|
|
107
|
-
const envelopes = await this.
|
|
116
|
+
const envelopes = await this.factManager.save(facts.map(fact => ({
|
|
108
117
|
fact,
|
|
109
118
|
signatures: []
|
|
110
119
|
})));
|
|
@@ -115,7 +124,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
115
124
|
const authorizedFacts = await this.authorizationEngine.authorizeFacts(facts, userFact);
|
|
116
125
|
if (userIdentity) {
|
|
117
126
|
const signedFacts = await this.keystore.signFacts(userIdentity, authorizedFacts);
|
|
118
|
-
const envelopes = await this.
|
|
127
|
+
const envelopes = await this.factManager.save(signedFacts);
|
|
119
128
|
return envelopes.map(envelope => envelope.fact);
|
|
120
129
|
}
|
|
121
130
|
else {
|
|
@@ -123,7 +132,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
async verifyDistribution(userIdentity: UserIdentity | null, feeds:
|
|
135
|
+
async verifyDistribution(userIdentity: UserIdentity | null, feeds: Specification[], namedStart: ReferencesByName): Promise<void> {
|
|
127
136
|
if (!this.distributionEngine) {
|
|
128
137
|
return;
|
|
129
138
|
}
|
|
@@ -131,7 +140,7 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
131
140
|
const userReference: FactReference | null = userIdentity
|
|
132
141
|
? await this.keystore.getUserFact(userIdentity)
|
|
133
142
|
: null;
|
|
134
|
-
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds,
|
|
143
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, namedStart, userReference);
|
|
135
144
|
if (canDistribute.type === "failure") {
|
|
136
145
|
throw new Forbidden(canDistribute.reason);
|
|
137
146
|
}
|
package/src/http/router.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { Handler, Router } from "express";
|
|
|
2
2
|
import {
|
|
3
3
|
Authorization,
|
|
4
4
|
Declaration,
|
|
5
|
+
FactManager,
|
|
5
6
|
FactRecord,
|
|
6
7
|
FactReference,
|
|
7
|
-
|
|
8
|
+
FeedCache,
|
|
9
|
+
FeedObject,
|
|
8
10
|
FeedResponse,
|
|
9
11
|
FeedsResponse,
|
|
10
12
|
Forbidden,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
ProjectedResult,
|
|
15
17
|
QueryMessage,
|
|
16
18
|
QueryResponse,
|
|
19
|
+
ReferencesByName,
|
|
17
20
|
SaveMessage,
|
|
18
21
|
Specification,
|
|
19
22
|
SpecificationParser,
|
|
@@ -21,42 +24,91 @@ import {
|
|
|
21
24
|
UserIdentity,
|
|
22
25
|
buildFeeds,
|
|
23
26
|
computeObjectHash,
|
|
27
|
+
computeTupleSubsetHash,
|
|
24
28
|
fromDescriptiveString,
|
|
29
|
+
invertSpecification,
|
|
25
30
|
parseLoadMessage,
|
|
26
31
|
parseQueryMessage,
|
|
27
|
-
parseSaveMessage
|
|
32
|
+
parseSaveMessage
|
|
28
33
|
} from "jinaga";
|
|
29
34
|
|
|
30
|
-
import {
|
|
35
|
+
import { Stream } from "./stream";
|
|
31
36
|
|
|
32
37
|
interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
|
|
33
38
|
|
|
34
|
-
function
|
|
39
|
+
function getOrStream<U>(
|
|
40
|
+
getMethod: ((req: RequestUser, params: { [key: string]: string }, query: ParsedQs) => Promise<U | null>),
|
|
41
|
+
streamMethod: ((req: RequestUser, params: { [key: string]: string }, query: ParsedQs) => Promise<Stream<U> | null>)
|
|
42
|
+
): Handler {
|
|
35
43
|
return (req, res, next) => {
|
|
36
44
|
const user = <RequestUser>req.user;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const accept = req.headers["accept"];
|
|
46
|
+
if (accept && accept.indexOf("application/x-jinaga-feed-stream") >= 0) {
|
|
47
|
+
streamMethod(user, req.params, req.query)
|
|
48
|
+
.then(response => {
|
|
49
|
+
if (!response) {
|
|
50
|
+
res.sendStatus(404);
|
|
51
|
+
next();
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
res.type("application/x-jinaga-feed-stream");
|
|
55
|
+
res.set("Connection", "keep-alive");
|
|
56
|
+
res.set("Cache-Control", "no-cache");
|
|
57
|
+
res.set("Access-Control-Allow-Origin", "*");
|
|
58
|
+
res.flushHeaders();
|
|
59
|
+
req.on("close", () => {
|
|
60
|
+
response.close();
|
|
61
|
+
});
|
|
62
|
+
const timeout = setTimeout(() => {
|
|
63
|
+
response.close();
|
|
64
|
+
}, 5 * 60 * 1000);
|
|
65
|
+
response
|
|
66
|
+
.next(data => {
|
|
67
|
+
res.write(JSON.stringify(data) + "\n\n");
|
|
68
|
+
})
|
|
69
|
+
.done(() => {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
next();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch(error => {
|
|
76
|
+
if (error instanceof Forbidden) {
|
|
77
|
+
res.type("text");
|
|
78
|
+
res.status(403).send(error.message);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
Trace.error(error);
|
|
82
|
+
res.status(500).send(error.message);
|
|
83
|
+
}
|
|
41
84
|
next();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
getMethod(user, req.params, req.query)
|
|
89
|
+
.then(response => {
|
|
90
|
+
if (!response) {
|
|
91
|
+
res.sendStatus(404);
|
|
92
|
+
next();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
res.type("json");
|
|
96
|
+
res.send(JSON.stringify(response));
|
|
97
|
+
next();
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.catch(error => {
|
|
101
|
+
if (error instanceof Forbidden) {
|
|
102
|
+
res.type("text");
|
|
103
|
+
res.status(403).send(error.message);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
Trace.error(error);
|
|
107
|
+
res.status(500).send(error.message);
|
|
108
|
+
}
|
|
46
109
|
next();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.catch(error => {
|
|
50
|
-
if (error instanceof Forbidden) {
|
|
51
|
-
res.type("text");
|
|
52
|
-
res.status(403).send(error.message);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
Trace.error(error);
|
|
56
|
-
res.status(500).send(error.message);
|
|
57
|
-
}
|
|
58
|
-
next();
|
|
59
|
-
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
60
112
|
};
|
|
61
113
|
}
|
|
62
114
|
|
|
@@ -124,7 +176,7 @@ function postString<U>(method: (user: RequestUser, message: string) => Promise<U
|
|
|
124
176
|
return (req, res, next) => {
|
|
125
177
|
const user = <RequestUser>req.user;
|
|
126
178
|
const input = parseString(req.body);
|
|
127
|
-
if (!input || typeof(input) !== 'string') {
|
|
179
|
+
if (!input || typeof (input) !== 'string') {
|
|
128
180
|
res.type("text");
|
|
129
181
|
res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
|
|
130
182
|
}
|
|
@@ -187,7 +239,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
|
|
|
187
239
|
return (req, res, next) => {
|
|
188
240
|
const user = <RequestUser>req.user;
|
|
189
241
|
const input = parseString(req.body);
|
|
190
|
-
if (!input || typeof(input) !== 'string') {
|
|
242
|
+
if (!input || typeof (input) !== 'string') {
|
|
191
243
|
res.type("text");
|
|
192
244
|
res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
|
|
193
245
|
}
|
|
@@ -214,7 +266,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
|
|
|
214
266
|
};
|
|
215
267
|
}
|
|
216
268
|
|
|
217
|
-
function serializeUserIdentity(user: RequestUser | null)
|
|
269
|
+
function serializeUserIdentity(user: RequestUser | null): UserIdentity | null {
|
|
218
270
|
if (!user) {
|
|
219
271
|
return null;
|
|
220
272
|
}
|
|
@@ -233,7 +285,7 @@ export interface RequestUser {
|
|
|
233
285
|
export class HttpRouter {
|
|
234
286
|
handler: Handler;
|
|
235
287
|
|
|
236
|
-
constructor(private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
|
|
288
|
+
constructor(private factManager: FactManager, private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
|
|
237
289
|
const router = Router();
|
|
238
290
|
router.get('/login', getAuthenticate(user => this.login(user)));
|
|
239
291
|
if (backwardCompatible) {
|
|
@@ -257,7 +309,9 @@ export class HttpRouter {
|
|
|
257
309
|
parseString,
|
|
258
310
|
(user, input: string) => this.feeds(user, input)
|
|
259
311
|
));
|
|
260
|
-
router.get('/feeds/:hash',
|
|
312
|
+
router.get('/feeds/:hash', getOrStream<FeedResponse>(
|
|
313
|
+
(user, params, query) => this.feed(user, params, query),
|
|
314
|
+
(user, params, query) => this.streamFeed(user, params, query)));
|
|
261
315
|
|
|
262
316
|
this.handler = router;
|
|
263
317
|
}
|
|
@@ -273,7 +327,7 @@ export class HttpRouter {
|
|
|
273
327
|
};
|
|
274
328
|
}
|
|
275
329
|
|
|
276
|
-
private async query(user: RequestUser | null, queryMessage: QueryMessage)
|
|
330
|
+
private async query(user: RequestUser | null, queryMessage: QueryMessage): Promise<QueryResponse> {
|
|
277
331
|
const userIdentity = serializeUserIdentity(user);
|
|
278
332
|
const query = fromDescriptiveString(queryMessage.query);
|
|
279
333
|
const result = await this.authorization.query(userIdentity, queryMessage.start, query);
|
|
@@ -282,7 +336,7 @@ export class HttpRouter {
|
|
|
282
336
|
};
|
|
283
337
|
}
|
|
284
338
|
|
|
285
|
-
private async load(user: RequestUser, loadMessage: LoadMessage)
|
|
339
|
+
private async load(user: RequestUser, loadMessage: LoadMessage): Promise<LoadResponse> {
|
|
286
340
|
const userIdentity = serializeUserIdentity(user);
|
|
287
341
|
const result = await this.authorization.load(userIdentity, loadMessage.references);
|
|
288
342
|
return {
|
|
@@ -290,7 +344,7 @@ export class HttpRouter {
|
|
|
290
344
|
};
|
|
291
345
|
}
|
|
292
346
|
|
|
293
|
-
private async save(user: RequestUser | null, saveMessage: SaveMessage)
|
|
347
|
+
private async save(user: RequestUser | null, saveMessage: SaveMessage): Promise<void> {
|
|
294
348
|
const userIdentity = serializeUserIdentity(user);
|
|
295
349
|
await this.authorization.save(userIdentity, saveMessage.facts);
|
|
296
350
|
}
|
|
@@ -346,33 +400,21 @@ export class HttpRouter {
|
|
|
346
400
|
throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
347
401
|
}
|
|
348
402
|
}
|
|
403
|
+
|
|
404
|
+
const namedStart = specification.given.reduce((map, label, index) => ({
|
|
405
|
+
...map,
|
|
406
|
+
[label.name]: start[index]
|
|
407
|
+
}), {} as ReferencesByName);
|
|
349
408
|
|
|
350
409
|
// Verify that I can distribute all feeds to the user.
|
|
351
410
|
const feeds = buildFeeds(specification);
|
|
352
411
|
const userIdentity = serializeUserIdentity(user);
|
|
353
|
-
await this.authorization.verifyDistribution(userIdentity, feeds,
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
const indexedStart = feed.inputs.map(input => ({
|
|
357
|
-
factReference: start[input.inputIndex],
|
|
358
|
-
index: input.inputIndex
|
|
359
|
-
}));
|
|
360
|
-
const feedDefinition: FeedDefinition = {
|
|
361
|
-
start: indexedStart,
|
|
362
|
-
feed
|
|
363
|
-
};
|
|
364
|
-
return {
|
|
365
|
-
hash: urlSafeHash(feed),
|
|
366
|
-
feedDefinition
|
|
367
|
-
};
|
|
368
|
-
});
|
|
369
|
-
// Store all feeds in the cache.
|
|
370
|
-
for (const d of feedDefinitionsByHash) {
|
|
371
|
-
await this.feedCache.storeFeed(d.hash, d.feedDefinition);
|
|
372
|
-
}
|
|
412
|
+
await this.authorization.verifyDistribution(userIdentity, feeds, namedStart);
|
|
413
|
+
|
|
414
|
+
const feedHashes = this.feedCache.addFeeds(feeds, namedStart);
|
|
373
415
|
|
|
374
416
|
return {
|
|
375
|
-
feeds:
|
|
417
|
+
feeds: feedHashes
|
|
376
418
|
}
|
|
377
419
|
}
|
|
378
420
|
|
|
@@ -390,10 +432,7 @@ export class HttpRouter {
|
|
|
390
432
|
const bookmark = query["b"] as string ?? "";
|
|
391
433
|
|
|
392
434
|
const userIdentity = serializeUserIdentity(user);
|
|
393
|
-
const start = feedDefinition.
|
|
394
|
-
start[input.index] = input.factReference;
|
|
395
|
-
return start;
|
|
396
|
-
}, [] as FactReference[]);
|
|
435
|
+
const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
|
|
397
436
|
const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
|
|
398
437
|
// Return distinct fact references from all the tuples.
|
|
399
438
|
const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
|
|
@@ -406,6 +445,62 @@ export class HttpRouter {
|
|
|
406
445
|
return response;
|
|
407
446
|
}
|
|
408
447
|
|
|
448
|
+
private async streamFeed(user: RequestUser | null, params: { [key: string]: string }, query: ParsedQs): Promise<Stream<FeedResponse> | null> {
|
|
449
|
+
const feedHash = params["hash"];
|
|
450
|
+
if (!feedHash) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const feedDefinition = await this.feedCache.getFeed(feedHash);
|
|
455
|
+
if (!feedDefinition) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let bookmark = query["b"] as string ?? "";
|
|
460
|
+
|
|
461
|
+
const userIdentity = serializeUserIdentity(user);
|
|
462
|
+
const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
|
|
463
|
+
const givenHash = computeObjectHash(feedDefinition.namedStart);
|
|
464
|
+
|
|
465
|
+
const stream = new Stream<FeedResponse>();
|
|
466
|
+
Trace.info("Initial response");
|
|
467
|
+
bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream);
|
|
468
|
+
const inverses = invertSpecification(feedDefinition.feed);
|
|
469
|
+
const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(
|
|
470
|
+
inverse.inverseSpecification,
|
|
471
|
+
async (results) => {
|
|
472
|
+
// Filter out results that do not match the given.
|
|
473
|
+
const matchingResults = results.filter(pr =>
|
|
474
|
+
givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
|
|
475
|
+
if (matchingResults.length != 0) {
|
|
476
|
+
bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
));
|
|
480
|
+
stream.done(() => {
|
|
481
|
+
Trace.info("Done");
|
|
482
|
+
for (const listener of listeners) {
|
|
483
|
+
this.factManager.removeSpecificationListener(listener);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
return stream;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async streamFeedResponse(userIdentity: UserIdentity | null, feedDefinition: FeedObject, start: FactReference[], bookmark: string, stream: Stream<FeedResponse>, skipIfEmpty = false): Promise<string> {
|
|
490
|
+
const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
|
|
491
|
+
// Return distinct fact references from all the tuples.
|
|
492
|
+
const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
|
|
493
|
+
);
|
|
494
|
+
if (!skipIfEmpty || references.length > 0) {
|
|
495
|
+
const response: FeedResponse = {
|
|
496
|
+
references,
|
|
497
|
+
bookmark: results.bookmark
|
|
498
|
+
};
|
|
499
|
+
stream.feed(response);
|
|
500
|
+
}
|
|
501
|
+
return results.bookmark;
|
|
502
|
+
}
|
|
503
|
+
|
|
409
504
|
private async getKnownFacts(user: RequestUser | null): Promise<Declaration> {
|
|
410
505
|
if (user) {
|
|
411
506
|
const userFact = await this.authorization.getOrCreateUserFact({
|
|
@@ -430,7 +525,7 @@ export class HttpRouter {
|
|
|
430
525
|
}
|
|
431
526
|
}
|
|
432
527
|
|
|
433
|
-
private selectStart(specification: Specification, declaration: Declaration)
|
|
528
|
+
private selectStart(specification: Specification, declaration: Declaration): FactReference[] {
|
|
434
529
|
// Select starting facts that match the inputs
|
|
435
530
|
return specification.given.map(input => {
|
|
436
531
|
const declaredFact = declaration.find(d => d.name === input.name);
|
|
@@ -449,11 +544,6 @@ function parseString(input: any): string {
|
|
|
449
544
|
return input;
|
|
450
545
|
}
|
|
451
546
|
|
|
452
|
-
function urlSafeHash(feed: Feed): string {
|
|
453
|
-
const base64 = computeObjectHash(feed);
|
|
454
|
-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
547
|
function extractResults(obj: any): any {
|
|
458
548
|
if (Array.isArray(obj)) {
|
|
459
549
|
const projectedResults: ProjectedResult[] = obj;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class Stream<T> {
|
|
2
|
+
private queue: T[] = [];
|
|
3
|
+
private handlers: ((data: T) => void)[] = [];
|
|
4
|
+
private doneHandlers: (() => void)[] = [];
|
|
5
|
+
private closed = false;
|
|
6
|
+
|
|
7
|
+
next(handler: (data: T) => void): Stream<T> {
|
|
8
|
+
this.handlers.push(handler);
|
|
9
|
+
this.queue.forEach(data => handler(data));
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
done(handler: () => void): Stream<T> {
|
|
14
|
+
this.doneHandlers.push(handler);
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
close(): void {
|
|
19
|
+
this.closed = true;
|
|
20
|
+
this.doneHandlers.forEach(handler => handler());
|
|
21
|
+
this.queue = [];
|
|
22
|
+
this.handlers = [];
|
|
23
|
+
this.doneHandlers = [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
feed(data: any): void {
|
|
27
|
+
if (!this.closed) {
|
|
28
|
+
this.queue.push(data);
|
|
29
|
+
this.handlers.forEach(handler => handler(data));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
export { AuthenticationDevice } from "./authentication/authentication-device";
|
|
2
2
|
export { AuthenticationSession } from "./authentication/authentication-session";
|
|
3
3
|
export { AuthorizationKeystore } from "./authorization/authorization-keystore";
|
|
4
|
-
export {
|
|
5
|
-
export { FeedCache } from "./http/feed-cache";
|
|
4
|
+
export { DistributedFactCache } from "./authorization/distributed-fact-cache";
|
|
6
5
|
export { HttpRouter, RequestUser } from "./http/router";
|
|
7
6
|
export { JinagaServer, JinagaServerConfig, JinagaServerInstance, tracePool } from "./jinaga-server";
|
|
8
|
-
export {
|
|
7
|
+
export { Keystore } from "./keystore";
|
|
9
8
|
export { MemoryKeystore } from "./memory/memory-keystore";
|
|
10
9
|
export { PostgresKeystore } from "./postgres/postgres-keystore";
|
|
11
|
-
export { PostgresStore } from "./postgres/postgres-store";
|
|
10
|
+
export { PostgresStore } from "./postgres/postgres-store";
|