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.
Files changed (43) hide show
  1. package/.github/workflows/publish.yml +2 -0
  2. package/dist/authorization/authorization-keystore.d.ts +8 -5
  3. package/dist/authorization/authorization-keystore.js +64 -5
  4. package/dist/authorization/authorization-keystore.js.map +1 -1
  5. package/dist/authorization/distributed-fact-cache.d.ts +7 -0
  6. package/dist/authorization/distributed-fact-cache.js +28 -0
  7. package/dist/authorization/distributed-fact-cache.js.map +1 -0
  8. package/dist/http/feed-cache.d.ts +10 -3
  9. package/dist/http/router.d.ts +1 -1
  10. package/dist/http/router.js +50 -14
  11. package/dist/http/router.js.map +1 -1
  12. package/dist/jinaga-server.d.ts +2 -1
  13. package/dist/jinaga-server.js +6 -4
  14. package/dist/jinaga-server.js.map +1 -1
  15. package/dist/memory/memory-feed-cache.d.ts +4 -5
  16. package/dist/memory/memory-feed-cache.js +4 -4
  17. package/dist/memory/memory-feed-cache.js.map +1 -1
  18. package/dist/postgres/postgres-store.d.ts +1 -1
  19. package/dist/postgres/postgres-store.js +2 -2
  20. package/dist/postgres/postgres-store.js.map +1 -1
  21. package/dist/postgres/specification-result-sql.js +1 -1
  22. package/dist/postgres/specification-result-sql.js.map +1 -1
  23. package/dist/postgres/specification-sql.d.ts +1 -1
  24. package/dist/postgres/specification-sql.js +18 -8
  25. package/dist/postgres/specification-sql.js.map +1 -1
  26. package/integration-test/jinaga-test/Dockerfile +1 -1
  27. package/integration-test/jinaga-test/package-lock.json +38 -25
  28. package/integration-test/jinaga-test/package.json +3 -3
  29. package/integration-test/postgres/Dockerfile +1 -1
  30. package/package.json +11 -6
  31. package/replicator/index.ts +1 -1
  32. package/replicator/package-lock.json +25 -16
  33. package/replicator/package.json +1 -1
  34. package/src/authorization/authorization-keystore.ts +70 -9
  35. package/src/authorization/distributed-fact-cache.ts +34 -0
  36. package/src/http/feed-cache.ts +11 -3
  37. package/src/http/router.ts +66 -27
  38. package/src/jinaga-server.ts +9 -5
  39. package/src/memory/memory-feed-cache.ts +6 -7
  40. package/src/postgres/postgres-store.ts +2 -2
  41. package/src/postgres/specification-result-sql.ts +1 -1
  42. package/src/postgres/specification-sql.ts +19 -8
  43. package/test/authorization/authorizationSpec.ts +1 -1
@@ -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://dev:devpw@localhost:5432/replicator';
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": "^8.7.3"
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.8.0",
534
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
535
- "integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
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.5.0",
540
- "pg-pool": "^3.5.2",
541
- "pg-protocol": "^1.5.0",
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.5.0",
559
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
560
- "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
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.5.2",
572
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
573
- "integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
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.5.0",
580
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
581
- "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
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",
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "cors": "^2.8.5",
19
19
  "express": "^4.18.1",
20
- "pg": "^8.7.3"
20
+ "pg": "8.11"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/cors": "^2.8.12",
@@ -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
- return this.store.read(start, specification);
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
- return this.store.feed(feed, bookmark);
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
- return this.store.load(references);
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
+ }
@@ -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, feed: Feed): Promise<void>;
5
- getFeed(feedHash: string): Promise<Feed | undefined>;
12
+ storeFeed(feedHash: string, feedDefinition: FeedDefinition): Promise<void>;
13
+ getFeed(feedHash: string): Promise<FeedDefinition | undefined>;
6
14
  }
@@ -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
- Trace.error(error);
47
- res.status(500).send(error.message);
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
- router.post('/query', post((user, queryMessage: QueryMessage) => this.query(user, queryMessage)));
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
- const feedDefinitions = buildFeeds(start, specification).map(feed => ({
308
- hash: urlSafeHash(feed),
309
- feed
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 feedDefinition of feedDefinitions) {
313
- await this.feedCache.storeFeed(feedDefinition.hash, feedDefinition.feed);
347
+ for (const d of feedDefinitionsByHash) {
348
+ await this.feedCache.storeFeed(d.hash, d.feedDefinition);
314
349
  }
315
350
 
316
351
  return {
317
- feeds: feedDefinitions.map(f => f.hash)
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 feed = await this.feedCache.getFeed(feedHash);
328
- if (!feed) {
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 results = await this.authorization.feed(userIdentity, feed, bookmark);
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,
@@ -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 authorization = createAuthorization(authorizationRules, store, keystore);
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
- const router = new HttpRouter(authorization, feedCache);
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 { Feed } from "jinaga";
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 feeds: { [feedHash: string]: Feed } = {};
4
+ private feedDefinitions: { [feedHash: string]: FeedDefinition } = {};
6
5
 
7
- storeFeed(feedHash: string, feed: Feed): Promise<void> {
8
- this.feeds[feedHash] = feed;
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<Feed | undefined> {
13
- return Promise.resolve(this.feeds[feedHash]);
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: [],
@@ -222,7 +222,7 @@ class QueryDescription {
222
222
  }
223
223
 
224
224
  outputLength(): number {
225
- return this.outputs.length;
225
+ return this.inputs.length + this.outputs.length;
226
226
  }
227
227
 
228
228
  generateResultSqlQuery(schema: string): SpecificationSqlQuery {
@@ -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.factHash);
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
- const feeds = buildFeeds(start, specification);
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() {