@veramo/data-store
Version:
Veramo data storage plugin based on TypeORM database drivers
435 lines (406 loc) • 14.3 kB
text/typescript
import {
AuthorizedDIDContext,
FindArgs,
IAgentPlugin,
IDataStoreORM,
IIdentifier,
IMessage,
PartialIdentifier,
TClaimsColumns,
TCredentialColumns,
TIdentifiersColumns,
TMessageColumns,
TPresentationColumns,
UniqueVerifiableCredential,
UniqueVerifiablePresentation,
Where,
} from '@veramo/core-types'
import { schema } from '@veramo/core-types'
import { createMessage, Message } from './entities/message.js'
import { Claim } from './entities/claim.js'
import { Credential } from './entities/credential.js'
import { Presentation } from './entities/presentation.js'
import { Identifier } from './entities/identifier.js'
import {
Any,
Between,
Brackets,
DataSource,
Equal,
In,
IsNull,
LessThan,
LessThanOrEqual,
Like,
MoreThan,
MoreThanOrEqual,
Not,
SelectQueryBuilder,
} from 'typeorm'
import { getConnectedDb } from './utils.js'
import { OrPromise } from '@veramo/utils'
/**
* This class implements the {@link @veramo/core-types#IDataStoreORM} query interface using a TypeORM compatible database.
*
* This allows you to filter Verifiable Credentials, Presentations and Messages by some common properties that are
* parsed and stored in database tables.
*
* This class is designed to work with {@link @veramo/data-store#DataStore} which is the default way to populate the
* database with Credentials, Presentations and Messages in such a way that they can be queried by this class.
* These two classes MUST also share the same database connection.
*
* @see {@link @veramo/core-types#IDataStoreORM}
* @see {@link @veramo/core-types#IDataStore}
*
* @beta This API may change without a BREAKING CHANGE notice.
*/
export class DataStoreORM implements IAgentPlugin {
readonly methods: IDataStoreORM
readonly schema = schema.IDataStoreORM
private dbConnection: OrPromise<DataSource>
constructor(dbConnection: OrPromise<DataSource>) {
this.dbConnection = dbConnection
this.methods = {
dataStoreORMGetIdentifiers: this.dataStoreORMGetIdentifiers.bind(this),
dataStoreORMGetIdentifiersCount: this.dataStoreORMGetIdentifiersCount.bind(this),
dataStoreORMGetMessages: this.dataStoreORMGetMessages.bind(this),
dataStoreORMGetMessagesCount: this.dataStoreORMGetMessagesCount.bind(this),
dataStoreORMGetVerifiableCredentialsByClaims:
this.dataStoreORMGetVerifiableCredentialsByClaims.bind(this),
dataStoreORMGetVerifiableCredentialsByClaimsCount:
this.dataStoreORMGetVerifiableCredentialsByClaimsCount.bind(this),
dataStoreORMGetVerifiableCredentials: this.dataStoreORMGetVerifiableCredentials.bind(this),
dataStoreORMGetVerifiableCredentialsCount: this.dataStoreORMGetVerifiableCredentialsCount.bind(this),
dataStoreORMGetVerifiablePresentations: this.dataStoreORMGetVerifiablePresentations.bind(this),
dataStoreORMGetVerifiablePresentationsCount:
this.dataStoreORMGetVerifiablePresentationsCount.bind(this),
}
}
// Identifiers
private async identifiersQuery(
args: FindArgs<TIdentifiersColumns>,
context?: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Identifier>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
.getRepository(Identifier)
.createQueryBuilder('identifier')
.leftJoinAndSelect('identifier.keys', 'keys')
.leftJoinAndSelect('identifier.services', 'services')
.where(where)
qb = decorateQB(qb, 'message', args)
return qb
}
async dataStoreORMGetIdentifiers(
args: FindArgs<TIdentifiersColumns>,
context: AuthorizedDIDContext,
): Promise<PartialIdentifier[]> {
const identifiers = await (await this.identifiersQuery(args)).getMany()
return identifiers.map((i) => {
const identifier: PartialIdentifier = i as PartialIdentifier
if (identifier.controllerKeyId === null) {
delete identifier.controllerKeyId
}
if (identifier.alias === null) {
delete identifier.alias
}
if (identifier.provider === null) {
delete identifier.provider
}
return identifier as IIdentifier
})
}
async dataStoreORMGetIdentifiersCount(
args: FindArgs<TIdentifiersColumns>,
context: AuthorizedDIDContext,
): Promise<number> {
return await (await this.identifiersQuery(args, context)).getCount()
}
// Messages
private async messagesQuery(
args: FindArgs<TMessageColumns>,
context: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Message>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
.getRepository(Message)
.createQueryBuilder('message')
.leftJoinAndSelect('message.from', 'from')
.leftJoinAndSelect('message.to', 'to')
.leftJoinAndSelect('message.credentials', 'credentials')
.leftJoinAndSelect('message.presentations', 'presentations')
.where(where)
qb = decorateQB(qb, 'message', args)
if (context.authorizedDID) {
qb = qb.andWhere(
new Brackets((qb) => {
qb.where('message.to = :ident', { ident: context.authorizedDID }).orWhere('message.from = :ident', {
ident: context.authorizedDID,
})
}),
)
}
return qb
}
async dataStoreORMGetMessages(
args: FindArgs<TMessageColumns>,
context: AuthorizedDIDContext,
): Promise<IMessage[]> {
const messages = await (await this.messagesQuery(args, context)).getMany()
return messages.map(createMessage)
}
async dataStoreORMGetMessagesCount(
args: FindArgs<TMessageColumns>,
context: AuthorizedDIDContext,
): Promise<number> {
return (await this.messagesQuery(args, context)).getCount()
}
// Claims
private async claimsQuery(
args: FindArgs<TClaimsColumns>,
context: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Claim>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
.getRepository(Claim)
.createQueryBuilder('claim')
.leftJoinAndSelect('claim.issuer', 'issuer')
.leftJoinAndSelect('claim.subject', 'subject')
.where(where)
qb = decorateQB(qb, 'claim', args)
qb = qb.leftJoinAndSelect('claim.credential', 'credential')
if (context.authorizedDID) {
qb = qb.andWhere(
new Brackets((qb) => {
qb.where('claim.subject = :ident', { ident: context.authorizedDID }).orWhere(
'claim.issuer = :ident',
{
ident: context.authorizedDID,
},
)
}),
)
}
return qb
}
async dataStoreORMGetVerifiableCredentialsByClaims(
args: FindArgs<TClaimsColumns>,
context: AuthorizedDIDContext,
): Promise<Array<UniqueVerifiableCredential>> {
const claims = await (await this.claimsQuery(args, context)).getMany()
return claims
.map((claim) => ({
hash: claim.credential.hash,
verifiableCredential: claim.credential.raw,
}))
.reduce((acc: UniqueVerifiableCredential[], current: UniqueVerifiableCredential) => {
if (!acc.some((item) => item.hash === current.hash)) {
acc.push(current)
}
return acc
}, [])
}
async dataStoreORMGetVerifiableCredentialsByClaimsCount(
args: FindArgs<TClaimsColumns>,
context: AuthorizedDIDContext,
): Promise<number> {
return (await this.claimsQuery(args, context)).getCount()
}
// Credentials
private async credentialsQuery(
args: FindArgs<TCredentialColumns>,
context: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Credential>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
.getRepository(Credential)
.createQueryBuilder('credential')
.leftJoinAndSelect('credential.issuer', 'issuer')
.leftJoinAndSelect('credential.subject', 'subject')
.where(where)
qb = decorateQB(qb, 'credential', args)
if (context.authorizedDID) {
qb = qb.andWhere(
new Brackets((qb) => {
qb.where('credential.subject = :ident', { ident: context.authorizedDID }).orWhere(
'credential.issuer = :ident',
{
ident: context.authorizedDID,
},
)
}),
)
}
return qb
}
async dataStoreORMGetVerifiableCredentials(
args: FindArgs<TCredentialColumns>,
context: AuthorizedDIDContext,
): Promise<Array<UniqueVerifiableCredential>> {
const credentials = await (await this.credentialsQuery(args, context)).getMany()
return credentials.map((vc) => ({
hash: vc.hash,
verifiableCredential: vc.raw,
}))
}
async dataStoreORMGetVerifiableCredentialsCount(
args: FindArgs<TCredentialColumns>,
context: AuthorizedDIDContext,
): Promise<number> {
return (await this.credentialsQuery(args, context)).getCount()
}
// Presentations
private async presentationsQuery(
args: FindArgs<TPresentationColumns>,
context: AuthorizedDIDContext,
): Promise<SelectQueryBuilder<Presentation>> {
const where = createWhereObject(args)
let qb = (await getConnectedDb(this.dbConnection))
.getRepository(Presentation)
.createQueryBuilder('presentation')
.leftJoinAndSelect('presentation.holder', 'holder')
.leftJoinAndSelect('presentation.verifier', 'verifier')
.where(where)
qb = decorateQB(qb, 'presentation', args)
qb = addVerifierQuery(args, qb)
if (context.authorizedDID) {
qb = qb.andWhere(
new Brackets((qb) => {
qb.where('verifier.did = :ident', {
ident: context.authorizedDID,
}).orWhere('presentation.holder = :ident', { ident: context.authorizedDID })
}),
)
}
return qb
}
async dataStoreORMGetVerifiablePresentations(
args: FindArgs<TPresentationColumns>,
context: AuthorizedDIDContext,
): Promise<Array<UniqueVerifiablePresentation>> {
const presentations = await (await this.presentationsQuery(args, context)).getMany()
return presentations.map((vp) => ({
hash: vp.hash,
verifiablePresentation: vp.raw,
}))
}
async dataStoreORMGetVerifiablePresentationsCount(
args: FindArgs<TPresentationColumns>,
context: AuthorizedDIDContext,
): Promise<number> {
return (await this.presentationsQuery(args, context)).getCount()
}
}
function opToSQL(item: Where<any>): any[] {
switch (item.op) {
case 'IsNull':
return ['IS NULL', '']
case 'Like':
if (item.value?.length != 1) throw Error('Operation Equal requires one value')
return ['LIKE :value', item.value[0]]
case 'Equal':
if (item.value?.length != 1) throw Error('Operation Equal requires one value')
return ['= :value', item.value[0]]
case 'Any':
case 'Between':
case 'LessThan':
case 'LessThanOrEqual':
case 'MoreThan':
case 'MoreThanOrEqual':
throw new Error(`${item.op} not compatible with DID argument`)
case 'In':
default:
return ['IN (:...value)', item.value]
}
}
function addVerifierQuery(input: FindArgs<any>, qb: SelectQueryBuilder<any>): SelectQueryBuilder<any> {
if (!input) {
return qb
}
if (!Array.isArray(input.where)) {
return qb
}
const verifierWhere = input.where.find((item) => item.column === 'verifier')
if (!verifierWhere) {
return qb
}
const [op, value] = opToSQL(verifierWhere)
return qb.andWhere(`verifier.did ${op}`, { value })
}
function createWhereObject(
input: FindArgs<
TMessageColumns | TClaimsColumns | TCredentialColumns | TPresentationColumns | TIdentifiersColumns
>,
): any {
const where: Record<string, any> = {}
if (input?.where) {
for (const item of input.where) {
if (item.column === 'verifier') {
continue
}
switch (item.op) {
case 'Any':
if (!Array.isArray(item.value)) throw Error('Operator Any requires value to be an array')
where[item.column] = Any(item.value)
break
case 'Between':
if (item.value?.length != 2) throw Error('Operation Between requires two values')
where[item.column] = Between(item.value[0], item.value[1])
break
case 'Equal':
if (item.value?.length != 1) throw Error('Operation Equal requires one value')
where[item.column] = Equal(item.value[0])
break
case 'IsNull':
where[item.column] = IsNull()
break
case 'LessThan':
if (item.value?.length != 1) throw Error('Operation LessThan requires one value')
where[item.column] = LessThan(item.value[0])
break
case 'LessThanOrEqual':
if (item.value?.length != 1) throw Error('Operation LessThanOrEqual requires one value')
where[item.column] = LessThanOrEqual(item.value[0])
break
case 'Like':
if (item.value?.length != 1) throw Error('Operation Like requires one value')
where[item.column] = Like(item.value[0])
break
case 'MoreThan':
if (item.value?.length != 1) throw Error('Operation MoreThan requires one value')
where[item.column] = MoreThan(item.value[0])
break
case 'MoreThanOrEqual':
if (item.value?.length != 1) throw Error('Operation MoreThanOrEqual requires one value')
where[item.column] = MoreThanOrEqual(item.value[0])
break
case 'In':
default:
if (!Array.isArray(item.value)) throw Error('Operator IN requires value to be an array')
where[item.column] = In(item.value)
}
if (item.not === true) {
where[item.column] = Not(where[item.column])
}
}
}
return where
}
function decorateQB(
qb: SelectQueryBuilder<any>,
tableName: string,
input: FindArgs<any>,
): SelectQueryBuilder<any> {
if (input?.skip) qb = qb.offset(input.skip)
if (input?.take) qb = qb.limit(input.take)
if (input?.order) {
for (const item of input.order) {
qb = qb.addSelect(
qb.connection.driver.escape(tableName) + '.' + qb.connection.driver.escape(item.column),
item.column,
)
qb = qb.orderBy(qb.connection.driver.escape(item.column), item.direction)
}
}
return qb
}