-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add didcomm message record (#593)
Signed-off-by: Timo Glastra <[email protected]>
- Loading branch information
1 parent
4d71835
commit e547fb1
Showing
11 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
packages/core/src/storage/__tests__/DidCommMessageRecord.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |
72 changes: 72 additions & 0 deletions
72
packages/core/src/storage/__tests__/DidCommMessageRepository.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}) | ||
) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
51
packages/core/src/storage/didcomm/DidCommMessageRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export enum DidCommMessageRole { | ||
Sender = 'sender', | ||
Receiver = 'receiver', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './DidCommMessageRecord' | ||
export * from './DidCommMessageRepository' | ||
export * from './DidCommMessageRole' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './didcomm' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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']) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |