UNPKG

@sphereon/ssi-sdk.data-store

Version:

184 lines (162 loc) • 7.01 kB
import { AbstractDigitalCredentialStore } from './AbstractDigitalCredentialStore' import { AddCredentialArgs, CredentialRole, CredentialStateType, DigitalCredential, GetCredentialArgs, GetCredentialsArgs, GetCredentialsResponse, NonPersistedDigitalCredential, RemoveCredentialArgs, UpdateCredentialStateArgs, } from '../types' import { OrPromise } from '@sphereon/ssi-types' import { DataSource, type FindOptionsOrder, type FindOptionsWhere, Repository } from 'typeorm' import Debug from 'debug' import { DigitalCredentialEntity } from '../entities/digitalCredential/DigitalCredentialEntity' import { digitalCredentialFrom, digitalCredentialsFrom, nonPersistedDigitalCredentialEntityFromAddArgs } from '../../src' import { parseAndValidateOrderOptions } from '../utils/SortingUtils' const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:credential-store') export class DigitalCredentialStore extends AbstractDigitalCredentialStore { private readonly dbConnection: OrPromise<DataSource> private dcRepo: Repository<DigitalCredentialEntity> | undefined constructor(dbConnection: OrPromise<DataSource>) { super() this.dbConnection = dbConnection } addCredential = async (args: AddCredentialArgs): Promise<DigitalCredential> => { debug('Adding credential', args) const credentialEntity: NonPersistedDigitalCredential = nonPersistedDigitalCredentialEntityFromAddArgs(args) const validationError = this.assertValidDigitalCredential(credentialEntity) if (validationError) { return Promise.reject(validationError) } const dcRepo = await this.getRepository() const createdResult: DigitalCredentialEntity = await dcRepo.save(credentialEntity) return Promise.resolve(digitalCredentialFrom(createdResult)) } getCredential = async (args: GetCredentialArgs): Promise<DigitalCredential> => { const dcRepo = await this.getRepository() const result: DigitalCredentialEntity | null = await dcRepo.findOne({ where: args, }) if (!result) { return Promise.reject(Error(`No credential found for arg: ${JSON.stringify(args)}`)) } return digitalCredentialFrom(result) } getCredentials = async (args?: GetCredentialsArgs): Promise<GetCredentialsResponse> => { const { filter = {}, offset, limit, order = 'createdAt.asc' } = args ?? {} const sortOptions: FindOptionsOrder<DigitalCredentialEntity> = order && typeof order === 'string' ? parseAndValidateOrderOptions<DigitalCredentialEntity>(order) : <FindOptionsOrder<DigitalCredentialEntity>>order const dcRepo = await this.getRepository() const [result, total] = await dcRepo.findAndCount({ where: filter, skip: offset, take: limit, order: sortOptions, }) return { data: digitalCredentialsFrom(result), total, } } removeCredential = async (args: RemoveCredentialArgs): Promise<boolean> => { if (!args) { return false } let query: FindOptionsWhere<DigitalCredentialEntity> = {} if ('id' in args) { query.id = args.id } else if ('hash' in args) { query.hash = args.hash } else { return false } try { const dcRepo = await this.getRepository() // TODO create a flag whether we want to delete recursively or return an error when there are child credentials? const affected = await this.deleteTree(dcRepo, query) return affected > 0 } catch (error) { console.error('Error removing digital credential:', error) return false } } private async deleteTree(dcRepo: Repository<DigitalCredentialEntity>, query: FindOptionsWhere<DigitalCredentialEntity>): Promise<number> { let affected: number = 0 const findResult = await dcRepo.findBy(query) for (const dc of findResult) { affected += await this.deleteTree(dcRepo, { parentId: dc.id }) const result = await dcRepo.delete(dc.id) if (result.affected) { affected += result.affected } } return affected } private async getRepository(): Promise<Repository<DigitalCredentialEntity>> { if (this.dcRepo !== undefined) { return Promise.resolve(this.dcRepo) } this.dcRepo = (await this.dbConnection).getRepository(DigitalCredentialEntity) if (this.dcRepo === undefined) { return Promise.reject(Error('Could not get DigitalCredentialEntity repository')) } return this.dcRepo } updateCredentialState = async (args: UpdateCredentialStateArgs): Promise<DigitalCredential> => { const credentialRepository: Repository<DigitalCredentialEntity> = (await this.dbConnection).getRepository(DigitalCredentialEntity) const whereClause: Record<string, any> = {} if ('id' in args) { whereClause.id = args.id } else if ('hash' in args) { whereClause.hash = args.hash } else { throw new Error('No id or hash param is provided.') } if (!args.verifiedState) { throw new Error('No verifiedState param is provided.') } if (args.verifiedState === CredentialStateType.REVOKED && !args.revokedAt) { throw new Error('No revokedAt param is provided.') } if (args.verifiedState !== CredentialStateType.REVOKED && !args.verifiedAt) { throw new Error('No verifiedAt param is provided.') } const credential: DigitalCredentialEntity | null = await credentialRepository.findOne({ where: whereClause, }) if (!credential) { return Promise.reject(Error(`No credential found for args: ${JSON.stringify(whereClause)}`)) } const updatedCredential: DigitalCredential = { ...credential, ...(args.verifiedState !== CredentialStateType.REVOKED && { verifiedAt: args.verifiedAt }), ...(args.verifiedState === CredentialStateType.REVOKED && { revokedAt: args.revokedAt }), identifierMethod: credential.identifierMethod, lastUpdatedAt: new Date(), verifiedState: args.verifiedState, } debug('Updating credential', credential) const updatedResult: DigitalCredentialEntity = await credentialRepository.save(updatedCredential, { transaction: true }) return digitalCredentialFrom(updatedResult) } private assertValidDigitalCredential(credentialEntity: NonPersistedDigitalCredential): Error | undefined { const { kmsKeyRef, identifierMethod, credentialRole, isIssuerSigned } = credentialEntity const isRoleInvalid = credentialRole === CredentialRole.ISSUER || (credentialRole === CredentialRole.HOLDER && !isIssuerSigned) if (isRoleInvalid && (!kmsKeyRef || !identifierMethod)) { const missingFields = [] if (!kmsKeyRef) missingFields.push('kmsKeyRef') if (!identifierMethod) missingFields.push('identifierMethod') const fields = missingFields.join(' and ') return new Error( `DigitalCredential field(s) ${fields} is/are required for credential role ${credentialRole} with isIssuerSigned=${isIssuerSigned}.`, ) } return undefined } }