jinaga-server 2.3.5 → 2.4.1

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 (40) 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 +67 -23
  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-sql.d.ts +1 -1
  22. package/dist/postgres/specification-sql.js +18 -8
  23. package/dist/postgres/specification-sql.js.map +1 -1
  24. package/integration-test/jinaga-test/Dockerfile +1 -1
  25. package/integration-test/jinaga-test/package-lock.json +38 -25
  26. package/integration-test/jinaga-test/package.json +3 -3
  27. package/integration-test/postgres/Dockerfile +1 -1
  28. package/package.json +11 -6
  29. package/replicator/index.ts +1 -1
  30. package/replicator/package-lock.json +25 -16
  31. package/replicator/package.json +1 -1
  32. package/src/authorization/authorization-keystore.ts +70 -9
  33. package/src/authorization/distributed-fact-cache.ts +34 -0
  34. package/src/http/feed-cache.ts +11 -3
  35. package/src/http/router.ts +105 -36
  36. package/src/jinaga-server.ts +9 -5
  37. package/src/memory/memory-feed-cache.ts +6 -7
  38. package/src/postgres/postgres-store.ts +2 -2
  39. package/src/postgres/specification-sql.ts +19 -8
  40. package/test/authorization/authorizationSpec.ts +1 -1
@@ -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,15 @@ import {
20
19
  SpecificationParser,
21
20
  Trace,
22
21
  UserIdentity,
22
+ buildFeeds,
23
+ computeObjectHash,
24
+ fromDescriptiveString,
25
+ parseLoadMessage,
26
+ parseQueryMessage,
27
+ parseSaveMessage,
23
28
  } from "jinaga";
24
- import { FeedResponse, FeedsResponse } from "jinaga/dist/http/messages";
25
29
 
26
- import { FeedCache } from "./feed-cache";
30
+ import { FeedCache, FeedDefinition } from "./feed-cache";
27
31
 
28
32
  interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
29
33
 
@@ -43,8 +47,14 @@ function get<U>(method: ((req: RequestUser, params: { [key: string]: string }, q
43
47
  }
44
48
  })
45
49
  .catch(error => {
46
- Trace.error(error);
47
- res.status(500).send(error.message);
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
+ }
48
58
  next();
49
59
  });
50
60
  };
@@ -72,10 +82,13 @@ function getAuthenticate<U>(method: ((req: RequestUser, params?: { [key: string]
72
82
  };
73
83
  }
74
84
 
75
- function post<T, U>(method: (user: RequestUser, message: T, params?: { [key: string]: string }) => Promise<U>): Handler {
85
+ function post<T, U>(
86
+ parse: (input: any) => T,
87
+ method: (user: RequestUser, message: T, params?: { [key: string]: string }) => Promise<U>
88
+ ): Handler {
76
89
  return (req, res, next) => {
77
90
  const user = <RequestUser>req.user;
78
- const message = <T>req.body;
91
+ const message = parse(req.body);
79
92
  if (!message) {
80
93
  throw new Error('Ensure that you have called app.use(express.json()).');
81
94
  }
@@ -110,7 +123,7 @@ function post<T, U>(method: (user: RequestUser, message: T, params?: { [key: str
110
123
  function postString<U>(method: (user: RequestUser, message: string) => Promise<U>): Handler {
111
124
  return (req, res, next) => {
112
125
  const user = <RequestUser>req.user;
113
- const input = <string>req.body;
126
+ const input = parseString(req.body);
114
127
  if (!input || typeof(input) !== 'string') {
115
128
  res.type("text");
116
129
  res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
@@ -139,10 +152,13 @@ function postString<U>(method: (user: RequestUser, message: string) => Promise<U
139
152
  };
140
153
  }
141
154
 
142
- function postCreate<T>(method: (user: RequestUser, message: T) => Promise<void>): Handler {
155
+ function postCreate<T>(
156
+ parse: (input: any) => T,
157
+ method: (user: RequestUser, message: T) => Promise<void>
158
+ ): Handler {
143
159
  return (req, res, next) => {
144
160
  const user = <RequestUser>req.user;
145
- const message = <T>req.body;
161
+ const message = parse(req.body);
146
162
  if (!message) {
147
163
  throw new Error('Ensure that you have called app.use(express.json()).');
148
164
  }
@@ -170,7 +186,7 @@ function postCreate<T>(method: (user: RequestUser, message: T) => Promise<void>)
170
186
  function postStringCreate(method: (user: RequestUser, message: string) => Promise<void>): Handler {
171
187
  return (req, res, next) => {
172
188
  const user = <RequestUser>req.user;
173
- const input = <string>req.body;
189
+ const input = parseString(req.body);
174
190
  if (!input || typeof(input) !== 'string') {
175
191
  res.type("text");
176
192
  res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
@@ -198,7 +214,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
198
214
  };
199
215
  }
200
216
 
201
- function serializeUserIdentity(user: RequestUser) : UserIdentity | null {
217
+ function serializeUserIdentity(user: RequestUser | null) : UserIdentity | null {
202
218
  if (!user) {
203
219
  return null;
204
220
  }
@@ -217,16 +233,30 @@ export interface RequestUser {
217
233
  export class HttpRouter {
218
234
  handler: Handler;
219
235
 
220
- constructor(private authorization: Authorization, private feedCache: FeedCache) {
236
+ constructor(private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
221
237
  const router = Router();
222
238
  router.get('/login', getAuthenticate(user => this.login(user)));
223
- router.post('/query', post((user, queryMessage: QueryMessage) => this.query(user, queryMessage)));
224
- router.post('/load', post((user, loadMessage: LoadMessage) => this.load(user, loadMessage)));
225
- router.post('/save', postCreate((user, saveMessage: SaveMessage) => this.save(user, saveMessage)));
239
+ if (backwardCompatible) {
240
+ router.post('/query', post(
241
+ parseQueryMessage,
242
+ (user, queryMessage) => this.query(user, queryMessage)
243
+ ));
244
+ }
245
+ router.post('/load', post(
246
+ parseLoadMessage,
247
+ (user, loadMessage) => this.load(user, loadMessage)
248
+ ));
249
+ router.post('/save', postCreate(
250
+ parseSaveMessage,
251
+ (user, saveMessage) => this.save(user, saveMessage)
252
+ ));
226
253
 
227
254
  router.post('/read', postString((user, input: string) => this.read(user, input)));
228
255
  router.post('/write', postStringCreate((user, input: string) => this.write(user, input)));
229
- router.post('/feeds', post((user, input: string) => this.feeds(user, input)));
256
+ router.post('/feeds', post(
257
+ parseString,
258
+ (user, input: string) => this.feeds(user, input)
259
+ ));
230
260
  router.get('/feeds/:hash', get((user, params, query) => this.feed(user, params, query)));
231
261
 
232
262
  this.handler = router;
@@ -243,7 +273,7 @@ export class HttpRouter {
243
273
  };
244
274
  }
245
275
 
246
- private async query(user: RequestUser, queryMessage: QueryMessage) : Promise<QueryResponse> {
276
+ private async query(user: RequestUser | null, queryMessage: QueryMessage) : Promise<QueryResponse> {
247
277
  const userIdentity = serializeUserIdentity(user);
248
278
  const query = fromDescriptiveString(queryMessage.query);
249
279
  const result = await this.authorization.query(userIdentity, queryMessage.start, query);
@@ -260,17 +290,18 @@ export class HttpRouter {
260
290
  };
261
291
  }
262
292
 
263
- private async save(user: RequestUser, saveMessage: SaveMessage) : Promise<void> {
293
+ private async save(user: RequestUser | null, saveMessage: SaveMessage) : Promise<void> {
264
294
  const userIdentity = serializeUserIdentity(user);
265
295
  await this.authorization.save(userIdentity, saveMessage.facts);
266
296
  }
267
297
 
268
- private async read(user: RequestUser, input: string): Promise<any[]> {
298
+ private async read(user: RequestUser | null, input: string): Promise<any[]> {
269
299
  const knownFacts = await this.getKnownFacts(user);
270
300
  const parser = new SpecificationParser(input);
271
301
  parser.skipWhitespace();
272
302
  const declaration = parser.parseDeclaration(knownFacts);
273
303
  const specification = parser.parseSpecification();
304
+ parser.expectEnd();
274
305
  const start = this.selectStart(specification, declaration);
275
306
 
276
307
  const userIdentity = serializeUserIdentity(user);
@@ -278,11 +309,12 @@ export class HttpRouter {
278
309
  return extractResults(results);
279
310
  }
280
311
 
281
- private async write(user: RequestUser, input: string): Promise<void> {
312
+ private async write(user: RequestUser | null, input: string): Promise<void> {
282
313
  const knownFacts = await this.getKnownFacts(user);
283
314
  const parser = new SpecificationParser(input);
284
315
  parser.skipWhitespace();
285
316
  var declaration = parser.parseDeclaration(knownFacts);
317
+ parser.expectEnd();
286
318
 
287
319
  const factRecords: FactRecord[] = [];
288
320
  for (const value of declaration) {
@@ -296,7 +328,7 @@ export class HttpRouter {
296
328
  await this.authorization.save(userIdentity, factRecords);
297
329
  }
298
330
 
299
- private async feeds(user: RequestUser, input: string): Promise<FeedsResponse> {
331
+ private async feeds(user: RequestUser | null, input: string): Promise<FeedsResponse> {
300
332
  const knownFacts = await this.getKnownFacts(user);
301
333
  const parser = new SpecificationParser(input);
302
334
  parser.skipWhitespace();
@@ -304,35 +336,65 @@ export class HttpRouter {
304
336
  const specification = parser.parseSpecification();
305
337
  const start = this.selectStart(specification, declaration);
306
338
 
307
- const feedDefinitions = buildFeeds(start, specification).map(feed => ({
308
- hash: urlSafeHash(feed),
309
- feed
310
- }));
339
+ // Verify that the number of start facts equals the number of inputs
340
+ if (start.length !== specification.given.length) {
341
+ throw new Error(`The number of start facts (${start.length}) does not equal the number of inputs (${specification.given.length})`);
342
+ }
343
+ // Verify that the input type matches the start fact type
344
+ for (let i = 0; i < start.length; i++) {
345
+ if (start[i].type !== specification.given[i].type) {
346
+ 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
+ }
348
+ }
349
+
350
+ // Verify that I can distribute all feeds to the user.
351
+ const feeds = buildFeeds(specification);
352
+ const userIdentity = serializeUserIdentity(user);
353
+ await this.authorization.verifyDistribution(userIdentity, feeds, start);
354
+
355
+ const feedDefinitionsByHash = feeds.map(feed => {
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
+ });
311
369
  // Store all feeds in the cache.
312
- for (const feedDefinition of feedDefinitions) {
313
- await this.feedCache.storeFeed(feedDefinition.hash, feedDefinition.feed);
370
+ for (const d of feedDefinitionsByHash) {
371
+ await this.feedCache.storeFeed(d.hash, d.feedDefinition);
314
372
  }
315
373
 
316
374
  return {
317
- feeds: feedDefinitions.map(f => f.hash)
375
+ feeds: feedDefinitionsByHash.map(f => f.hash)
318
376
  }
319
377
  }
320
378
 
321
- private async feed(user: RequestUser, params: { [key: string]: string }, query: ParsedQs): Promise<FeedResponse | null> {
379
+ private async feed(user: RequestUser | null, params: { [key: string]: string }, query: ParsedQs): Promise<FeedResponse | null> {
322
380
  const feedHash = params["hash"];
323
381
  if (!feedHash) {
324
382
  return null;
325
383
  }
326
384
 
327
- const feed = await this.feedCache.getFeed(feedHash);
328
- if (!feed) {
385
+ const feedDefinition = await this.feedCache.getFeed(feedHash);
386
+ if (!feedDefinition) {
329
387
  return null;
330
388
  }
331
389
 
332
390
  const bookmark = query["b"] as string ?? "";
333
391
 
334
392
  const userIdentity = serializeUserIdentity(user);
335
- const results = await this.authorization.feed(userIdentity, feed, bookmark);
393
+ const start = feedDefinition.start.reduce((start, input) => {
394
+ start[input.index] = input.factReference;
395
+ return start;
396
+ }, [] as FactReference[]);
397
+ const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
336
398
  // Return distinct fact references from all the tuples.
337
399
  const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
338
400
  self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
@@ -344,7 +406,7 @@ export class HttpRouter {
344
406
  return response;
345
407
  }
346
408
 
347
- private async getKnownFacts(user: RequestUser): Promise<Declaration> {
409
+ private async getKnownFacts(user: RequestUser | null): Promise<Declaration> {
348
410
  if (user) {
349
411
  const userFact = await this.authorization.getOrCreateUserFact({
350
412
  provider: user.provider,
@@ -380,6 +442,13 @@ export class HttpRouter {
380
442
  }
381
443
  }
382
444
 
445
+ function parseString(input: any): string {
446
+ if (typeof input !== 'string') {
447
+ throw new Error("Expected a string. Check the content type of the request.");
448
+ }
449
+ return input;
450
+ }
451
+
383
452
  function urlSafeHash(feed: Feed): string {
384
453
  const base64 = computeObjectHash(feed);
385
454
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
@@ -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: [],
@@ -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() {