jinaga-server 2.3.4 → 2.4.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/.github/workflows/publish.yml +2 -0
- package/dist/authorization/authorization-keystore.d.ts +8 -5
- package/dist/authorization/authorization-keystore.js +64 -5
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/authorization/distributed-fact-cache.d.ts +7 -0
- package/dist/authorization/distributed-fact-cache.js +28 -0
- package/dist/authorization/distributed-fact-cache.js.map +1 -0
- package/dist/http/feed-cache.d.ts +10 -3
- package/dist/http/router.d.ts +1 -1
- package/dist/http/router.js +50 -14
- package/dist/http/router.js.map +1 -1
- package/dist/jinaga-server.d.ts +2 -1
- package/dist/jinaga-server.js +6 -4
- package/dist/jinaga-server.js.map +1 -1
- package/dist/memory/memory-feed-cache.d.ts +4 -5
- package/dist/memory/memory-feed-cache.js +4 -4
- package/dist/memory/memory-feed-cache.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +1 -1
- package/dist/postgres/postgres-store.js +2 -2
- package/dist/postgres/postgres-store.js.map +1 -1
- package/dist/postgres/specification-result-sql.js +1 -1
- package/dist/postgres/specification-result-sql.js.map +1 -1
- package/dist/postgres/specification-sql.d.ts +1 -1
- package/dist/postgres/specification-sql.js +18 -8
- package/dist/postgres/specification-sql.js.map +1 -1
- package/integration-test/jinaga-test/Dockerfile +1 -1
- package/integration-test/jinaga-test/package-lock.json +38 -25
- package/integration-test/jinaga-test/package.json +3 -3
- package/integration-test/postgres/Dockerfile +1 -1
- package/package.json +11 -6
- package/replicator/index.ts +1 -1
- package/replicator/package-lock.json +25 -16
- package/replicator/package.json +1 -1
- package/src/authorization/authorization-keystore.ts +70 -9
- package/src/authorization/distributed-fact-cache.ts +34 -0
- package/src/http/feed-cache.ts +11 -3
- package/src/http/router.ts +66 -27
- package/src/jinaga-server.ts +9 -5
- package/src/memory/memory-feed-cache.ts +6 -7
- package/src/postgres/postgres-store.ts +2 -2
- package/src/postgres/specification-result-sql.ts +1 -1
- package/src/postgres/specification-sql.ts +19 -8
- package/test/authorization/authorizationSpec.ts +1 -1
package/replicator/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ app.use(express.text());
|
|
|
18
18
|
app.use(cors());
|
|
19
19
|
|
|
20
20
|
const pgConnection = process.env.JINAGA_POSTGRESQL ||
|
|
21
|
-
'postgresql://
|
|
21
|
+
'postgresql://raasuser:raaspw@localhost:5432/raas';
|
|
22
22
|
const { handler } = JinagaServer.create({
|
|
23
23
|
pgStore: pgConnection
|
|
24
24
|
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"cors": "^2.8.5",
|
|
13
13
|
"express": "^4.18.1",
|
|
14
|
-
"pg": "
|
|
14
|
+
"pg": "8.11"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/cors": "^2.8.12",
|
|
@@ -530,21 +530,24 @@
|
|
|
530
530
|
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
|
531
531
|
},
|
|
532
532
|
"node_modules/pg": {
|
|
533
|
-
"version": "8.
|
|
534
|
-
"resolved": "https://registry.npmjs.org/pg/-/pg-8.
|
|
535
|
-
"integrity": "sha512-
|
|
533
|
+
"version": "8.11.0",
|
|
534
|
+
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz",
|
|
535
|
+
"integrity": "sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==",
|
|
536
536
|
"dependencies": {
|
|
537
537
|
"buffer-writer": "2.0.0",
|
|
538
538
|
"packet-reader": "1.0.0",
|
|
539
|
-
"pg-connection-string": "^2.
|
|
540
|
-
"pg-pool": "^3.
|
|
541
|
-
"pg-protocol": "^1.
|
|
539
|
+
"pg-connection-string": "^2.6.0",
|
|
540
|
+
"pg-pool": "^3.6.0",
|
|
541
|
+
"pg-protocol": "^1.6.0",
|
|
542
542
|
"pg-types": "^2.1.0",
|
|
543
543
|
"pgpass": "1.x"
|
|
544
544
|
},
|
|
545
545
|
"engines": {
|
|
546
546
|
"node": ">= 8.0.0"
|
|
547
547
|
},
|
|
548
|
+
"optionalDependencies": {
|
|
549
|
+
"pg-cloudflare": "^1.1.0"
|
|
550
|
+
},
|
|
548
551
|
"peerDependencies": {
|
|
549
552
|
"pg-native": ">=3.0.1"
|
|
550
553
|
},
|
|
@@ -554,10 +557,16 @@
|
|
|
554
557
|
}
|
|
555
558
|
}
|
|
556
559
|
},
|
|
560
|
+
"node_modules/pg-cloudflare": {
|
|
561
|
+
"version": "1.1.0",
|
|
562
|
+
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz",
|
|
563
|
+
"integrity": "sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==",
|
|
564
|
+
"optional": true
|
|
565
|
+
},
|
|
557
566
|
"node_modules/pg-connection-string": {
|
|
558
|
-
"version": "2.
|
|
559
|
-
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.
|
|
560
|
-
"integrity": "sha512-
|
|
567
|
+
"version": "2.6.0",
|
|
568
|
+
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.0.tgz",
|
|
569
|
+
"integrity": "sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg=="
|
|
561
570
|
},
|
|
562
571
|
"node_modules/pg-int8": {
|
|
563
572
|
"version": "1.0.1",
|
|
@@ -568,17 +577,17 @@
|
|
|
568
577
|
}
|
|
569
578
|
},
|
|
570
579
|
"node_modules/pg-pool": {
|
|
571
|
-
"version": "3.
|
|
572
|
-
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.
|
|
573
|
-
"integrity": "sha512-
|
|
580
|
+
"version": "3.6.0",
|
|
581
|
+
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz",
|
|
582
|
+
"integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==",
|
|
574
583
|
"peerDependencies": {
|
|
575
584
|
"pg": ">=8.0"
|
|
576
585
|
}
|
|
577
586
|
},
|
|
578
587
|
"node_modules/pg-protocol": {
|
|
579
|
-
"version": "1.
|
|
580
|
-
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.
|
|
581
|
-
"integrity": "sha512-
|
|
588
|
+
"version": "1.6.0",
|
|
589
|
+
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
|
|
590
|
+
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
|
|
582
591
|
},
|
|
583
592
|
"node_modules/pg-types": {
|
|
584
593
|
"version": "2.2.0",
|
package/replicator/package.json
CHANGED
|
@@ -2,29 +2,41 @@ import {
|
|
|
2
2
|
Authorization,
|
|
3
3
|
AuthorizationEngine,
|
|
4
4
|
AuthorizationRules,
|
|
5
|
+
DistributionEngine,
|
|
6
|
+
DistributionRules,
|
|
5
7
|
FactEnvelope,
|
|
6
8
|
FactFeed,
|
|
7
9
|
FactRecord,
|
|
8
10
|
FactReference,
|
|
9
11
|
Feed,
|
|
12
|
+
Forbidden,
|
|
10
13
|
Query,
|
|
11
14
|
Specification,
|
|
12
15
|
Storage,
|
|
13
|
-
UserIdentity
|
|
16
|
+
UserIdentity,
|
|
17
|
+
buildFeeds,
|
|
18
|
+
factReferenceEquals
|
|
14
19
|
} from "jinaga";
|
|
15
20
|
|
|
16
21
|
import { Keystore } from "../keystore";
|
|
22
|
+
import { distinct } from "../util/fn";
|
|
23
|
+
import { DistributedFactCache } from "./distributed-fact-cache";
|
|
17
24
|
|
|
18
25
|
export class AuthorizationKeystore implements Authorization {
|
|
19
26
|
private authorizationEngine: AuthorizationEngine | null;
|
|
27
|
+
private distributionEngine: DistributionEngine | null;
|
|
28
|
+
private distributedFacts: DistributedFactCache = new DistributedFactCache();
|
|
20
29
|
|
|
21
30
|
constructor(
|
|
22
31
|
private store: Storage,
|
|
23
32
|
private keystore: Keystore,
|
|
24
|
-
authorizationRules: AuthorizationRules | null
|
|
33
|
+
authorizationRules: AuthorizationRules | null,
|
|
34
|
+
distributionRules: DistributionRules | null
|
|
25
35
|
) {
|
|
26
36
|
this.authorizationEngine = authorizationRules &&
|
|
27
37
|
new AuthorizationEngine(authorizationRules, store);
|
|
38
|
+
this.distributionEngine = distributionRules &&
|
|
39
|
+
new DistributionEngine(distributionRules, store);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
async getOrCreateUserFact(userIdentity: UserIdentity) {
|
|
@@ -39,20 +51,55 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
39
51
|
return userFact;
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
query(userIdentity: UserIdentity, start: FactReference, query: Query) {
|
|
54
|
+
query(userIdentity: UserIdentity | null, start: FactReference, query: Query) {
|
|
43
55
|
return this.store.query(start, query);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
read(userIdentity: UserIdentity, start: FactReference[], specification: Specification) {
|
|
47
|
-
|
|
58
|
+
async read(userIdentity: UserIdentity | null, start: FactReference[], specification: Specification) {
|
|
59
|
+
if (this.distributionEngine) {
|
|
60
|
+
const userReference: FactReference | null = userIdentity
|
|
61
|
+
? await this.keystore.getUserFact(userIdentity)
|
|
62
|
+
: null;
|
|
63
|
+
// Break the specification into feeds and check distribution.
|
|
64
|
+
const feeds = buildFeeds(specification);
|
|
65
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, start, userReference);
|
|
66
|
+
if (!canDistribute) {
|
|
67
|
+
throw new Forbidden("Unauthorized");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return await this.store.read(start, specification);
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
feed(userIdentity: UserIdentity, feed: Feed, bookmark: string): Promise<FactFeed> {
|
|
51
|
-
|
|
73
|
+
async feed(userIdentity: UserIdentity | null, feed: Feed, start: FactReference[], bookmark: string): Promise<FactFeed> {
|
|
74
|
+
if (this.distributionEngine) {
|
|
75
|
+
const userReference: FactReference | null = userIdentity
|
|
76
|
+
? await this.keystore.getUserFact(userIdentity)
|
|
77
|
+
: null;
|
|
78
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll([feed], start, userReference);
|
|
79
|
+
if (!canDistribute) {
|
|
80
|
+
throw new Forbidden("Unauthorized");
|
|
81
|
+
}
|
|
82
|
+
const factFeed = await this.store.feed(feed, start, bookmark);
|
|
83
|
+
const factReferences = factFeed.tuples
|
|
84
|
+
.flatMap(tuple => tuple.facts)
|
|
85
|
+
.filter((value, index, self) => self.findIndex(factReferenceEquals(value)) === index);
|
|
86
|
+
this.distributedFacts.add(factReferences, userReference);
|
|
87
|
+
return factFeed;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
return await this.store.feed(feed, start, bookmark);
|
|
91
|
+
}
|
|
52
92
|
}
|
|
53
93
|
|
|
54
|
-
load(userIdentity: UserIdentity, references: FactReference[]) {
|
|
55
|
-
|
|
94
|
+
async load(userIdentity: UserIdentity, references: FactReference[]) {
|
|
95
|
+
if (this.distributionEngine) {
|
|
96
|
+
const userFact = userIdentity ? await this.keystore.getUserFact(userIdentity) : null;
|
|
97
|
+
const canDistribute = this.distributedFacts.includesAll(references, userFact);
|
|
98
|
+
if (!canDistribute) {
|
|
99
|
+
throw new Forbidden("Unauthorized");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return await this.store.load(references);
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
async save(userIdentity: UserIdentity | null, facts: FactRecord[]) {
|
|
@@ -75,4 +122,18 @@ export class AuthorizationKeystore implements Authorization {
|
|
|
75
122
|
return authorizedFacts;
|
|
76
123
|
}
|
|
77
124
|
}
|
|
125
|
+
|
|
126
|
+
async verifyDistribution(userIdentity: UserIdentity | null, feeds: Feed[], start: FactReference[]): Promise<void> {
|
|
127
|
+
if (!this.distributionEngine) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const userReference: FactReference | null = userIdentity
|
|
132
|
+
? await this.keystore.getUserFact(userIdentity)
|
|
133
|
+
: null;
|
|
134
|
+
const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, start, userReference);
|
|
135
|
+
if (!canDistribute) {
|
|
136
|
+
throw new Forbidden("Unauthorized");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
78
139
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FactReference, factReferenceEquals } from "jinaga";
|
|
2
|
+
|
|
3
|
+
interface Batch {
|
|
4
|
+
factReferences: FactReference[];
|
|
5
|
+
userReference: FactReference | null;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DistributedFactCache {
|
|
10
|
+
private batches: Batch[] = [];
|
|
11
|
+
|
|
12
|
+
add(factReferences: FactReference[], userReference: FactReference | null): void {
|
|
13
|
+
this.removeOldBatches();
|
|
14
|
+
this.batches.push({
|
|
15
|
+
factReferences,
|
|
16
|
+
userReference,
|
|
17
|
+
createdAt: new Date()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
includesAll(references: FactReference[], userFact: FactReference | null): boolean {
|
|
22
|
+
this.removeOldBatches();
|
|
23
|
+
return references.every(reference =>
|
|
24
|
+
this.batches.some(batch =>
|
|
25
|
+
batch.factReferences.some(factReferenceEquals(reference)) &&
|
|
26
|
+
(batch.userReference === null || batch.userReference === userFact)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
removeOldBatches() {
|
|
30
|
+
const cutoff = new Date().getTime() - 1000 * 60 * 5;
|
|
31
|
+
this.batches = this.batches.filter(batch =>
|
|
32
|
+
batch.createdAt.getTime() > cutoff);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/http/feed-cache.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { Feed } from "jinaga";
|
|
1
|
+
import { FactReference, Feed } from "jinaga";
|
|
2
|
+
|
|
3
|
+
export interface FeedDefinition {
|
|
4
|
+
start: {
|
|
5
|
+
factReference: FactReference,
|
|
6
|
+
index: number
|
|
7
|
+
}[],
|
|
8
|
+
feed: Feed
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
export interface FeedCache {
|
|
4
|
-
storeFeed(feedHash: string,
|
|
5
|
-
getFeed(feedHash: string): Promise<
|
|
12
|
+
storeFeed(feedHash: string, feedDefinition: FeedDefinition): Promise<void>;
|
|
13
|
+
getFeed(feedHash: string): Promise<FeedDefinition | undefined>;
|
|
6
14
|
}
|
package/src/http/router.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Handler, Router } from "express";
|
|
2
2
|
import {
|
|
3
3
|
Authorization,
|
|
4
|
-
buildFeeds,
|
|
5
|
-
computeObjectHash,
|
|
6
4
|
Declaration,
|
|
7
5
|
FactRecord,
|
|
8
6
|
FactReference,
|
|
9
7
|
Feed,
|
|
8
|
+
FeedResponse,
|
|
9
|
+
FeedsResponse,
|
|
10
10
|
Forbidden,
|
|
11
|
-
fromDescriptiveString,
|
|
12
11
|
LoadMessage,
|
|
13
12
|
LoadResponse,
|
|
14
13
|
ProfileMessage,
|
|
@@ -20,10 +19,12 @@ import {
|
|
|
20
19
|
SpecificationParser,
|
|
21
20
|
Trace,
|
|
22
21
|
UserIdentity,
|
|
22
|
+
buildFeeds,
|
|
23
|
+
computeObjectHash,
|
|
24
|
+
fromDescriptiveString,
|
|
23
25
|
} from "jinaga";
|
|
24
|
-
import { FeedResponse, FeedsResponse } from "jinaga/dist/http/messages";
|
|
25
26
|
|
|
26
|
-
import { FeedCache } from "./feed-cache";
|
|
27
|
+
import { FeedCache, FeedDefinition } from "./feed-cache";
|
|
27
28
|
|
|
28
29
|
interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
|
|
29
30
|
|
|
@@ -43,8 +44,14 @@ function get<U>(method: ((req: RequestUser, params: { [key: string]: string }, q
|
|
|
43
44
|
}
|
|
44
45
|
})
|
|
45
46
|
.catch(error => {
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if (error instanceof Forbidden) {
|
|
48
|
+
res.type("text");
|
|
49
|
+
res.status(403).send(error.message);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
Trace.error(error);
|
|
53
|
+
res.status(500).send(error.message);
|
|
54
|
+
}
|
|
48
55
|
next();
|
|
49
56
|
});
|
|
50
57
|
};
|
|
@@ -198,7 +205,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
|
|
|
198
205
|
};
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
function serializeUserIdentity(user: RequestUser) : UserIdentity | null {
|
|
208
|
+
function serializeUserIdentity(user: RequestUser | null) : UserIdentity | null {
|
|
202
209
|
if (!user) {
|
|
203
210
|
return null;
|
|
204
211
|
}
|
|
@@ -217,10 +224,12 @@ export interface RequestUser {
|
|
|
217
224
|
export class HttpRouter {
|
|
218
225
|
handler: Handler;
|
|
219
226
|
|
|
220
|
-
constructor(private authorization: Authorization, private feedCache: FeedCache) {
|
|
227
|
+
constructor(private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
|
|
221
228
|
const router = Router();
|
|
222
229
|
router.get('/login', getAuthenticate(user => this.login(user)));
|
|
223
|
-
|
|
230
|
+
if (backwardCompatible) {
|
|
231
|
+
router.post('/query', post((user, queryMessage: QueryMessage) => this.query(user, queryMessage)));
|
|
232
|
+
}
|
|
224
233
|
router.post('/load', post((user, loadMessage: LoadMessage) => this.load(user, loadMessage)));
|
|
225
234
|
router.post('/save', postCreate((user, saveMessage: SaveMessage) => this.save(user, saveMessage)));
|
|
226
235
|
|
|
@@ -243,7 +252,7 @@ export class HttpRouter {
|
|
|
243
252
|
};
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
private async query(user: RequestUser, queryMessage: QueryMessage) : Promise<QueryResponse> {
|
|
255
|
+
private async query(user: RequestUser | null, queryMessage: QueryMessage) : Promise<QueryResponse> {
|
|
247
256
|
const userIdentity = serializeUserIdentity(user);
|
|
248
257
|
const query = fromDescriptiveString(queryMessage.query);
|
|
249
258
|
const result = await this.authorization.query(userIdentity, queryMessage.start, query);
|
|
@@ -260,12 +269,12 @@ export class HttpRouter {
|
|
|
260
269
|
};
|
|
261
270
|
}
|
|
262
271
|
|
|
263
|
-
private async save(user: RequestUser, saveMessage: SaveMessage) : Promise<void> {
|
|
272
|
+
private async save(user: RequestUser | null, saveMessage: SaveMessage) : Promise<void> {
|
|
264
273
|
const userIdentity = serializeUserIdentity(user);
|
|
265
274
|
await this.authorization.save(userIdentity, saveMessage.facts);
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
private async read(user: RequestUser, input: string): Promise<any[]> {
|
|
277
|
+
private async read(user: RequestUser | null, input: string): Promise<any[]> {
|
|
269
278
|
const knownFacts = await this.getKnownFacts(user);
|
|
270
279
|
const parser = new SpecificationParser(input);
|
|
271
280
|
parser.skipWhitespace();
|
|
@@ -278,7 +287,7 @@ export class HttpRouter {
|
|
|
278
287
|
return extractResults(results);
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
private async write(user: RequestUser, input: string): Promise<void> {
|
|
290
|
+
private async write(user: RequestUser | null, input: string): Promise<void> {
|
|
282
291
|
const knownFacts = await this.getKnownFacts(user);
|
|
283
292
|
const parser = new SpecificationParser(input);
|
|
284
293
|
parser.skipWhitespace();
|
|
@@ -296,7 +305,7 @@ export class HttpRouter {
|
|
|
296
305
|
await this.authorization.save(userIdentity, factRecords);
|
|
297
306
|
}
|
|
298
307
|
|
|
299
|
-
private async feeds(user: RequestUser, input: string): Promise<FeedsResponse> {
|
|
308
|
+
private async feeds(user: RequestUser | null, input: string): Promise<FeedsResponse> {
|
|
300
309
|
const knownFacts = await this.getKnownFacts(user);
|
|
301
310
|
const parser = new SpecificationParser(input);
|
|
302
311
|
parser.skipWhitespace();
|
|
@@ -304,35 +313,65 @@ export class HttpRouter {
|
|
|
304
313
|
const specification = parser.parseSpecification();
|
|
305
314
|
const start = this.selectStart(specification, declaration);
|
|
306
315
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
316
|
+
// Verify that the number of start facts equals the number of inputs
|
|
317
|
+
if (start.length !== specification.given.length) {
|
|
318
|
+
throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
319
|
+
}
|
|
320
|
+
// Verify that the input type matches the start fact type
|
|
321
|
+
for (let i = 0; i < start.length; i++) {
|
|
322
|
+
if (start[i].type !== specification.given[i].type) {
|
|
323
|
+
throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Verify that I can distribute all feeds to the user.
|
|
328
|
+
const feeds = buildFeeds(specification);
|
|
329
|
+
const userIdentity = serializeUserIdentity(user);
|
|
330
|
+
await this.authorization.verifyDistribution(userIdentity, feeds, start);
|
|
331
|
+
|
|
332
|
+
const feedDefinitionsByHash = feeds.map(feed => {
|
|
333
|
+
const indexedStart = feed.inputs.map(input => ({
|
|
334
|
+
factReference: start[input.inputIndex],
|
|
335
|
+
index: input.inputIndex
|
|
336
|
+
}));
|
|
337
|
+
const feedDefinition: FeedDefinition = {
|
|
338
|
+
start: indexedStart,
|
|
339
|
+
feed
|
|
340
|
+
};
|
|
341
|
+
return {
|
|
342
|
+
hash: urlSafeHash(feed),
|
|
343
|
+
feedDefinition
|
|
344
|
+
};
|
|
345
|
+
});
|
|
311
346
|
// Store all feeds in the cache.
|
|
312
|
-
for (const
|
|
313
|
-
await this.feedCache.storeFeed(
|
|
347
|
+
for (const d of feedDefinitionsByHash) {
|
|
348
|
+
await this.feedCache.storeFeed(d.hash, d.feedDefinition);
|
|
314
349
|
}
|
|
315
350
|
|
|
316
351
|
return {
|
|
317
|
-
feeds:
|
|
352
|
+
feeds: feedDefinitionsByHash.map(f => f.hash)
|
|
318
353
|
}
|
|
319
354
|
}
|
|
320
355
|
|
|
321
|
-
private async feed(user: RequestUser, params: { [key: string]: string }, query: ParsedQs): Promise<FeedResponse | null> {
|
|
356
|
+
private async feed(user: RequestUser | null, params: { [key: string]: string }, query: ParsedQs): Promise<FeedResponse | null> {
|
|
322
357
|
const feedHash = params["hash"];
|
|
323
358
|
if (!feedHash) {
|
|
324
359
|
return null;
|
|
325
360
|
}
|
|
326
361
|
|
|
327
|
-
const
|
|
328
|
-
if (!
|
|
362
|
+
const feedDefinition = await this.feedCache.getFeed(feedHash);
|
|
363
|
+
if (!feedDefinition) {
|
|
329
364
|
return null;
|
|
330
365
|
}
|
|
331
366
|
|
|
332
367
|
const bookmark = query["b"] as string ?? "";
|
|
333
368
|
|
|
334
369
|
const userIdentity = serializeUserIdentity(user);
|
|
335
|
-
const
|
|
370
|
+
const start = feedDefinition.start.reduce((start, input) => {
|
|
371
|
+
start[input.index] = input.factReference;
|
|
372
|
+
return start;
|
|
373
|
+
}, [] as FactReference[]);
|
|
374
|
+
const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
|
|
336
375
|
// Return distinct fact references from all the tuples.
|
|
337
376
|
const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
|
|
338
377
|
self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
|
|
@@ -344,7 +383,7 @@ export class HttpRouter {
|
|
|
344
383
|
return response;
|
|
345
384
|
}
|
|
346
385
|
|
|
347
|
-
private async getKnownFacts(user: RequestUser): Promise<Declaration> {
|
|
386
|
+
private async getKnownFacts(user: RequestUser | null): Promise<Declaration> {
|
|
348
387
|
if (user) {
|
|
349
388
|
const userFact = await this.authorization.getOrCreateUserFact({
|
|
350
389
|
provider: user.provider,
|
package/src/jinaga-server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Authorization,
|
|
5
5
|
AuthorizationNoOp,
|
|
6
6
|
AuthorizationRules,
|
|
7
|
+
DistributionRules,
|
|
7
8
|
FactManager,
|
|
8
9
|
Jinaga,
|
|
9
10
|
MemoryStore,
|
|
@@ -35,7 +36,8 @@ export type JinagaServerConfig = {
|
|
|
35
36
|
pgKeystore?: string | Pool,
|
|
36
37
|
pgKeystoreSchema?: string,
|
|
37
38
|
model?: Model,
|
|
38
|
-
authorization?: (a: AuthorizationRules) => AuthorizationRules
|
|
39
|
+
authorization?: (a: AuthorizationRules) => AuthorizationRules,
|
|
40
|
+
distribution?: (d: DistributionRules) => DistributionRules,
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
export type JinagaServerInstance = {
|
|
@@ -59,9 +61,11 @@ export class JinagaServer {
|
|
|
59
61
|
const fork = new PassThroughFork(store);
|
|
60
62
|
const keystore = createKeystore(config, pools);
|
|
61
63
|
const authorizationRules = config.authorization ? config.authorization(new AuthorizationRules(config.model)) : null;
|
|
62
|
-
const
|
|
64
|
+
const distributionRules = config.distribution ? config.distribution(new DistributionRules([])) : null;
|
|
65
|
+
const authorization = createAuthorization(authorizationRules, distributionRules, store, keystore);
|
|
63
66
|
const feedCache = new MemoryFeedCache();
|
|
64
|
-
|
|
67
|
+
// Retain backward compatibility until the user provides distribution rules.
|
|
68
|
+
const router = new HttpRouter(authorization, feedCache, distributionRules === null);
|
|
65
69
|
const authentication = createAuthentication(store, keystore, authorizationRules);
|
|
66
70
|
const network = new NetworkNoOp();
|
|
67
71
|
const factManager = new FactManager(authentication, fork, source, store, network);
|
|
@@ -106,9 +110,9 @@ function createKeystore(config: JinagaServerConfig, pools: { [uri: string]: Pool
|
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
function createAuthorization(authorizationRules: AuthorizationRules | null, store: Storage, keystore: Keystore | null): Authorization {
|
|
113
|
+
function createAuthorization(authorizationRules: AuthorizationRules | null, distributionRules: DistributionRules | null, store: Storage, keystore: Keystore | null): Authorization {
|
|
110
114
|
if (keystore) {
|
|
111
|
-
const authorization = new AuthorizationKeystore(store, keystore, authorizationRules);
|
|
115
|
+
const authorization = new AuthorizationKeystore(store, keystore, authorizationRules, distributionRules);
|
|
112
116
|
return authorization;
|
|
113
117
|
}
|
|
114
118
|
else {
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FeedCache } from "../http/feed-cache";
|
|
1
|
+
import { FeedCache, FeedDefinition } from "../http/feed-cache";
|
|
3
2
|
|
|
4
3
|
export class MemoryFeedCache implements FeedCache {
|
|
5
|
-
private
|
|
4
|
+
private feedDefinitions: { [feedHash: string]: FeedDefinition } = {};
|
|
6
5
|
|
|
7
|
-
storeFeed(feedHash: string,
|
|
8
|
-
this.
|
|
6
|
+
storeFeed(feedHash: string, feedDefinition: FeedDefinition): Promise<void> {
|
|
7
|
+
this.feedDefinitions[feedHash] = feedDefinition;
|
|
9
8
|
return Promise.resolve();
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
getFeed(feedHash: string): Promise<
|
|
13
|
-
return Promise.resolve(this.
|
|
11
|
+
getFeed(feedHash: string): Promise<FeedDefinition | undefined> {
|
|
12
|
+
return Promise.resolve(this.feedDefinitions[feedHash]);
|
|
14
13
|
}
|
|
15
14
|
}
|
|
@@ -261,10 +261,10 @@ export class PostgresStore implements Storage {
|
|
|
261
261
|
return composer.compose(resultSets, factRecords);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
async feed(feed: Feed, bookmark: string): Promise<FactFeed> {
|
|
264
|
+
async feed(feed: Feed, start: FactReference[], bookmark: string): Promise<FactFeed> {
|
|
265
265
|
const factTypes: FactTypeMap = await this.loadFactTypesFromFeed(feed);
|
|
266
266
|
const roleMap: RoleMap = await this.loadRolesFromFeed(feed, factTypes);
|
|
267
|
-
const sql = sqlFromFeed(feed, this.schema, bookmark, 100, factTypes, roleMap);
|
|
267
|
+
const sql = sqlFromFeed(feed, start, this.schema, bookmark, 100, factTypes, roleMap);
|
|
268
268
|
if (!sql) {
|
|
269
269
|
return {
|
|
270
270
|
tuples: [],
|
|
@@ -228,7 +228,7 @@ class DescriptionBuilder {
|
|
|
228
228
|
return true;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
buildDescription(feed: Feed): QueryDescription {
|
|
231
|
+
buildDescription(feed: Feed, start: FactReference[]): QueryDescription {
|
|
232
232
|
const parameters: (string | number)[] = [];
|
|
233
233
|
function addParameter(value: string | number) {
|
|
234
234
|
parameters.push(value);
|
|
@@ -237,7 +237,7 @@ class DescriptionBuilder {
|
|
|
237
237
|
|
|
238
238
|
// Allocate parameters for the inputs.
|
|
239
239
|
const inputs: InputDescription[] = feed.inputs.map(input =>
|
|
240
|
-
this.buildInputDescription(feed, input, addParameter)
|
|
240
|
+
this.buildInputDescription(feed, input, start, addParameter)
|
|
241
241
|
);
|
|
242
242
|
|
|
243
243
|
// Allocate parameters for the edge roles.
|
|
@@ -266,7 +266,7 @@ class DescriptionBuilder {
|
|
|
266
266
|
);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
private buildInputDescription(feed: Feed, input: FeedInputDescription, addParameter: (value: string | number) => number): InputDescription {
|
|
269
|
+
private buildInputDescription(feed: Feed, input: FeedInputDescription, start: FactReference[], addParameter: (value: string | number) => number): InputDescription {
|
|
270
270
|
const fact = feed.facts.find(f => f.factIndex === input.factIndex);
|
|
271
271
|
if (!fact) {
|
|
272
272
|
throw new Error(`Fact not found: ${input.factIndex}`);
|
|
@@ -278,7 +278,7 @@ class DescriptionBuilder {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
const factTypeParameter = addParameter(factTypeId);
|
|
281
|
-
const factHashParameter = addParameter(input.
|
|
281
|
+
const factHashParameter = addParameter(start[input.inputIndex].hash);
|
|
282
282
|
return {
|
|
283
283
|
factIndex: input.factIndex,
|
|
284
284
|
factTypeParameter,
|
|
@@ -335,7 +335,18 @@ class DescriptionBuilder {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
export function sqlFromSpecification(start: FactReference[], schema: string, bookmarks: string[], limit: number, specification: Specification, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery[] {
|
|
338
|
-
|
|
338
|
+
// Verify that the number of start facts equals the number of inputs
|
|
339
|
+
if (start.length !== specification.given.length) {
|
|
340
|
+
throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
|
|
341
|
+
}
|
|
342
|
+
// Verify that the input type matches the start fact type
|
|
343
|
+
for (let i = 0; i < start.length; i++) {
|
|
344
|
+
if (start[i].type !== specification.given[i].type) {
|
|
345
|
+
throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const feeds = buildFeeds(specification);
|
|
339
350
|
const descriptionBuilder = new DescriptionBuilder(factTypes, roleMap);
|
|
340
351
|
const feedAndBookmark = feeds.map((feed, index) => ({
|
|
341
352
|
feed,
|
|
@@ -344,19 +355,19 @@ export function sqlFromSpecification(start: FactReference[], schema: string, boo
|
|
|
344
355
|
const satisfiableFeedsAndBookmarks = feedAndBookmark.filter(fb =>
|
|
345
356
|
descriptionBuilder.isSatisfiable(fb.feed, fb.feed.edges));
|
|
346
357
|
const sqlQueries = satisfiableFeedsAndBookmarks.map(fb => {
|
|
347
|
-
const description = descriptionBuilder.buildDescription(fb.feed);
|
|
358
|
+
const description = descriptionBuilder.buildDescription(fb.feed, start);
|
|
348
359
|
const sql = description.generateSqlQuery(schema, fb.bookmark, limit);
|
|
349
360
|
return sql;
|
|
350
361
|
});
|
|
351
362
|
return sqlQueries;
|
|
352
363
|
}
|
|
353
364
|
|
|
354
|
-
export function sqlFromFeed(feed: Feed, schema: string, bookmark: string, limit: number, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery | null {
|
|
365
|
+
export function sqlFromFeed(feed: Feed, start: FactReference[], schema: string, bookmark: string, limit: number, factTypes: Map<string, number>, roleMap: Map<number, Map<string, number>>): SpecificationSqlQuery | null {
|
|
355
366
|
const descriptionBuilder = new DescriptionBuilder(factTypes, roleMap);
|
|
356
367
|
if (!descriptionBuilder.isSatisfiable(feed, feed.edges)) {
|
|
357
368
|
return null;
|
|
358
369
|
}
|
|
359
|
-
const description = descriptionBuilder.buildDescription(feed);
|
|
370
|
+
const description = descriptionBuilder.buildDescription(feed, start);
|
|
360
371
|
const sql = description.generateSqlQuery(schema, bookmark, limit);
|
|
361
372
|
return sql;
|
|
362
373
|
}
|
|
@@ -167,7 +167,7 @@ function givenAuthorizationWithStorage(storage: MemoryStore) {
|
|
|
167
167
|
.type('Like', j.for(likeUser))
|
|
168
168
|
.type('Delete', j.for(deleteSender))
|
|
169
169
|
;
|
|
170
|
-
return new AuthorizationKeystore(storage, keystore, authorizationRules);
|
|
170
|
+
return new AuthorizationKeystore(storage, keystore, authorizationRules, null);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function givenOtherUser() {
|