Skip to content

Commit 4b27dc8

Browse files
feat: claims about claims (#948)
1 parent 6e32e04 commit 4b27dc8

File tree

11 files changed

+373
-7
lines changed

11 files changed

+373
-7
lines changed

scripts/CreateClaim.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const main = async () => {
66
const attributes = {
77
name: process.argv[2] || 'testing claim',
88
author: process.argv[3] || 'the tester',
9+
about: [ process.argv[4] ],
910
}
1011
const client = new Client()
1112
const claim = await createACDClaim(attributes)

src/API/API.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { getVerifiableClaimSigner } from '@po.et/poet-js'
22
import { Collection, MongoClient, Db } from 'mongodb'
3-
import * as Pino from 'pino'
43

54
import { LoggingConfiguration } from 'Configuration'
65
import { createModuleLogger } from 'Helpers/Logging'
@@ -9,6 +8,8 @@ import { Messaging } from 'Messaging/Messaging'
98
import { ExchangeConfiguration } from './ExchangeConfiguration'
109
import { FileController, FileControllerConfiguration } from './FileController'
1110
import * as FileDAO from './FileDAO'
11+
import { GraphController } from './GraphController'
12+
import { GraphDAO } from './GraphDAO'
1213
import { HealthController } from './HealthController'
1314
import { Router } from './Router'
1415
import { WorkController } from './WorkController'
@@ -31,6 +32,7 @@ export const API = (configuration: APIConfiguration): API => {
3132
let router: Router
3233
let messaging: Messaging
3334
let fileCollection: Collection
35+
let graphCollection: Collection
3436

3537
const logger = createModuleLogger(configuration, __dirname)
3638

@@ -40,6 +42,7 @@ export const API = (configuration: APIConfiguration): API => {
4042
dbConnection = await mongoClient.db()
4143

4244
fileCollection = dbConnection.collection('files')
45+
graphCollection = dbConnection.collection('graph')
4346

4447
messaging = new Messaging(configuration.rabbitmqUrl, configuration.exchanges)
4548
await messaging.start()
@@ -50,6 +53,12 @@ export const API = (configuration: APIConfiguration): API => {
5053
},
5154
})
5255

56+
const graphDao = GraphDAO({
57+
dependencies: {
58+
collection: graphCollection,
59+
},
60+
})
61+
5362
const fileController = FileController({
5463
dependencies: {
5564
fileDao,
@@ -70,6 +79,13 @@ export const API = (configuration: APIConfiguration): API => {
7079
exchange: configuration.exchanges,
7180
})
7281

82+
const graphController = GraphController({
83+
dependencies: {
84+
logger,
85+
graphDao,
86+
},
87+
})
88+
7389
const healthController = HealthController({
7490
dependencies: {
7591
db: dbConnection,
@@ -82,6 +98,7 @@ export const API = (configuration: APIConfiguration): API => {
8298
logger,
8399
fileController,
84100
workController,
101+
graphController,
85102
healthController,
86103
verifiableClaimSigner: getVerifiableClaimSigner(),
87104
},

src/API/GraphController.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as Pino from 'pino'
2+
3+
import { childWithFileName } from 'Helpers/Logging'
4+
import { GraphDAO } from './GraphDAO'
5+
6+
export interface Dependencies {
7+
readonly logger: Pino.Logger
8+
readonly graphDao: GraphDAO
9+
}
10+
11+
export interface Arguments {
12+
readonly dependencies: Dependencies
13+
}
14+
15+
interface Edge {
16+
readonly origin: string
17+
readonly target: string
18+
}
19+
20+
export interface GraphController {
21+
readonly getByUri: (uri: string) => Promise<ReadonlyArray<Edge>>
22+
}
23+
24+
export const GraphController = ({
25+
dependencies: {
26+
logger: parentLogger,
27+
graphDao,
28+
},
29+
}: Arguments): GraphController => {
30+
const logger = childWithFileName(parentLogger, __filename)
31+
32+
const getByUri = async (uri: string): Promise<ReadonlyArray<Edge>> => {
33+
const methodLogger = logger.child({ method: 'getById', uri })
34+
methodLogger.trace('Getting graph edges')
35+
36+
const edgesToUris = (edges: ReadonlyArray<Edge>): ReadonlyArray<string> =>
37+
edges
38+
.map(({ origin, target }) => [origin, target])
39+
.reduce((a, c) => [...a, ...c], []) // TODO: Array.flatMap
40+
41+
const edgesMatch = (a: Edge) => (b: Edge) => a.origin === b.origin && a.target === b.target
42+
43+
const recursive = async (
44+
searchUris: ReadonlyArray<string> | string,
45+
searchedUris: ReadonlyArray<string> = [],
46+
foundEdges: ReadonlyArray<Edge> = [],
47+
stackDepth: number = 1,
48+
): Promise<{ edges: ReadonlyArray<Edge>, stackDepth: number }> => {
49+
if (typeof searchUris === 'string')
50+
return recursive([searchUris])
51+
if (searchUris.length === 0)
52+
return { edges: foundEdges, stackDepth }
53+
const newFoundEdges = await graphDao.findEdges(searchUris)
54+
const deduplicatedNewFoundEdges = newFoundEdges.filter(edge => !foundEdges.some(edgesMatch(edge)))
55+
const newSearchUris = edgesToUris(deduplicatedNewFoundEdges)
56+
.filter(uri => !searchUris.includes(uri))
57+
.filter(uri => !searchedUris.includes(uri))
58+
const deduplicatedNewSearchUris = searchUris.filter(uri => !searchedUris.includes(uri))
59+
return recursive(
60+
newSearchUris,
61+
[...searchedUris, ...deduplicatedNewSearchUris],
62+
[...foundEdges, ...deduplicatedNewFoundEdges],
63+
stackDepth + 1,
64+
)
65+
}
66+
67+
const { edges, stackDepth } = await recursive(uri)
68+
69+
methodLogger.info({ uri, stackDepth })
70+
71+
return edges
72+
}
73+
74+
return {
75+
getByUri,
76+
}
77+
}

src/API/GraphDAO.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Collection } from 'mongodb'
2+
3+
export interface Entry {
4+
readonly origin: string
5+
readonly target: string
6+
}
7+
8+
export interface Dependencies {
9+
readonly collection: Collection
10+
}
11+
12+
export interface Arguments {
13+
readonly dependencies: Dependencies
14+
}
15+
16+
export interface GraphDAO {
17+
readonly findEdges: (uris: ReadonlyArray<string>) => Promise<ReadonlyArray<Entry>>
18+
}
19+
20+
export const GraphDAO = ({
21+
dependencies: {
22+
collection,
23+
},
24+
}: Arguments): GraphDAO => {
25+
const findEdges = (uris: ReadonlyArray<string>) => collection.find(
26+
{
27+
$or: [
28+
{ origin: { $in: uris } },
29+
{ target: { $in: uris } },
30+
],
31+
},
32+
{ projection: { _id: false },
33+
}).toArray()
34+
35+
return {
36+
findEdges,
37+
}
38+
}

src/API/Router.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { map, values, prop, pipe } from 'ramda'
2020
import { childWithFileName } from 'Helpers/Logging'
2121

2222
import { FileController } from './FileController'
23+
import { GraphController } from './GraphController'
2324
import { HealthController } from './HealthController'
2425
import { HttpExceptionsMiddleware } from './Middlewares/HttpExceptionsMiddleware'
2526
import { LoggerMiddleware } from './Middlewares/LoggerMiddleware'
@@ -39,6 +40,7 @@ export interface Dependencies {
3940
readonly logger: Pino.Logger
4041
readonly fileController: FileController
4142
readonly workController: WorkController
43+
readonly graphController: GraphController
4244
readonly healthController: HealthController
4345
readonly verifiableClaimSigner: VerifiableClaimSigner
4446
}
@@ -58,6 +60,7 @@ export const Router = ({
5860
logger,
5961
fileController,
6062
workController,
63+
graphController,
6164
healthController,
6265
verifiableClaimSigner,
6366
},
@@ -163,6 +166,13 @@ export const Router = ({
163166
context.status = 202
164167
}
165168

169+
const getGraph = async (context: KoaRouter.IRouterContext, next: () => Promise<any>) => {
170+
const { uri } = context.params
171+
routerLogger.debug({ uri }, 'GET /graph')
172+
const graph = await graphController.getByUri(uri)
173+
context.body = graph
174+
}
175+
166176
koaRouter.post(
167177
'/files',
168178
KoaBody({ multipart: true, formidable: { multiples: false, maxFields: 1 } }),
@@ -171,6 +181,7 @@ export const Router = ({
171181
koaRouter.get('/works/:id', RequestValidationMiddleware(getWorkSchema), getWork)
172182
koaRouter.get('/works', RequestValidationMiddleware(getWorksSchema), getWorks)
173183
koaRouter.post('/works', KoaBody({ textLimit: 1000000 }), postWork)
184+
koaRouter.get('/graph/:uri', getGraph)
174185
koaRouter.get('/health', getHealth)
175186
koaRouter.get('/metrics', getWorkCounts)
176187

src/View/View.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const View = (configuration: ViewConfiguration): View => {
5454
exchange: configuration.exchanges,
5555
})
5656
await router.start()
57+
await workController.createDbIndices()
5758

5859
logger.info('View Started')
5960
}

src/View/WorkController.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Work, PoetBlockAnchor } from '@po.et/poet-js'
1+
import { Work, PoetBlockAnchor, SignedVerifiableClaim } from '@po.et/poet-js'
22
import { Collection, Db } from 'mongodb'
33
import * as Pino from 'pino'
44

@@ -15,6 +15,7 @@ export interface Arguments {
1515
}
1616

1717
export interface WorkController {
18+
readonly createDbIndices: () => Promise<void>
1819
readonly createWork: (work: Work) => Promise<void>
1920
readonly setIPFSHash: (workId: string, ipfsFileHash: string) => Promise<void>
2021
readonly setDirectoryHashOnEntries: (data: {
@@ -39,16 +40,23 @@ export const WorkController = ({
3940
const workControllerLogger: Pino.Logger = childWithFileName(logger, __filename)
4041
const anchorCollection: Collection = db.collection('anchors')
4142
const workCollection: Collection = db.collection('works')
43+
const graphCollection: Collection = db.collection('graph')
4244

43-
const createWork = async (work: Work): Promise<void> => {
45+
const createDbIndices = async () => {
46+
await graphCollection.createIndex({ origin: 1, target: 1 }, { unique: true })
47+
}
48+
49+
const createWork = async (verifiableClaim: SignedVerifiableClaim): Promise<void> => {
4450
const logger = workControllerLogger.child({ method: 'createWork' })
45-
logger.trace({ work }, 'Creating Work')
51+
logger.trace({ verifiableClaim }, 'Creating Work')
4652

47-
const existing = await workCollection.findOne({ id: work.id })
53+
const existing = await workCollection.findOne({ id: verifiableClaim.id })
4854

4955
if (existing) return
5056

51-
await workCollection.insertOne(work)
57+
await workCollection.insertOne(verifiableClaim)
58+
if (Array.isArray(verifiableClaim.claim.about))
59+
await insertGraphEdges(verifiableClaim.id, verifiableClaim.claim.about)
5260
}
5361

5462
const setIPFSHash = async (workId: string, ipfsFileHash: string): Promise<void> => {
@@ -132,9 +140,19 @@ export const WorkController = ({
132140
workCollection.updateOne({ 'anchor.ipfsFileHash': ipfsFileHash }, { $set: claim }, { upsert: true }),
133141
),
134142
)
143+
const verifiableClaims = claimIPFSHashPairs.map(({ claim }) => claim)
144+
await Promise.all(
145+
verifiableClaims
146+
.filter(verifiableClaim => Array.isArray(verifiableClaim.claim.about))
147+
.map(verifiableClaim => insertGraphEdges(verifiableClaim.id, verifiableClaim.claim.about as string[])), // See 1
148+
)
135149
}
136150

151+
const insertGraphEdges = async (id: string, about: ReadonlyArray<string>) =>
152+
graphCollection.insertMany(about.map(target => ({ origin: `poet:claims/${id}`, target })))
153+
137154
return {
155+
createDbIndices,
138156
createWork,
139157
setIPFSHash,
140158
setDirectoryHashOnEntries,
@@ -144,3 +162,12 @@ export const WorkController = ({
144162
upsertClaimIPFSHashPair,
145163
}
146164
}
165+
166+
// 1. as string[]: TypeScript doesn't [yet] automatically narrow types in Array.filter unless an explicit type
167+
// guard is provided. Proper solution would be something like
168+
// ```
169+
// const isStandardVerifiableClaim = (x: VerifiableClaim): x is StandardVerifiableClaim =>
170+
// Array.isArray(x.claim.about) && etc
171+
// ...
172+
// verifiableClaims.filter(isStandardVerifiableClaim).map(...)
173+
// ```

tests/helpers/createClaims.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { issuerACD, issuerEAP, issuerMA, privateKeyACD, privateKeyEAP, privateKe
55

66
const { configureSignVerifiableClaim } = getVerifiableClaimSigner()
77

8-
const createACDWorkClaim = configureCreateVerifiableClaim({ issuer: issuerACD })
8+
const context = {
9+
about: 'schema:url',
10+
}
11+
12+
const createACDWorkClaim = configureCreateVerifiableClaim({ issuer: issuerACD, context })
913
const createEAPWorkClaim = configureCreateVerifiableClaim({ issuer: issuerEAP })
1014
const createMAWorkClaim = configureCreateVerifiableClaim({ issuer: issuerMA })
1115

tests/helpers/works.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ export const postWorkWithDelay = (port: string, host?: string, delayValue: numbe
2424
await delay(delayValue)
2525
return response
2626
}
27+
28+
export const getGraph = (port: string, host?: string) => (uri: string) =>
29+
fetch(`${baseUrl(port, host)}/graph/${encodeURIComponent(uri)}`)

0 commit comments

Comments
 (0)