Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking if we shouldn't use message ID instead of creating an artificial one. But I guess it's actually a record ID and not a message ID in this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem with message id is that it can be provided by other agents, meaning it could be non-unique.... That's why I went with a new, generated, id.

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)
}