diff --git a/__tests__/initial.migration.test.ts b/__tests__/initial.migration.test.ts index 7902e7c0b..20f933f7c 100644 --- a/__tests__/initial.migration.test.ts +++ b/__tests__/initial.migration.test.ts @@ -32,6 +32,7 @@ import { } from '../packages/data-store/src' import { KeyManager } from '../packages/key-manager/src' import { DIDManager } from '../packages/did-manager/src' +import { CredentialPlugin } from "../packages/credential-w3c/src"; import { FakeDidProvider, FakeDidResolver } from '../packages/test-utils/src' import { DataSource, DataSourceOptions } from 'typeorm' @@ -135,12 +136,13 @@ describe('database initial migration tests', () => { new DataStore(dbConnection), new DataStoreORM(dbConnection), new DIDComm(), + new CredentialPlugin() ], }) return true }) afterAll(async () => { - await (await dbConnection).close() + await (await dbConnection).destroy() fs.unlinkSync(databaseFile) }) @@ -260,6 +262,22 @@ describe('database initial migration tests', () => { const msg = await agent.unpackDIDCommMessage(packed) expect(msg.message.body).toEqual({ hello: 'world' }) }) + + it('saves credential with undefined issuanceDate', async () => { + const issuer = await agent.didManagerCreate({ provider: 'did:key' }) + const cred = await agent.createVerifiableCredential({ + credential: { + issuer: issuer.did, + credentialSubject: { + name: 'Alice', + }, + issuanceDate: undefined, + }, + proofFormat: 'jwt', + }) + const stored = await agent.dataStoreSaveVerifiableCredential({ verifiableCredential: cred }) + expect(stored).toBeDefined() + }) }) } }) diff --git a/__tests__/localAgent.test.ts b/__tests__/localAgent.test.ts index 395918524..e0cc0decf 100644 --- a/__tests__/localAgent.test.ts +++ b/__tests__/localAgent.test.ts @@ -264,7 +264,7 @@ const setup = async (options?: IAgentOptions): Promise => { const tearDown = async (): Promise => { try { await (await dbConnection).dropDatabase() - await (await dbConnection).close() + await (await dbConnection).destroy() } catch (e) { // nop } diff --git a/__tests__/localMemoryStoreAgent.test.ts b/__tests__/localMemoryStoreAgent.test.ts index 54000bab9..7beff0ec4 100644 --- a/__tests__/localMemoryStoreAgent.test.ts +++ b/__tests__/localMemoryStoreAgent.test.ts @@ -213,6 +213,7 @@ const setup = async (options?: IAgentOptions): Promise => { const tearDown = async (): Promise => { try { + await dbConnection?.dropDatabase() await dbConnection?.destroy() } catch (e) { // nop diff --git a/__tests__/restAgent.test.ts b/__tests__/restAgent.test.ts index 3d4b88df6..88d0d94e9 100644 --- a/__tests__/restAgent.test.ts +++ b/__tests__/restAgent.test.ts @@ -273,7 +273,7 @@ const tearDown = async (): Promise => { await new Promise((resolve, reject) => restServer.close(resolve)) try { await (await dbConnection).dropDatabase() - await (await dbConnection).close() + await (await dbConnection).destroy() } catch (e) { // nop } diff --git a/__tests__/shared/saveClaims.ts b/__tests__/shared/saveClaims.ts index 0e4d00927..52074b054 100644 --- a/__tests__/shared/saveClaims.ts +++ b/__tests__/shared/saveClaims.ts @@ -11,6 +11,7 @@ import { TAgent, } from '../../packages/core-types/src' import { ISelectiveDisclosure } from '../../packages/selective-disclosure/src' +import { beforeAll } from '@jest/globals' type ConfiguredAgent = TAgent< IDIDManager & ICredentialIssuer & IDataStoreORM & IDataStore & IMessageHandler & ISelectiveDisclosure @@ -28,13 +29,9 @@ export default (testContext: { beforeAll(async () => { await testContext.setup() agent = testContext.getAgent() - }) - afterAll(testContext.tearDown) - - it('should create identifier', async () => { identifier = await agent.didManagerCreate({ kms: 'local' }) - expect(identifier).toHaveProperty('did') }) + afterAll(testContext.tearDown) it('should create verifiable credentials', async () => { // Looping these in a map/forEach throws SQL UNIQUE CONSTRAINT errors @@ -123,59 +120,56 @@ export default (testContext: { { column: 'value', value: ['math', 'art'] }, ], order: [{ column: 'issuanceDate', direction: 'DESC' }], - take: 1 + take: 1, }) expect(credentials).toHaveLength(1) }) it('should be able to limit credentials when searching and sorting', async () => { const credentials = await agent.dataStoreORMGetVerifiableCredentials({ - where: [ - { column: 'type', value: ['VerifiableCredential'] }, - ], + where: [{ column: 'type', value: ['VerifiableCredential'] }], order: [{ column: 'issuanceDate', direction: 'DESC' }], take: 1, - skip: 1 + skip: 1, }) expect(credentials).toHaveLength(1) }) it('should be able to limit credentials when sorting', async () => { const credentialsAllDesc = await agent.dataStoreORMGetVerifiableCredentials({ - order: [{ column: 'issuanceDate', direction: 'DESC' }] + order: [{ column: 'issuanceDate', direction: 'DESC' }], }) const credentialsAllAsc = await agent.dataStoreORMGetVerifiableCredentials({ - order: [{ column: 'issuanceDate', direction: 'ASC' }] + order: [{ column: 'issuanceDate', direction: 'ASC' }], }) const credentialsIdAllDesc = await agent.dataStoreORMGetVerifiableCredentials({ - order: [{ column: 'id', direction: 'DESC' }] + order: [{ column: 'id', direction: 'DESC' }], }) const credentialsIdAllAsc = await agent.dataStoreORMGetVerifiableCredentials({ - order: [{ column: 'id', direction: 'ASC' }] + order: [{ column: 'id', direction: 'ASC' }], }) - const credentials1 = await agent.dataStoreORMGetVerifiableCredentials({ order: [{ column: 'issuanceDate', direction: 'DESC' }], take: 1, - skip: 0 + skip: 0, }) const credentials2 = await agent.dataStoreORMGetVerifiableCredentials({ order: [{ column: 'issuanceDate', direction: 'DESC' }], take: 1, - skip: 1 + skip: 1, }) const credentials3 = await agent.dataStoreORMGetVerifiableCredentials({ order: [{ column: 'issuanceDate', direction: 'ASC' }], take: 2, - skip: 0 + skip: 0, }) const credentials4 = await agent.dataStoreORMGetVerifiableCredentials({ order: [{ column: 'issuanceDate', direction: 'ASC' }], take: 2, - skip: 1 + skip: 1, }) expect(credentialsAllDesc).toHaveLength(3) @@ -190,18 +184,30 @@ export default (testContext: { expect(credentialsIdAllDesc[1].verifiableCredential.id).toEqual('b') expect(credentialsIdAllDesc[2].verifiableCredential.id).toEqual('a') - expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual(credentials1[0].verifiableCredential.issuanceDate) - expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual(credentials2[0].verifiableCredential.issuanceDate) - - expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[2].verifiableCredential.issuanceDate) - expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[1].verifiableCredential.issuanceDate) - expect(credentialsAllDesc[2].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[0].verifiableCredential.issuanceDate) + expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual( + credentials1[0].verifiableCredential.issuanceDate, + ) + expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual( + credentials2[0].verifiableCredential.issuanceDate, + ) + + expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual( + credentialsAllAsc[2].verifiableCredential.issuanceDate, + ) + expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual( + credentialsAllAsc[1].verifiableCredential.issuanceDate, + ) + expect(credentialsAllDesc[2].verifiableCredential.issuanceDate).toEqual( + credentialsAllAsc[0].verifiableCredential.issuanceDate, + ) - expect(new Date(credentials1[0].verifiableCredential.issuanceDate).getTime()) - .toBeGreaterThan(new Date(credentials2[0].verifiableCredential.issuanceDate).getTime()) + expect(new Date(credentials1[0].verifiableCredential.issuanceDate).getTime()).toBeGreaterThan( + new Date(credentials2[0].verifiableCredential.issuanceDate).getTime(), + ) - expect(new Date(credentials4[0].verifiableCredential.issuanceDate).getTime()) - .toBeGreaterThan(new Date(credentials3[0].verifiableCredential.issuanceDate).getTime()) + expect(new Date(credentials4[0].verifiableCredential.issuanceDate).getTime()).toBeGreaterThan( + new Date(credentials3[0].verifiableCredential.issuanceDate).getTime(), + ) }) it('should be able to delete credential', async () => { @@ -216,4 +222,78 @@ export default (testContext: { expect(credentials2).toHaveLength(2) }) }) + + describe('credential queries', () => { + let agent: ConfiguredAgent + + beforeAll(async () => { + await testContext.setup() + agent = testContext.getAgent() + }) + + it('should query by type and issuer', async () => { + const issuer = await agent.didManagerCreate({ kms: 'local' }) + const cred1 = await agent.createVerifiableCredential({ + credential: { + issuer: { id: issuer.did }, + type: ['Test123'], + credentialSubject:{ + hello: 'world', + } + }, + proofFormat: 'jwt', + }) + const credentialHash = await agent.dataStoreSaveVerifiableCredential({ verifiableCredential: cred1 }) + + const found = await agent.dataStoreORMGetVerifiableCredentials({ + where: [ + { column: 'type', value: ['VerifiableCredential,Test123'] }, + { column: 'issuer', value: [issuer.did] }, + ], + }) + expect(found).toHaveLength(1) + expect(found[0].hash).toEqual(credentialHash) + }) + + it('should query by type and issuer with orderby', async () => { + const issuer = await agent.didManagerCreate({ kms: 'local' }) + + const cred1 = await agent.createVerifiableCredential({ + credential: { + issuer: { id: issuer.did }, + type: ['Test321'], + credentialSubject:{ + first: true, + }, + issuanceDate: undefined // intentionally use a nullish looking value here + }, + proofFormat: 'jwt', + }) + const cred1Hash = await agent.dataStoreSaveVerifiableCredential({ verifiableCredential: cred1 }) + + const cred2 = await agent.createVerifiableCredential({ + credential: { + issuer: { id: issuer.did }, + type: ['Test321'], + credentialSubject:{ + first: false, + }, + issuanceDate: '2000-01-01T00:00:00Z', + }, + proofFormat: 'jwt', + }) + const cred2Hash = await agent.dataStoreSaveVerifiableCredential({ verifiableCredential: cred2 }) + + const found = await agent.dataStoreORMGetVerifiableCredentials({ + where: [ + { column: 'type', value: ['VerifiableCredential,Test321'] }, + { column: 'issuer', value: [issuer.did] }, + ], + order: [{ column: 'issuanceDate', direction: 'DESC' }], + take: 1 + }) + expect(found).toHaveLength(1) + expect(found[0].hash).toEqual(cred2Hash) + }) + }) } diff --git a/packages/data-store/src/entities/claim.ts b/packages/data-store/src/entities/claim.ts index 8632d0bbd..ef40930f5 100644 --- a/packages/data-store/src/entities/claim.ts +++ b/packages/data-store/src/entities/claim.ts @@ -36,9 +36,10 @@ export class Claim extends BaseEntity { // @ts-ignore credential: Relation - @Column() + // The VC data model does not allow credentials without an issuance date, but some credentials from the wild may + @Column({ nullable: true }) // @ts-ignore - issuanceDate: Date + issuanceDate?: Date @Column({ nullable: true }) expirationDate?: Date diff --git a/packages/data-store/src/entities/credential.ts b/packages/data-store/src/entities/credential.ts index ae9407218..3f3874ad4 100644 --- a/packages/data-store/src/entities/credential.ts +++ b/packages/data-store/src/entities/credential.ts @@ -18,8 +18,7 @@ import { asArray, computeEntryHash, extractIssuer } from '@veramo/utils' /** * Represents some common properties of a Verifiable Credential that are stored in a TypeORM database for querying. * - * @see {@link @veramo/core-types#IDataStoreORM.dataStoreORMGetVerifiableCredentials | dataStoreORMGetVerifiableCredentials} - * for the interface defining how this can be queried. + * @see {@link @veramo/core-types#IDataStoreORM.dataStoreORMGetVerifiableCredentials | dataStoreORMGetVerifiableCredentials} for the interface defining how this can be queried. * * @see {@link @veramo/data-store#DataStoreORM | DataStoreORM} for the implementation of the query interface. * @@ -65,9 +64,10 @@ export class Credential extends BaseEntity { @Column({ nullable: true }) id?: string - @Column() + // The VC data model does not allow credentials without an issuance date, but some credentials from the wild may + @Column({ nullable: true }) // @ts-ignore - issuanceDate: Date + issuanceDate?: Date @Column({ nullable: true }) expirationDate?: Date diff --git a/packages/data-store/src/migrations/5.allowNullVCIssuanceDate.ts b/packages/data-store/src/migrations/5.allowNullVCIssuanceDate.ts new file mode 100644 index 000000000..4cf410e12 --- /dev/null +++ b/packages/data-store/src/migrations/5.allowNullVCIssuanceDate.ts @@ -0,0 +1,116 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { Claim, Credential } from '../index.js' +import Debug from 'debug' +import { migrationGetExistingTableByName } from './migration-functions.js' + +const debug = Debug('veramo:data-store:migrate-credentials-issuance-date') + +/** + * Reduce issuanceDate constraint of Credential and Claim entities. + * + * @public + */ +export class AllowNullIssuanceDateForCredentials1697194289809 implements MigrationInterface { + name = 'AllowNullIssuanceDateForCredentials1697194289809' // Used in case this class gets minified, which would + // change the classname + + async up(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.driver.options.type === 'sqlite') { + debug(`splitting migration into multiple transactions to allow sqlite table updates`) + await queryRunner.commitTransaction() + debug(`turning off foreign keys`) + await queryRunner.query('PRAGMA foreign_keys=off') + await queryRunner.startTransaction() + } + + // update issuanceDate column for credentials + { + const table = migrationGetExistingTableByName(queryRunner, 'credential') + const oldColumn = table?.findColumnByName('issuanceDate')! + const newColumn = oldColumn.clone() + newColumn.isNullable = true + debug(`updating issuanceDate for credentials to allow null`) + await queryRunner.changeColumn(table!, oldColumn, newColumn) + debug(`updated issuanceDate for credentials to allow null`) + } + + // update issuanceDate column for claims + { + const table = migrationGetExistingTableByName(queryRunner, 'claim') + const oldColumn = table?.findColumnByName('issuanceDate')! + const newColumn = oldColumn.clone() + newColumn.isNullable = true + debug(`updating issuanceDate for credential claims to allow null`) + await queryRunner.changeColumn(table!, oldColumn, newColumn) + debug(`updated issuanceDate for credential claims to allow null`) + } + + if (queryRunner.connection.driver.options.type === 'sqlite') { + debug(`splitting migration into multiple transactions to allow sqlite table updates`) + await queryRunner.commitTransaction() + debug(`turning on foreign keys`) + await queryRunner.query('PRAGMA foreign_keys=on') + await queryRunner.startTransaction() + } + } + + async down(queryRunner: QueryRunner): Promise { + if (queryRunner.connection.driver.options.type === 'sqlite') { + debug(`splitting migration into multiple transactions to allow sqlite table updates`) + await queryRunner.commitTransaction() + debug(`turning off foreign keys`) + await queryRunner.query('PRAGMA foreign_keys=off') + await queryRunner.startTransaction() + } + + // update issuanceDate column for credential table + { + const table = migrationGetExistingTableByName(queryRunner, 'credential') + debug(`DOWN update NULL 'issuanceDate' with FAKE data for '${table.name}' table`) + await queryRunner.manager + .createQueryBuilder() + .update(Credential) + .set({ issuanceDate: new Date(0) }) + .where('issuanceDate is NULL') + .execute() + // update issuanceDate column + + const oldColumn = table?.findColumnByName('issuanceDate')! + const newColumn = oldColumn.clone() + newColumn.isNullable = false + debug(`updating issuanceDate for credentials to NOT allow null`) + await queryRunner.changeColumn(table!, oldColumn, newColumn) + debug(`updated issuanceDate for credentials to NOT allow null`) + } + + // update issuanceDate for claim table + { + const table = migrationGetExistingTableByName(queryRunner, 'claim') + debug(`DOWN update NULL 'issuanceDate' with FAKE data for '${table.name}' table`) + await queryRunner.manager + .createQueryBuilder() + .update(Claim) + .set({ issuanceDate: new Date(0) }) + .where('issuanceDate is NULL') + .execute() + // update issuanceDate column + + const oldColumn = table?.findColumnByName('issuanceDate')! + const newColumn = oldColumn.clone() + newColumn.isNullable = false + debug(`updating issuanceDate for credential claims to NOT allow null`) + await queryRunner.changeColumn(table!, oldColumn, newColumn) + debug(`updated issuanceDate for credential claims to NOT allow null`) + } + + if (queryRunner.connection.driver.options.type === 'sqlite') { + debug(`splitting migration into multiple transactions to allow sqlite table updates`) + await queryRunner.commitTransaction() + debug(`turning on foreign keys`) + await queryRunner.query('PRAGMA foreign_keys=on') + await queryRunner.startTransaction() + } + + debug(`DOWN updated issuanceDate for credentials to NOT allow null`) + } +} diff --git a/packages/data-store/src/migrations/index.ts b/packages/data-store/src/migrations/index.ts index 45bc0d4e8..0240cca01 100644 --- a/packages/data-store/src/migrations/index.ts +++ b/packages/data-store/src/migrations/index.ts @@ -2,6 +2,7 @@ import { CreateDatabase1447159020001 } from './1.createDatabase.js' import { SimplifyRelations1447159020002 } from './2.simplifyRelations.js' import { CreatePrivateKeyStorage1629293428674 } from './3.createPrivateKeyStorage.js' import { AllowNullIssuanceDateForPresentations1637237492913 } from './4.allowNullVPIssuanceDate.js' +import { AllowNullIssuanceDateForCredentials1697194289809 } from './5.allowNullVCIssuanceDate.js' /** @@ -24,4 +25,5 @@ export const migrations = [ SimplifyRelations1447159020002, CreatePrivateKeyStorage1629293428674, AllowNullIssuanceDateForPresentations1637237492913, + AllowNullIssuanceDateForCredentials1697194289809 ]