jinaga-server 2.6.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authentication/authentication-device.js +27 -6
- package/dist/authentication/authentication-device.js.map +1 -1
- package/dist/authentication/authentication-session.js +27 -6
- package/dist/authentication/authentication-session.js.map +1 -1
- package/dist/authorization/authorization-keystore.d.ts +4 -5
- package/dist/authorization/authorization-keystore.js +42 -18
- package/dist/authorization/authorization-keystore.js.map +1 -1
- package/dist/http/router.d.ts +1 -2
- package/dist/http/router.js +14 -18
- package/dist/http/router.js.map +1 -1
- package/dist/jinaga-server.js +5 -6
- package/dist/jinaga-server.js.map +1 -1
- package/dist/postgres/postgres-store.d.ts +2 -5
- package/dist/postgres/postgres-store.js +44 -121
- package/dist/postgres/postgres-store.js.map +1 -1
- package/http/sans-auth/Feed sites.http +1 -1
- package/integration-test/jinaga-test/jinaga.test.js +247 -231
- package/integration-test/jinaga-test/package-lock.json +16 -59
- package/integration-test/jinaga-test/package.json +2 -5
- package/package.json +3 -4
- package/src/authentication/authentication-device.ts +29 -7
- package/src/authentication/authentication-session.ts +29 -7
- package/src/authorization/authorization-keystore.ts +42 -22
- package/src/http/router.ts +14 -24
- package/src/jinaga-server.ts +5 -6
- package/src/postgres/postgres-store.ts +63 -149
- package/test/authorization/authorizationSpec.ts +75 -89
- package/dist/postgres/sql.d.ts +0 -10
- package/dist/postgres/sql.js +0 -408
- package/dist/postgres/sql.js.map +0 -1
- package/src/postgres/sql.ts +0 -498
- package/test/postgres/postgresSpec.ts +0 -391
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canonicalPredecessors,
|
|
3
|
-
Direction,
|
|
4
|
-
ExistentialCondition,
|
|
5
3
|
FactEnvelope,
|
|
6
4
|
FactFeed,
|
|
7
|
-
FactPath,
|
|
8
5
|
FactRecord,
|
|
9
6
|
FactReference,
|
|
10
7
|
factReferenceEquals,
|
|
8
|
+
FactSignature,
|
|
11
9
|
FactTuple,
|
|
12
10
|
getAllFactTypes,
|
|
13
11
|
getAllRoles,
|
|
14
|
-
Join,
|
|
15
12
|
PredecessorCollection,
|
|
16
13
|
ProjectedResult,
|
|
17
|
-
PropertyCondition,
|
|
18
|
-
Query,
|
|
19
14
|
Specification,
|
|
20
|
-
|
|
21
|
-
Storage,
|
|
22
|
-
TopologicalSorter
|
|
15
|
+
Storage
|
|
23
16
|
} from "jinaga";
|
|
24
17
|
import { Pool, PoolClient } from "pg";
|
|
25
18
|
|
|
@@ -51,7 +44,6 @@ import {
|
|
|
51
44
|
} from "./maps";
|
|
52
45
|
import { ResultSetFact, ResultSetRow, ResultSetTree, resultSqlFromSpecification, SqlQueryTree } from "./specification-result-sql";
|
|
53
46
|
import { sqlFromFeed } from "./specification-sql";
|
|
54
|
-
import { sqlFromSteps } from "./sql";
|
|
55
47
|
|
|
56
48
|
interface FactTypeResult {
|
|
57
49
|
rows: {
|
|
@@ -96,6 +88,8 @@ interface AncestorResult {
|
|
|
96
88
|
name: string;
|
|
97
89
|
hash: string;
|
|
98
90
|
data: string;
|
|
91
|
+
public_key: string;
|
|
92
|
+
signature: string;
|
|
99
93
|
}[];
|
|
100
94
|
}
|
|
101
95
|
|
|
@@ -125,21 +119,6 @@ function loadFactTuple(labels: SpecificationLabel[], row: Row): FactTuple {
|
|
|
125
119
|
};
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
function loadFactPath(pathLength: number, factTypeNames: string[], r: Row): FactPath {
|
|
129
|
-
let path: FactPath = [];
|
|
130
|
-
for (let i = 0; i < pathLength; i++) {
|
|
131
|
-
const hash = r['hash' + (i + 2)];
|
|
132
|
-
if (!hash) {
|
|
133
|
-
throw new Error(`Cannot find column 'hash${i + 2}'`);
|
|
134
|
-
}
|
|
135
|
-
path.push({
|
|
136
|
-
type: factTypeNames[i],
|
|
137
|
-
hash: hash
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return path;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
122
|
export class PostgresStore implements Storage {
|
|
144
123
|
private connectionFactory: ConnectionFactory;
|
|
145
124
|
private factTypeMap: FactTypeMap = emptyFactTypeMap();
|
|
@@ -204,32 +183,6 @@ export class PostgresStore implements Storage {
|
|
|
204
183
|
}
|
|
205
184
|
}
|
|
206
185
|
|
|
207
|
-
async query(start: FactReference, query: Query): Promise<FactPath[]> {
|
|
208
|
-
if (query.steps.length === 0) {
|
|
209
|
-
return [[start]];
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
const factTypes = await this.loadFactTypesFromSteps(query.steps, start.type);
|
|
214
|
-
const roleMap = await this.loadRolesFromSteps(query.steps, factTypes, start.type);
|
|
215
|
-
|
|
216
|
-
const sqlQuery = sqlFromSteps(start, query.steps, factTypes, roleMap, this.schema);
|
|
217
|
-
if (!sqlQuery) {
|
|
218
|
-
throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}"`);
|
|
219
|
-
}
|
|
220
|
-
if (sqlQuery.empty) {
|
|
221
|
-
return [];
|
|
222
|
-
}
|
|
223
|
-
const { rows } = await this.connectionFactory.with(async (connection) => {
|
|
224
|
-
return await connection.query(sqlQuery.sql, sqlQuery.parameters);
|
|
225
|
-
});
|
|
226
|
-
return rows.map(row => loadFactPath(sqlQuery.pathLength, sqlQuery.factTypeNames, row));
|
|
227
|
-
}
|
|
228
|
-
catch (e) {
|
|
229
|
-
throw new Error(`Could not generate SQL for query "${query.toDescriptiveString()}" starting at "${start.type}": ${e}`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
186
|
async read(start: FactReference[], specification: Specification): Promise<ProjectedResult[]> {
|
|
234
187
|
const factTypes = await this.loadFactTypesFromSpecification(specification);
|
|
235
188
|
const roleMap = await this.loadRolesFromSpecification(specification, factTypes);
|
|
@@ -253,7 +206,8 @@ export class PostgresStore implements Storage {
|
|
|
253
206
|
const factReferences: FactReference[] = composer.findFactReferences(resultSets);
|
|
254
207
|
|
|
255
208
|
// Load the references into a fact tree
|
|
256
|
-
const
|
|
209
|
+
const factEnvelopes = await this.load(factReferences);
|
|
210
|
+
const factRecords = factEnvelopes.map(envelope => envelope.fact);
|
|
257
211
|
|
|
258
212
|
return composer.compose(resultSets, factRecords);
|
|
259
213
|
}
|
|
@@ -279,22 +233,6 @@ export class PostgresStore implements Storage {
|
|
|
279
233
|
};
|
|
280
234
|
}
|
|
281
235
|
|
|
282
|
-
private async loadFactTypesFromSteps(steps: Step[], startType: string): Promise<FactTypeMap> {
|
|
283
|
-
const factTypes = this.factTypeMap;
|
|
284
|
-
const unknownFactTypes = [...allFactTypes(steps), startType]
|
|
285
|
-
.filter(factType => !factTypes.has(factType))
|
|
286
|
-
.filter(distinct);
|
|
287
|
-
if (unknownFactTypes.length > 0) {
|
|
288
|
-
const loadedFactTypes = await this.connectionFactory.with(async (connection) => {
|
|
289
|
-
return await loadFactTypes(unknownFactTypes, connection, this.schema);
|
|
290
|
-
});
|
|
291
|
-
const merged = mergeFactTypes(this.factTypeMap, loadedFactTypes);
|
|
292
|
-
this.factTypeMap = merged;
|
|
293
|
-
return merged;
|
|
294
|
-
}
|
|
295
|
-
return factTypes;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
236
|
async loadFactTypesFromFeed(feed: Specification): Promise<FactTypeMap> {
|
|
299
237
|
const factTypes = this.factTypeMap;
|
|
300
238
|
const unknownFactTypes = getAllFactTypes(feed)
|
|
@@ -325,21 +263,6 @@ export class PostgresStore implements Storage {
|
|
|
325
263
|
return factTypes;
|
|
326
264
|
}
|
|
327
265
|
|
|
328
|
-
private async loadRolesFromSteps(steps: Step[], factTypes: FactTypeMap, initialType: string) {
|
|
329
|
-
const roleMap = this.roleMap;
|
|
330
|
-
const unknownRoles = allRoles(steps, factTypes, getFactTypeId(factTypes, initialType))
|
|
331
|
-
.filter(r => !hasRole(roleMap, r.successor_type_id, r.role));
|
|
332
|
-
if (unknownRoles.length > 0) {
|
|
333
|
-
const loadedRoles = await this.connectionFactory.with(async (connection) => {
|
|
334
|
-
return await loadRoles(unknownRoles, roleMap, connection, this.schema);
|
|
335
|
-
});
|
|
336
|
-
const merged = mergeRoleMaps(this.roleMap, loadedRoles);
|
|
337
|
-
this.roleMap = merged;
|
|
338
|
-
return merged;
|
|
339
|
-
}
|
|
340
|
-
return roleMap;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
266
|
async loadRolesFromSpecification(specification: Specification, factTypes: FactTypeMap): Promise<RoleMap> {
|
|
344
267
|
const roleMap = this.roleMap;
|
|
345
268
|
const unknownRoles = getAllRoles(specification)
|
|
@@ -407,7 +330,7 @@ export class PostgresStore implements Storage {
|
|
|
407
330
|
return existing;
|
|
408
331
|
}
|
|
409
332
|
|
|
410
|
-
async load(references: FactReference[]): Promise<
|
|
333
|
+
async load(references: FactReference[]): Promise<FactEnvelope[]> {
|
|
411
334
|
if (references.length === 0) {
|
|
412
335
|
return [];
|
|
413
336
|
}
|
|
@@ -419,23 +342,30 @@ export class PostgresStore implements Storage {
|
|
|
419
342
|
const factParameters = flatten(references, (f) =>
|
|
420
343
|
[f.hash, factTypes.get(f.type)]);
|
|
421
344
|
const sql =
|
|
422
|
-
'SELECT f.fact_type_id,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
345
|
+
'SELECT f.fact_type_id, f.name, f.hash, f.data, s.public_key, s.signature ' +
|
|
346
|
+
'FROM (SELECT f.fact_id, f.fact_type_id, t.name, f.hash, f.data ' +
|
|
347
|
+
`FROM ${this.schema}.fact f ` +
|
|
348
|
+
`JOIN ${this.schema}.fact_type t ` +
|
|
349
|
+
' ON f.fact_type_id = t.fact_type_id ' +
|
|
350
|
+
'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
|
|
351
|
+
' ON v.fact_type_id = f.fact_type_id AND v.hash = f.hash ' +
|
|
352
|
+
'UNION ' +
|
|
353
|
+
'SELECT f2.fact_id, f2.fact_type_id, t.name, f2.hash, f2.data ' +
|
|
354
|
+
`FROM ${this.schema}.fact f1 ` +
|
|
355
|
+
'JOIN (VALUES ' + factValues.join(', ') + ') AS v (hash, fact_type_id) ' +
|
|
356
|
+
' ON v.fact_type_id = f1.fact_type_id AND v.hash = f1.hash ' +
|
|
357
|
+
`JOIN ${this.schema}.ancestor a ` +
|
|
358
|
+
' ON a.fact_id = f1.fact_id ' +
|
|
359
|
+
`JOIN ${this.schema}.fact f2 ` +
|
|
360
|
+
' ON f2.fact_id = a.ancestor_fact_id ' +
|
|
361
|
+
`JOIN ${this.schema}.fact_type t ` +
|
|
362
|
+
' ON t.fact_type_id = f2.fact_type_id) as f ' +
|
|
363
|
+
'LEFT JOIN (SELECT p.public_key, s.signature, s.fact_id ' +
|
|
364
|
+
`FROM ${this.schema}.signature s ` +
|
|
365
|
+
`JOIN ${this.schema}.public_key p ` +
|
|
366
|
+
' ON p.public_key_id = s.public_key_id) as s ' +
|
|
367
|
+
'ON s.fact_id = f.fact_id ' +
|
|
368
|
+
'ORDER BY f.fact_id, s.public_key;';
|
|
439
369
|
const result: AncestorResult = await this.connectionFactory.with(async (connection) => {
|
|
440
370
|
return await connection.query(sql, factParameters);
|
|
441
371
|
})
|
|
@@ -444,17 +374,46 @@ export class PostgresStore implements Storage {
|
|
|
444
374
|
emptyFactTypeMap()
|
|
445
375
|
);
|
|
446
376
|
this.factTypeMap = mergeFactTypes(this.factTypeMap, resultFactTypes);
|
|
447
|
-
const sorter = new TopologicalSorter<FactRecord>();
|
|
448
377
|
const records = result.rows.map((r) => {
|
|
449
378
|
const { fields, predecessors }: { fields: {}, predecessors: PredecessorCollection } = r.data as any;
|
|
450
|
-
|
|
379
|
+
const fact: FactRecord = {
|
|
451
380
|
type: r.name,
|
|
452
381
|
hash: r.hash,
|
|
453
382
|
fields,
|
|
454
383
|
predecessors
|
|
384
|
+
};
|
|
385
|
+
const signature: FactSignature = {
|
|
386
|
+
publicKey: r.public_key,
|
|
387
|
+
signature: r.signature
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
fact,
|
|
391
|
+
signature
|
|
455
392
|
}
|
|
456
393
|
});
|
|
457
|
-
|
|
394
|
+
// Group the records by fact type and hash.
|
|
395
|
+
const envelopes: FactEnvelope[] = records.reduce((acc: FactEnvelope[], record) => {
|
|
396
|
+
// Because the records are sorted by fact ID, we can just look at the last envelope.
|
|
397
|
+
const last = acc.length > 0 ? acc[acc.length - 1] : undefined;
|
|
398
|
+
if (last && last.fact.type === record.fact.type && last.fact.hash === record.fact.hash) {
|
|
399
|
+
last.signatures.push(record.signature);
|
|
400
|
+
}
|
|
401
|
+
else if (record.signature.signature === null) {
|
|
402
|
+
acc.push({
|
|
403
|
+
fact: record.fact,
|
|
404
|
+
signatures: []
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
acc.push({
|
|
409
|
+
fact: record.fact,
|
|
410
|
+
signatures: [record.signature]
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return acc;
|
|
414
|
+
}, []);
|
|
415
|
+
// Assuming that the facts were inserted in topological order, they don't need to be sorted.
|
|
416
|
+
return envelopes;
|
|
458
417
|
}
|
|
459
418
|
|
|
460
419
|
private async loadFactTypesFromReferences(references: FactReference[]): Promise<FactTypeMap> {
|
|
@@ -894,51 +853,6 @@ async function insertSignatures(envelopes: FactEnvelope[], allFacts: FactMap, fa
|
|
|
894
853
|
}
|
|
895
854
|
}
|
|
896
855
|
|
|
897
|
-
function allFactTypes(steps: Step[]): string[] {
|
|
898
|
-
const factTypes = steps
|
|
899
|
-
.filter(step => step instanceof PropertyCondition && step.name === 'type')
|
|
900
|
-
.map(step => (step as PropertyCondition).value);
|
|
901
|
-
const childFactTypes = steps
|
|
902
|
-
.filter(step => step instanceof ExistentialCondition)
|
|
903
|
-
.flatMap(step => allFactTypes((step as ExistentialCondition).steps));
|
|
904
|
-
return [...factTypes, ...childFactTypes].filter(distinct);
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function allRoles(steps: Step[], factTypes: FactTypeMap, initialTypeId: number | undefined) {
|
|
908
|
-
let roles: { successor_type_id: number; role: string }[] = [];
|
|
909
|
-
let successor_type_id = initialTypeId;
|
|
910
|
-
let role: string | undefined = undefined;
|
|
911
|
-
|
|
912
|
-
for (const step of steps) {
|
|
913
|
-
if (step instanceof PropertyCondition) {
|
|
914
|
-
if (step.name === 'type') {
|
|
915
|
-
successor_type_id = getFactTypeId(factTypes, step.value);
|
|
916
|
-
if (successor_type_id && role) {
|
|
917
|
-
roles.push({ successor_type_id: successor_type_id, role });
|
|
918
|
-
}
|
|
919
|
-
role = undefined;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
else if (step instanceof Join) {
|
|
923
|
-
if (step.direction === Direction.Predecessor) {
|
|
924
|
-
if (successor_type_id) {
|
|
925
|
-
roles.push({ successor_type_id: successor_type_id, role: step.role });
|
|
926
|
-
}
|
|
927
|
-
role = undefined;
|
|
928
|
-
}
|
|
929
|
-
else {
|
|
930
|
-
role = step.role;
|
|
931
|
-
}
|
|
932
|
-
successor_type_id = undefined;
|
|
933
|
-
}
|
|
934
|
-
else if (step instanceof ExistentialCondition) {
|
|
935
|
-
roles = roles.concat(allRoles(step.steps, factTypes, successor_type_id));
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return roles;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
856
|
function predecessorsOf(fact: FactRecord): FactReference[] {
|
|
943
857
|
const references = Object.values(fact.predecessors).flatMap(predecessor =>
|
|
944
858
|
Array.isArray(predecessor) ? predecessor : [predecessor]
|
|
@@ -1,21 +1,59 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AuthenticationNoOp,
|
|
3
2
|
AuthorizationRules,
|
|
3
|
+
buildModel,
|
|
4
4
|
dehydrateFact,
|
|
5
|
-
|
|
5
|
+
FactEnvelope,
|
|
6
6
|
FactManager,
|
|
7
7
|
FactRecord,
|
|
8
8
|
hydrate,
|
|
9
|
-
Jinaga as j,
|
|
10
9
|
MemoryStore,
|
|
11
10
|
NetworkNoOp,
|
|
12
11
|
ObservableSource,
|
|
13
|
-
PassThroughFork
|
|
12
|
+
PassThroughFork,
|
|
13
|
+
User
|
|
14
14
|
} from "jinaga";
|
|
15
15
|
|
|
16
16
|
import { AuthorizationKeystore } from "../../src/authorization/authorization-keystore";
|
|
17
17
|
import { MemoryKeystore } from "../../src/memory/memory-keystore";
|
|
18
18
|
|
|
19
|
+
class Hashtag {
|
|
20
|
+
public static Type = "Hashtag" as const;
|
|
21
|
+
public type = Hashtag.Type;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
public word: string
|
|
25
|
+
) { }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Tweet {
|
|
29
|
+
public static Type = "Tweet" as const;
|
|
30
|
+
public type = Tweet.Type;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
public message: string,
|
|
34
|
+
public sender: User
|
|
35
|
+
) { }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Like {
|
|
39
|
+
public static Type = "Like" as const;
|
|
40
|
+
public type = Like.Type;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
public user: User,
|
|
44
|
+
public tweet: Tweet
|
|
45
|
+
) { }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class Delete {
|
|
49
|
+
public static Type = "Delete" as const;
|
|
50
|
+
public type = Delete.Type;
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
public tweet: Tweet
|
|
54
|
+
) { }
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
describe('Authorization', () => {
|
|
20
58
|
it('should authorize empty save', async () => {
|
|
21
59
|
const authorization = givenAuthorization();
|
|
@@ -25,19 +63,13 @@ describe('Authorization', () => {
|
|
|
25
63
|
|
|
26
64
|
it('should save a new fact', async () => {
|
|
27
65
|
const authorization = givenAuthorization();
|
|
28
|
-
const result = await whenSave(authorization, dehydrateFact(
|
|
29
|
-
type: 'Hashtag',
|
|
30
|
-
word: 'vorpal'
|
|
31
|
-
}));
|
|
66
|
+
const result = await whenSave(authorization, dehydrateFact(new Hashtag('vorpal')));
|
|
32
67
|
expect(result.length).toEqual(1);
|
|
33
68
|
});
|
|
34
69
|
|
|
35
70
|
it('should save a fact once', async () => {
|
|
36
71
|
const authorization = givenAuthorization();
|
|
37
|
-
const facts = dehydrateFact(
|
|
38
|
-
type: 'Hashtag',
|
|
39
|
-
word: 'vorpal'
|
|
40
|
-
});
|
|
72
|
+
const facts = dehydrateFact(new Hashtag('vorpal'));
|
|
41
73
|
await whenSave(authorization, facts);
|
|
42
74
|
const result = await whenSave(authorization, facts);
|
|
43
75
|
expect(result.length).toEqual(0);
|
|
@@ -46,64 +78,36 @@ describe('Authorization', () => {
|
|
|
46
78
|
it('should reject a fact from an unauthorized user', async () => {
|
|
47
79
|
const authorization = givenAuthorization();
|
|
48
80
|
const mickeyMouse = givenOtherUser();
|
|
49
|
-
const promise = whenSave(authorization, dehydrateFact(
|
|
50
|
-
type: 'Tweet',
|
|
51
|
-
message: 'Twas brillig',
|
|
52
|
-
sender: mickeyMouse
|
|
53
|
-
}));
|
|
81
|
+
const promise = whenSave(authorization, dehydrateFact(new Tweet("Twas brillig", mickeyMouse)));
|
|
54
82
|
await expect(promise).rejects.not.toBeNull();
|
|
55
83
|
});
|
|
56
84
|
|
|
57
85
|
it('should accept a fact from an authorized user', async () => {
|
|
58
86
|
const authorization = givenAuthorization();
|
|
59
87
|
const lewiscarrol = await givenLoggedInUser(authorization);
|
|
60
|
-
const result = await whenSave(authorization, dehydrateFact(
|
|
61
|
-
type: 'Tweet',
|
|
62
|
-
message: 'Twas brillig',
|
|
63
|
-
sender: lewiscarrol
|
|
64
|
-
}));
|
|
88
|
+
const result = await whenSave(authorization, dehydrateFact(new Tweet("Twas brillig", lewiscarrol)));
|
|
65
89
|
expect(result.length).toBeGreaterThan(0);
|
|
66
90
|
});
|
|
67
91
|
|
|
68
92
|
it('should accept a predecessor from an authorized user', async () => {
|
|
69
93
|
const authorization = givenAuthorization();
|
|
70
94
|
const lewiscarrol = await givenLoggedInUser(authorization);
|
|
71
|
-
const result = await whenSave(authorization, dehydrateFact(
|
|
72
|
-
|
|
73
|
-
user: lewiscarrol,
|
|
74
|
-
tweet: {
|
|
75
|
-
type: 'Tweet',
|
|
76
|
-
message: 'Twas Brillig',
|
|
77
|
-
sender: lewiscarrol
|
|
78
|
-
}
|
|
79
|
-
}));
|
|
80
|
-
expect(result.filter(r => r.type === 'Tweet').length).toEqual(1);
|
|
95
|
+
const result = await whenSave(authorization, dehydrateFact(new Like(lewiscarrol, new Tweet('Twas Brillig', lewiscarrol))));
|
|
96
|
+
expect(result.filter(r => r.fact.type === 'Tweet').length).toEqual(1);
|
|
81
97
|
});
|
|
82
98
|
|
|
83
99
|
it('should reject a predecessor from an unauthorized user', async () => {
|
|
84
100
|
const authorization = givenAuthorization();
|
|
85
101
|
const lewiscarrol = await givenLoggedInUser(authorization);
|
|
86
102
|
const mickeyMouse = givenOtherUser();
|
|
87
|
-
const promise = whenSave(authorization, dehydrateFact(
|
|
88
|
-
type: 'Like',
|
|
89
|
-
user: lewiscarrol,
|
|
90
|
-
tweet: {
|
|
91
|
-
type: 'Tweet',
|
|
92
|
-
message: 'Hiya, Pal!',
|
|
93
|
-
sender: mickeyMouse
|
|
94
|
-
}
|
|
95
|
-
}));
|
|
103
|
+
const promise = whenSave(authorization, dehydrateFact(new Like(lewiscarrol, new Tweet("Hiya, Pal!", mickeyMouse))));
|
|
96
104
|
await expect(promise).rejects.not.toBeNull();
|
|
97
105
|
});
|
|
98
106
|
|
|
99
107
|
it('should accept a pre-existing predecessor from an unauthorized user', async () => {
|
|
100
108
|
const storage = givenStorage();
|
|
101
109
|
const mickeyMouse = givenOtherUser();
|
|
102
|
-
const tweet =
|
|
103
|
-
type: 'Tweet',
|
|
104
|
-
message: 'Hiya, Pal!',
|
|
105
|
-
sender: mickeyMouse
|
|
106
|
-
};
|
|
110
|
+
const tweet = new Tweet("Hiya, Pal!", mickeyMouse);
|
|
107
111
|
await storage.save(dehydrateFact(tweet).map(f => ({ fact: f, signatures: [] })));
|
|
108
112
|
|
|
109
113
|
const authorization = givenAuthorizationWithStorage(storage);
|
|
@@ -114,17 +118,13 @@ describe('Authorization', () => {
|
|
|
114
118
|
tweet: tweet
|
|
115
119
|
}));
|
|
116
120
|
expect(result.length).toBeGreaterThan(0);
|
|
117
|
-
expect(result.filter(r => r.type === 'Tweet').length).toEqual(0);
|
|
121
|
+
expect(result.filter(r => r.fact.type === 'Tweet').length).toEqual(0);
|
|
118
122
|
});
|
|
119
123
|
|
|
120
124
|
it('should accept a fact authorized by predecessor', async () => {
|
|
121
125
|
const authorization = givenAuthorization();
|
|
122
126
|
const lewiscarrol = await givenLoggedInUser(authorization);
|
|
123
|
-
const tweet =
|
|
124
|
-
type: 'Tweet',
|
|
125
|
-
message: 'Twas brillig',
|
|
126
|
-
sender: lewiscarrol
|
|
127
|
-
};
|
|
127
|
+
const tweet = new Tweet("Twas brillig", lewiscarrol);
|
|
128
128
|
await whenSave(authorization, dehydrateFact(tweet));
|
|
129
129
|
|
|
130
130
|
const result = await whenSave(authorization, dehydrateFact({
|
|
@@ -137,11 +137,7 @@ describe('Authorization', () => {
|
|
|
137
137
|
it('should accept a fact based on in-flight predecessor', async () => {
|
|
138
138
|
const authorization = givenAuthorization();
|
|
139
139
|
const lewiscarrol = await givenLoggedInUser(authorization);
|
|
140
|
-
const tweet =
|
|
141
|
-
type: 'Tweet',
|
|
142
|
-
message: 'Twas brillig',
|
|
143
|
-
sender: lewiscarrol
|
|
144
|
-
};
|
|
140
|
+
const tweet = new Tweet("Twas brillig", lewiscarrol);
|
|
145
141
|
// Note that this is missing.
|
|
146
142
|
// await whenSave(authorization, dehydrateFact(tweet));
|
|
147
143
|
|
|
@@ -165,32 +161,39 @@ function givenStorage() {
|
|
|
165
161
|
function givenAuthorizationWithStorage(storage: MemoryStore) {
|
|
166
162
|
const keystore = new MemoryKeystore();
|
|
167
163
|
keystore.getOrCreateUserFact(givenMockUserIdentity());
|
|
168
|
-
const authentication = new AuthenticationNoOp();
|
|
169
164
|
const fork = new PassThroughFork(storage);
|
|
170
165
|
const observableSource = new ObservableSource(storage);
|
|
171
166
|
const network = new NetworkNoOp();
|
|
172
|
-
const factManager = new FactManager(
|
|
173
|
-
const
|
|
174
|
-
.
|
|
175
|
-
.
|
|
176
|
-
.type(
|
|
177
|
-
|
|
178
|
-
.type(
|
|
167
|
+
const factManager = new FactManager(fork, observableSource, storage, network);
|
|
168
|
+
const model = buildModel(b => b
|
|
169
|
+
.type(Hashtag)
|
|
170
|
+
.type(User)
|
|
171
|
+
.type(Tweet, m => m
|
|
172
|
+
.predecessor("sender", User))
|
|
173
|
+
.type(Like, m => m
|
|
174
|
+
.predecessor("user", User)
|
|
175
|
+
.predecessor("tweet", Tweet))
|
|
176
|
+
.type(Delete, m => m
|
|
177
|
+
.predecessor("tweet", Tweet))
|
|
178
|
+
);
|
|
179
|
+
const authorizationRules = new AuthorizationRules(model)
|
|
180
|
+
.any(Hashtag)
|
|
181
|
+
.no(User)
|
|
182
|
+
.type(Tweet, t => t.sender)
|
|
183
|
+
.type(Like, l => l.user)
|
|
184
|
+
.type(Delete, d => d.tweet.sender)
|
|
179
185
|
;
|
|
180
186
|
return new AuthorizationKeystore(factManager, storage, keystore, authorizationRules, null);
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
function givenOtherUser() {
|
|
184
|
-
return
|
|
185
|
-
type: 'Jinaga.User',
|
|
186
|
-
publicKey: 'other'
|
|
187
|
-
};
|
|
190
|
+
return new User('other');
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
async function givenLoggedInUser(authorization: AuthorizationKeystore) {
|
|
191
194
|
const userIdentity = givenMockUserIdentity();
|
|
192
195
|
const userFact = await authorization.getOrCreateUserFact(userIdentity);
|
|
193
|
-
const user = hydrate<
|
|
196
|
+
const user = hydrate<User>(userFact);
|
|
194
197
|
return user;
|
|
195
198
|
}
|
|
196
199
|
|
|
@@ -203,24 +206,7 @@ function givenMockUserIdentity() {
|
|
|
203
206
|
|
|
204
207
|
async function whenSave(authorization: AuthorizationKeystore, facts: FactRecord[]) {
|
|
205
208
|
const userIdentity = givenMockUserIdentity();
|
|
206
|
-
const
|
|
209
|
+
const envelopes: FactEnvelope[] = facts.map(f => ({ fact: f, signatures: [] }));
|
|
210
|
+
const result = await authorization.save(userIdentity, envelopes);
|
|
207
211
|
return result;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function tweetSender(t: { sender: {} }) {
|
|
211
|
-
ensure(t).has("sender", "Jinaga.User");
|
|
212
|
-
|
|
213
|
-
return j.match(t.sender);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function likeUser(l: { user: {} }) {
|
|
217
|
-
ensure(l).has("user", "Jinaga.User");
|
|
218
|
-
|
|
219
|
-
return j.match(l.user);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function deleteSender(d: { tweet: { sender: {} } }) {
|
|
223
|
-
ensure(d).has("tweet", "Tweet").has("sender", "Jinaga.User");
|
|
224
|
-
|
|
225
|
-
return j.match(d.tweet.sender);
|
|
226
212
|
}
|
package/dist/postgres/sql.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { FactReference, Step } from "jinaga";
|
|
2
|
-
import { FactTypeMap, RoleMap } from "./maps";
|
|
3
|
-
export declare type SqlQuery = {
|
|
4
|
-
empty: boolean;
|
|
5
|
-
sql: string;
|
|
6
|
-
parameters: any[];
|
|
7
|
-
pathLength: number;
|
|
8
|
-
factTypeNames: string[];
|
|
9
|
-
};
|
|
10
|
-
export declare function sqlFromSteps(start: FactReference, steps: Step[], factTypes: FactTypeMap, roleMap: RoleMap, schema: string): SqlQuery | null;
|