Skip to content

Commit

Permalink
feat: add didcomm message record (#593)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Feb 8, 2022
1 parent b43e0d2 commit 7727412
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 0 deletions.
55 changes: 55 additions & 0 deletions packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ConnectionInvitationMessage } from '../../modules/connections'
import { DidCommMessageRecord, DidCommMessageRole } from '../didcomm'

describe('DidCommMessageRecord', () => {
it('correctly computes message type tags', () => {
const didCommMessage = {
'@id': '7eb74118-7f91-4ba9-9960-c709b036aa86',
'@type': 'https://didcomm.org/test-protocol/1.0/send-test',
some: { other: 'property' },
'~thread': {
thid: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c',
},
}

const didCommeMessageRecord = new DidCommMessageRecord({
message: didCommMessage,
role: DidCommMessageRole.Receiver,
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
})

expect(didCommeMessageRecord.getTags()).toEqual({
role: DidCommMessageRole.Receiver,
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',

// Computed properties based on message id and type
threadId: 'ea24e14a-4fc4-40f4-85a0-f6fcf02bfc1c',
protocolName: 'test-protocol',
messageName: 'send-test',
versionMajor: '1',
versionMinor: '0',
messageType: 'https://didcomm.org/test-protocol/1.0/send-test',
messageId: '7eb74118-7f91-4ba9-9960-c709b036aa86',
})
})

it('correctly returns a message class instance', () => {
const invitationJson = {
'@type': 'https://didcomm.org/connections/1.0/invitation',
'@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'],
serviceEndpoint: 'https://example.com',
label: 'test',
}

const didCommeMessageRecord = new DidCommMessageRecord({
message: invitationJson,
role: DidCommMessageRole.Receiver,
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
})

const invitation = didCommeMessageRecord.getMessageInstance(ConnectionInvitationMessage)

expect(invitation).toBeInstanceOf(ConnectionInvitationMessage)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { mockFunction } from '../../../tests/helpers'
import { ConnectionInvitationMessage } from '../../modules/connections'
import { JsonTransformer } from '../../utils/JsonTransformer'
import { IndyStorageService } from '../IndyStorageService'
import { DidCommMessageRecord, DidCommMessageRepository, DidCommMessageRole } from '../didcomm'

jest.mock('../IndyStorageService')

const StorageMock = IndyStorageService as unknown as jest.Mock<IndyStorageService<DidCommMessageRecord>>

const invitationJson = {
'@type': 'https://didcomm.org/connections/1.0/invitation',
'@id': '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
recipientKeys: ['recipientKeyOne', 'recipientKeyTwo'],
serviceEndpoint: 'https://example.com',
label: 'test',
}

describe('Repository', () => {
let repository: DidCommMessageRepository
let storageMock: IndyStorageService<DidCommMessageRecord>

beforeEach(async () => {
storageMock = new StorageMock()
repository = new DidCommMessageRepository(storageMock)
})

const getRecord = ({ id }: { id?: string } = {}) => {
return new DidCommMessageRecord({
id,
message: invitationJson,
role: DidCommMessageRole.Receiver,
associatedRecordId: '16ca6665-29f6-4333-a80e-d34db6bfe0b0',
})
}

describe('getAgentMessage()', () => {
it('should get the record using the storage service', async () => {
const record = getRecord({ id: 'test-id' })
mockFunction(storageMock.findByQuery).mockReturnValue(Promise.resolve([record]))

const invitation = await repository.getAgentMessage({
messageClass: ConnectionInvitationMessage,
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
})

expect(storageMock.findByQuery).toBeCalledWith(DidCommMessageRecord, {
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
messageType: 'https://didcomm.org/connections/1.0/invitation',
})
expect(invitation).toBeInstanceOf(ConnectionInvitationMessage)
})
})

describe('saveAgentMessage()', () => {
it('should transform and save the agent message', async () => {
await repository.saveAgentMessage({
role: DidCommMessageRole.Receiver,
agentMessage: JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage),
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
})

expect(storageMock.save).toBeCalledWith(
expect.objectContaining({
role: DidCommMessageRole.Receiver,
message: invitationJson,
associatedRecordId: '04a2c382-999e-4de9-a1d2-9dec0b2fa5e4',
})
)
})
})
})
99 changes: 99 additions & 0 deletions packages/core/src/storage/didcomm/DidCommMessageRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { AgentMessage } from '../../agent/AgentMessage'
import type { JsonObject } from '../../types'
import type { DidCommMessageRole } from './DidCommMessageRole'

import { AriesFrameworkError } from '../../error'
import { JsonTransformer } from '../../utils/JsonTransformer'
import { rightSplit } from '../../utils/string'
import { isJsonObject } from '../../utils/type'
import { uuid } from '../../utils/uuid'
import { BaseRecord } from '../BaseRecord'

export type DefaultDidCommMessageTags = {
role: DidCommMessageRole
associatedRecordId?: string

// Computed
protocolName: string
messageName: string
versionMajor: string
versionMinor: string
messageType: string
messageId: string
threadId: string
}

export interface DidCommMessageRecordProps {
role: DidCommMessageRole
message: JsonObject
id?: string
createdAt?: Date
associatedRecordId?: string
}

export class DidCommMessageRecord extends BaseRecord<DefaultDidCommMessageTags> {
public message!: JsonObject
public role!: DidCommMessageRole

/**
* The id of the record that is associated with this message record.
*
* E.g. if the connection record wants to store an invitation message
* the associatedRecordId will be the id of the connection record.
*/
public associatedRecordId?: string

public static readonly type = 'DidCommMessageRecord'
public readonly type = DidCommMessageRecord.type

public constructor(props: DidCommMessageRecordProps) {
super()

if (props) {
this.id = props.id ?? uuid()
this.createdAt = props.createdAt ?? new Date()
this.associatedRecordId = props.associatedRecordId
this.role = props.role
this.message = props.message
}
}

public getTags() {
const messageId = this.message['@id'] as string
const messageType = this.message['@type'] as string
const [, protocolName, protocolVersion, messageName] = rightSplit(messageType, '/', 3)
const [versionMajor, versionMinor] = protocolVersion.split('.')

const thread = this.message['~thread']
let threadId = messageId

if (isJsonObject(thread) && typeof thread.thid === 'string') {
threadId = thread.thid
}

return {
...this._tags,
role: this.role,
associatedRecordId: this.associatedRecordId,

// Computed properties based on message id and type
threadId,
protocolName,
messageName,
versionMajor,
versionMinor,
messageType,
messageId,
}
}

public getMessageInstance<MessageClass extends typeof AgentMessage = typeof AgentMessage>(
messageClass: MessageClass
): InstanceType<MessageClass> {
if (messageClass.type !== this.message['@type']) {
throw new AriesFrameworkError('Provided message class type does not match type of stored message')
}

return JsonTransformer.fromJSON(this.message, messageClass) as InstanceType<MessageClass>
}
}
51 changes: 51 additions & 0 deletions packages/core/src/storage/didcomm/DidCommMessageRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { AgentMessage } from '../../agent/AgentMessage'
import type { JsonObject } from '../../types'
import type { DidCommMessageRole } from './DidCommMessageRole'

import { inject, scoped, Lifecycle } from 'tsyringe'

import { InjectionSymbols } from '../../constants'
import { Repository } from '../Repository'
import { StorageService } from '../StorageService'

import { DidCommMessageRecord } from './DidCommMessageRecord'

@scoped(Lifecycle.ContainerScoped)
export class DidCommMessageRepository extends Repository<DidCommMessageRecord> {
public constructor(@inject(InjectionSymbols.StorageService) storageService: StorageService<DidCommMessageRecord>) {
super(DidCommMessageRecord, storageService)
}

public async saveAgentMessage({ role, agentMessage, associatedRecordId }: SaveAgentMessageOptions) {
const didCommMessageRecord = new DidCommMessageRecord({
message: agentMessage.toJSON() as JsonObject,
role,
associatedRecordId,
})

await this.save(didCommMessageRecord)
}

public async getAgentMessage<MessageClass extends typeof AgentMessage = typeof AgentMessage>({
associatedRecordId,
messageClass,
}: GetAgentMessageOptions<MessageClass>): Promise<InstanceType<MessageClass>> {
const record = await this.getSingleByQuery({
associatedRecordId,
messageType: messageClass.type,
})

return record.getMessageInstance(messageClass)
}
}

export interface SaveAgentMessageOptions {
role: DidCommMessageRole
agentMessage: AgentMessage
associatedRecordId: string
}

export interface GetAgentMessageOptions<MessageClass extends typeof AgentMessage> {
associatedRecordId: string
messageClass: MessageClass
}
4 changes: 4 additions & 0 deletions packages/core/src/storage/didcomm/DidCommMessageRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum DidCommMessageRole {
Sender = 'sender',
Receiver = 'receiver',
}
3 changes: 3 additions & 0 deletions packages/core/src/storage/didcomm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './DidCommMessageRecord'
export * from './DidCommMessageRepository'
export * from './DidCommMessageRole'
1 change: 1 addition & 0 deletions packages/core/src/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './didcomm'
6 changes: 6 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,9 @@ export interface OutboundPackage {
endpoint?: string
connectionId?: string
}

export type JsonValue = string | number | boolean | null | JsonObject | JsonArray
export type JsonArray = Array<JsonValue>
export interface JsonObject {
[property: string]: JsonValue
}
11 changes: 11 additions & 0 deletions packages/core/src/utils/__tests__/string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { rightSplit } from '../string'

describe('string', () => {
describe('rightSplit', () => {
it('correctly splits a string starting from the right', () => {
const messageType = 'https://didcomm.org/connections/1.0/invitation'

expect(rightSplit(messageType, '/', 3)).toEqual(['https://didcomm.org', 'connections', '1.0', 'invitation'])
})
})
})
4 changes: 4 additions & 0 deletions packages/core/src/utils/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function rightSplit(string: string, sep: string, limit: number) {
const split = string.split(sep)
return limit ? [split.slice(0, -limit).join(sep)].concat(split.slice(-limit)) : split
}
6 changes: 6 additions & 0 deletions packages/core/src/utils/type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { JsonObject } from '../types'

export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>

export const isString = (value: unknown): value is string => typeof value === 'string'
export const isNumber = (value: unknown): value is number => typeof value === 'number'
export const isBoolean = (value: unknown): value is boolean => typeof value === 'boolean'

export const isJsonObject = (value: unknown): value is JsonObject => {
return value !== undefined && typeof value === 'object' && value !== null && !Array.isArray(value)
}

0 comments on commit 7727412

Please sign in to comment.