Skip to content

Commit e547fb1

Browse files
authored
feat: add didcomm message record (#593)
Signed-off-by: Timo Glastra <timo@animo.id>
1 parent 4d71835 commit e547fb1

11 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ConnectionInvitationMessage } from '../../modules/connections'
2+
import { DidCommMessageRecord, DidCommMessageRole } from '../didcomm'
3+
4+
describe('DidCommMessageRecord', () => {
5+
it('correctly computes message type tags', () => {
6+
const didCommMessage = {
7+
'@id': '7eb74118-7f91-4ba9-9960-c709b036aa86',
8+
'@type': 'https://didcomm.org/test-protocol/1.0/send-test',
9+
some: { other: 'property' },
10+
'~thread': {
11+
thid: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c',
12+
},
13+
}
14+
15+
const didCommeMessageRecord = new DidCommMessageRecord({
16+
message: didCommMessage,
17+
role: DidCommMessageRole.Receiver,
18+
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
19+
})
20+
21+
expect(didCommeMessageRecord.getTags()).toEqual({
22+
role: DidCommMessageRole.Receiver,
23+
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
24+
25+
// Computed properties based on message id and type
26+
threadId: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c',
27+
protocolName: 'test-protocol',
28+
messageName: 'send-test',
29+
versionMajor: '1',
30+
versionMinor: '0',
31+
messageType: 'https://didcomm.org/test-protocol/1.0/send-test',
32+
messageId: '7eb74118-7f91-4ba9-9960-c709b036aa86',
33+
})
34+
})
35+
36+
it('correctly returns a message class instance', () => {
37+
const invitationJson = {
38+
'@type': 'https://didcomm.org/connections/1.0/invitation',
39+
'@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
40+
recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'],
41+
serviceEndpoint: 'https://example.com',
42+
label: 'test',
43+
}
44+
45+
const didCommeMessageRecord = new DidCommMessageRecord({
46+
message: invitationJson,
47+
role: DidCommMessageRole.Receiver,
48+
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
49+
})
50+
51+
const invitation = didCommeMessageRecord.getMessageInstance(ConnectionInvitationMessage)
52+
53+
expect(invitation).toBeInstanceOf(ConnectionInvitationMessage)
54+
})
55+
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { mockFunction } from '../../../tests/helpers'
2+
import { ConnectionInvitationMessage } from '../../modules/connections'
3+
import { JsonTransformer } from '../../utils/JsonTransformer'
4+
import { IndyStorageService } from '../IndyStorageService'
5+
import { DidCommMessageRecord, DidCommMessageRepository, DidCommMessageRole } from '../didcomm'
6+
7+
jest.mock('../IndyStorageService')
8+
9+
const StorageMock = IndyStorageService as unknown as jest.Mock<IndyStorageService<DidCommMessageRecord>>
10+
11+
const invitationJson = {
12+
'@type': 'https://didcomm.org/connections/1.0/invitation',
13+
'@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
14+
recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'],
15+
serviceEndpoint: 'https://example.com',
16+
label: 'test',
17+
}
18+
19+
describe('Repository', () => {
20+
let repository: DidCommMessageRepository
21+
let storageMock: IndyStorageService<DidCommMessageRecord>
22+
23+
beforeEach(async () => {
24+
storageMock = new StorageMock()
25+
repository = new DidCommMessageRepository(storageMock)
26+
})
27+
28+
const getRecord = ({ id }: { id?: string } = {}) => {
29+
return new DidCommMessageRecord({
30+
id,
31+
message: invitationJson,
32+
role: DidCommMessageRole.Receiver,
33+
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
34+
})
35+
}
36+
37+
describe('getAgentMessage()', () => {
38+
it('should get the record using the storage service', async () => {
39+
const record = getRecord({ id: 'test-id' })
40+
mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record]))
41+
42+
const invitation = await repository.getAgentMessage({
43+
messageClass: ConnectionInvitationMessage,
44+
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
45+
})
46+
47+
expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, {
48+
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
49+
messageType: 'https://didcomm.org/connections/1.0/invitation',
50+
})
51+
expect(invitation).toBeInstanceOf(ConnectionInvitationMessage)
52+
})
53+
})
54+
55+
describe('saveAgentMessage()', () => {
56+
it('should transform and save the agent message', async () => {
57+
await repository.saveAgentMessage({
58+
role: DidCommMessageRole.Receiver,
59+
agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage),
60+
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
61+
})
62+
63+
expect(storageMock.save).toBeCalledWith(
64+
expect.objectContaining({
65+
role: DidCommMessageRole.Receiver,
66+
message: invitationJson,
67+
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
68+
})
69+
)
70+
})
71+
})
72+
})
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { AgentMessage } from '../../agent/AgentMessage'
2+
import type { JsonObject } from '../../types'
3+
import type { DidCommMessageRole } from './DidCommMessageRole'
4+
5+
import { AriesFrameworkError } from '../../error'
6+
import { JsonTransformer } from '../../utils/JsonTransformer'
7+
import { rightSplit } from '../../utils/string'
8+
import { isJsonObject } from '../../utils/type'
9+
import { uuid } from '../../utils/uuid'
10+
import { BaseRecord } from '../BaseRecord'
11+
12+
export type DefaultDidCommMessageTags = {
13+
role: DidCommMessageRole
14+
associatedRecordId?: string
15+
16+
// Computed
17+
protocolName: string
18+
messageName: string
19+
versionMajor: string
20+
versionMinor: string
21+
messageType: string
22+
messageId: string
23+
threadId: string
24+
}
25+
26+
export interface DidCommMessageRecordProps {
27+
role: DidCommMessageRole
28+
message: JsonObject
29+
id?: string
30+
createdAt?: Date
31+
associatedRecordId?: string
32+
}
33+
34+
export class DidCommMessageRecord extends BaseRecord<DefaultDidCommMessageTags> {
35+
public message!: JsonObject
36+
public role!: DidCommMessageRole
37+
38+
/**
39+
* The id of the record that is associated with this message record.
40+
*
41+
* E.g. if the connection record wants to store an invitation message
42+
* the associatedRecordId will be the id of the connection record.
43+
*/
44+
public associatedRecordId?: string
45+
46+
public static readonly type = 'DidCommMessageRecord'
47+
public readonly type = DidCommMessageRecord.type
48+
49+
public constructor(props: DidCommMessageRecordProps) {
50+
super()
51+
52+
if (props) {
53+
this.id = props.id ?? uuid()
54+
this.createdAt = props.createdAt ?? new Date()
55+
this.associatedRecordId = props.associatedRecordId
56+
this.role = props.role
57+
this.message = props.message
58+
}
59+
}
60+
61+
public getTags() {
62+
const messageId = this.message['@id'] as string
63+
const messageType = this.message['@type'] as string
64+
const [, protocolName, protocolVersion, messageName] = rightSplit(messageType, '/', 3)
65+
const [versionMajor, versionMinor] = protocolVersion.split('.')
66+
67+
const thread = this.message['~thread']
68+
let threadId = messageId
69+
70+
if (isJsonObject(thread) && typeof thread.thid === 'string') {
71+
threadId = thread.thid
72+
}
73+
74+
return {
75+
...this._tags,
76+
role: this.role,
77+
associatedRecordId: this.associatedRecordId,
78+
79+
// Computed properties based on message id and type
80+
threadId,
81+
protocolName,
82+
messageName,
83+
versionMajor,
84+
versionMinor,
85+
messageType,
86+
messageId,
87+
}
88+
}
89+
90+
public getMessageInstance<MessageClass extends typeof AgentMessage = typeof AgentMessage>(
91+
messageClass: MessageClass
92+
): InstanceType<MessageClass> {
93+
if (messageClass.type !== this.message['@type']) {
94+
throw new AriesFrameworkError('Provided message class type does not match type of stored message')
95+
}
96+
97+
return JsonTransformer.fromJSON(this.message, messageClass) as InstanceType<MessageClass>
98+
}
99+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { AgentMessage } from '../../agent/AgentMessage'
2+
import type { JsonObject } from '../../types'
3+
import type { DidCommMessageRole } from './DidCommMessageRole'
4+
5+
import { inject, scoped, Lifecycle } from 'tsyringe'
6+
7+
import { InjectionSymbols } from '../../constants'
8+
import { Repository } from '../Repository'
9+
import { StorageService } from '../StorageService'
10+
11+
import { DidCommMessageRecord } from './DidCommMessageRecord'
12+
13+
@scoped(Lifecycle.ContainerScoped)
14+
export class DidCommMessageRepository extends Repository<DidCommMessageRecord> {
15+
public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService<DidCommMessageRecord>) {
16+
super(DidCommMessageRecord, storageService)
17+
}
18+
19+
public async saveAgentMessage({ role, agentMessage, associatedRecordId }: SaveAgentMessageOptions) {
20+
const didCommMessageRecord = new DidCommMessageRecord({
21+
message: agentMessage.toJSON() as JsonObject,
22+
role,
23+
associatedRecordId,
24+
})
25+
26+
await this.save(didCommMessageRecord)
27+
}
28+
29+
public async getAgentMessage<MessageClass extends typeof AgentMessage = typeof AgentMessage>({
30+
associatedRecordId,
31+
messageClass,
32+
}: GetAgentMessageOptions<MessageClass>): Promise<InstanceType<MessageClass>> {
33+
const record = await this.getSingleByQuery({
34+
associatedRecordId,
35+
messageType: messageClass.type,
36+
})
37+
38+
return record.getMessageInstance(messageClass)
39+
}
40+
}
41+
42+
export interface SaveAgentMessageOptions {
43+
role: DidCommMessageRole
44+
agentMessage: AgentMessage
45+
associatedRecordId: string
46+
}
47+
48+
export interface GetAgentMessageOptions<MessageClass extends typeof AgentMessage> {
49+
associatedRecordId: string
50+
messageClass: MessageClass
51+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum DidCommMessageRole {
2+
Sender = 'sender',
3+
Receiver = 'receiver',
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './DidCommMessageRecord'
2+
export * from './DidCommMessageRepository'
3+
export * from './DidCommMessageRole'

packages/core/src/storage/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './didcomm'

packages/core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ export interface OutboundPackage {
7878
endpoint?: string
7979
connectionId?: string
8080
}
81+
82+
export type JsonValue = string | number | boolean | null | JsonObject | JsonArray
83+
export type JsonArray = Array<JsonValue>
84+
export interface JsonObject {
85+
[property: string]: JsonValue
86+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { rightSplit } from '../string'
2+
3+
describe('string', () => {
4+
describe('rightSplit', () => {
5+
it('correctly splits a string starting from the right', () => {
6+
const messageType = 'https://didcomm.org/connections/1.0/invitation'
7+
8+
expect(rightSplit(messageType, '/', 3)).toEqual(['https://didcomm.org', 'connections', '1.0', 'invitation'])
9+
})
10+
})
11+
})

packages/core/src/utils/string.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function rightSplit(string: string, sep: string, limit: number) {
2+
const split = string.split(sep)
3+
return limit ? [split.slice(0, -limit).join(sep)].concat(split.slice(-limit)) : split
4+
}

0 commit comments

Comments
 (0)