diff --git a/packages/contact-manager/__tests__/localAgent.test.ts b/packages/contact-manager/__tests__/localAgent.test.ts index 640ca6d7e..87cdfd75a 100644 --- a/packages/contact-manager/__tests__/localAgent.test.ts +++ b/packages/contact-manager/__tests__/localAgent.test.ts @@ -1,11 +1,11 @@ import { createObjects, getConfig } from '../../agent-config/dist' -import { Connection } from 'typeorm' +import { DataSource } from 'typeorm' jest.setTimeout(30000) import contactManagerAgentLogic from './shared/contactManagerAgentLogic' -let dbConnection: Promise +let dbConnection: Promise let agent: any const setup = async (): Promise => { @@ -29,6 +29,6 @@ const testContext = { tearDown, } -describe('Local integration tests', () => { +describe('Local integration tests', (): void => { contactManagerAgentLogic(testContext) }) diff --git a/packages/contact-manager/__tests__/restAgent.test.ts b/packages/contact-manager/__tests__/restAgent.test.ts index f58f177a3..2e9044520 100644 --- a/packages/contact-manager/__tests__/restAgent.test.ts +++ b/packages/contact-manager/__tests__/restAgent.test.ts @@ -1,8 +1,8 @@ import 'cross-fetch/polyfill' // @ts-ignore -import express from 'express' +import express, { Router } from 'express' import { Server } from 'http' -import { Connection } from 'typeorm' +import { DataSource } from 'typeorm' import { IAgent, createAgent, IAgentOptions } from '@veramo/core' import { AgentRestClient } from '@veramo/remote-client' import { AgentRouter, RequestWithAgentRouter } from '@veramo/remote-server' @@ -17,7 +17,7 @@ const basePath = '/agent' let serverAgent: IAgent let restServer: Server -let dbConnection: Promise +let dbConnection: Promise const getAgent = (options?: IAgentOptions) => createAgent({ @@ -41,14 +41,14 @@ const setup = async (): Promise => { exposedMethods: serverAgent.availableMethods(), }) - const requestWithAgent = RequestWithAgentRouter({ + const requestWithAgent: Router = RequestWithAgentRouter({ agent: serverAgent, }) - return new Promise((resolve) => { + return new Promise((resolve): void => { const app = express() app.use(basePath, requestWithAgent, agentRouter) - restServer = app.listen(port, () => { + restServer = app.listen(port, (): void => { resolve(true) }) }) @@ -66,6 +66,6 @@ const testContext = { tearDown, } -describe('REST integration tests', () => { +describe('REST integration tests', (): void => { contactManagerAgentLogic(testContext) }) diff --git a/packages/contact-manager/__tests__/shared/contactManagerAgentLogic.ts b/packages/contact-manager/__tests__/shared/contactManagerAgentLogic.ts index 9a0a90085..a4628a83e 100644 --- a/packages/contact-manager/__tests__/shared/contactManagerAgentLogic.ts +++ b/packages/contact-manager/__tests__/shared/contactManagerAgentLogic.ts @@ -1,26 +1,50 @@ import { TAgent } from '@veramo/core' -import { IContactManager } from '../../src' -import { CorrelationIdentifierEnum, IContact, IdentityRoleEnum, IIdentity } from '../../../data-store/src' +import { AddContactArgs, IContactManager } from '../../src' +import { + PartyTypeEnum, + CorrelationIdentifierEnum, + NonPersistedIdentity, + Party, + IdentityRoleEnum, + Identity, + NaturalPerson, + GetPartiesArgs, + PartyRelationship, +} from '../../../data-store/src' type ConfiguredAgent = TAgent export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Promise; tearDown: () => Promise }): void => { - describe('Contact Manager Agent Plugin', () => { + describe('Contact Manager Agent Plugin', (): void => { let agent: ConfiguredAgent - let defaultContact: IContact - let defaultIdentity: IIdentity + let defaultContact: Party + let defaultIdentity: Identity beforeAll(async (): Promise => { await testContext.setup() agent = testContext.getAgent() - const contact = { - name: 'default_contact', - alias: 'default_contact_alias', + const contact: AddContactArgs = { + //NonPersistedParty + firstName: 'default_first_name', + middleName: 'default_middle_name', + lastName: 'default_last_name', + displayName: 'default_display_name', + contactType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + // contact: { + // firstName: 'default_first_name', + // middleName: 'default_middle_name', + // lastName: 'default_last_name', + // displayName: 'default_display_name', + // }, uri: 'example.com', } const correlationId = 'default_example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -34,8 +58,8 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro afterAll(testContext.tearDown) - it('should get contact by id', async () => { - const result = await agent.cmGetContact({ contactId: defaultContact.id }) + it('should get contact by id', async (): Promise => { + const result: Party = await agent.cmGetContact({ contactId: defaultContact.id }) expect(result.id).toEqual(defaultContact.id) }) @@ -43,122 +67,149 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro it('should throw error when getting contact with unknown id', async (): Promise => { const contactId = 'unknownContactId' - await expect(agent.cmGetContact({ contactId })).rejects.toThrow(`No contact found for id: ${contactId}`) + await expect(agent.cmGetContact({ contactId })).rejects.toThrow(`No party found for id: ${contactId}`) }) - it('should get all contacts', async () => { - const result = await agent.cmGetContacts() + it('should get all contacts', async (): Promise => { + const result: Array = await agent.cmGetContacts() expect(result.length).toBeGreaterThan(0) }) it('should get contacts by filter', async (): Promise => { - const args = { - filter: [{ name: 'default_contact' }, { alias: 'default_contact_alias' }, { uri: 'example.com' }], + const args: GetPartiesArgs = { + filter: [ + { + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + }, + }, + { + contact: { + displayName: 'default_display_name', + }, + }, + { uri: 'example.com' }, + ], } - const result = await agent.cmGetContacts(args) + const result: Array = await agent.cmGetContacts(args) expect(result.length).toBe(1) }) it('should get contacts by name', async (): Promise => { - const args = { - filter: [{ name: 'default_contact' }], + const args: GetPartiesArgs = { + filter: [ + { contact: { firstName: 'default_first_name' } }, + { contact: { middleName: 'default_middle_name' } }, + { contact: { lastName: 'default_last_name' } }, + ], } - const result = await agent.cmGetContacts(args) + const result: Array = await agent.cmGetContacts(args) expect(result.length).toBe(1) }) - it('should get contacts by alias', async (): Promise => { - const args = { - filter: [{ alias: 'default_contact_alias' }], + it('should get contacts by display name', async (): Promise => { + const args: GetPartiesArgs = { + filter: [{ contact: { displayName: 'default_display_name' } }], } - const result = await agent.cmGetContacts(args) + const result: Array = await agent.cmGetContacts(args) expect(result.length).toBe(1) }) it('should get contacts by uri', async (): Promise => { - const args = { + const args: GetPartiesArgs = { filter: [{ uri: 'example.com' }], } - const result = await agent.cmGetContacts(args) + const result: Array = await agent.cmGetContacts(args) expect(result.length).toBe(1) }) it('should return no contacts if filter does not match', async (): Promise => { - const args = { - filter: [{ name: 'no_match_contact' }, { alias: 'no_match_contact_alias' }, { uri: 'no_match_example.com' }], + const args: GetPartiesArgs = { + filter: [{ contact: { displayName: 'no_match_contact_display_name' } }, { uri: 'no_match_example.com' }], } - const result = await agent.cmGetContacts(args) + const result: Array = await agent.cmGetContacts(args) expect(result.length).toBe(0) }) it('should add contact', async (): Promise => { - const contact = { - name: 'new_contact', - alias: 'new_contact_alias', + const contact: AddContactArgs = { + //NonPersistedParty + firstName: 'new_first_name', + middleName: 'new_middle_name', + lastName: 'new_last_name', + displayName: 'new_display_name', + contactType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'new_name', + description: 'new_description', + }, + // contact: { + // firstName: 'new_first_name', + // middleName: 'new_middle_name', + // lastName: 'new_last_name', + // displayName: 'new_display_name', + // }, uri: 'example.com', + // TODO create better tests for electronicAddresses + electronicAddresses: [ + { + type: 'email', + electronicAddress: 'sphereon@sphereon.com', + }, + ], } - const result = await agent.cmAddContact(contact) + const result: Party = await agent.cmAddContact(contact) - expect(result.name).toEqual(contact.name) - expect(result.alias).toEqual(contact.alias) + expect(result.partyType.type).toEqual(contact.contactType.type) + expect(result.partyType.name).toEqual(contact.contactType.name) + expect(result.partyType.description).toEqual(contact.contactType.description) + expect((result.contact).firstName).toEqual(contact.firstName) + expect((result.contact).middleName).toEqual(contact.middleName) + expect((result.contact).lastName).toEqual(contact.lastName) + expect((result.contact).displayName).toEqual(contact.displayName) expect(result.uri).toEqual(contact.uri) - }) - - it('should throw error when adding contact with duplicate name', async (): Promise => { - const name = 'default_contact' - const alias = 'default_contact_new_alias' - const contact = { - name, - alias, - uri: 'example.com', - } - - await expect(agent.cmAddContact(contact)).rejects.toThrow(`Duplicate names or aliases are not allowed. Name: ${name}, Alias: ${alias}`) - }) - - it('should throw error when adding contact with duplicate alias', async (): Promise => { - const name = 'default_new_contact' - const alias = 'default_contact_alias' - const contact = { - name, - alias, - uri: 'example.com', - } - - await expect(agent.cmAddContact(contact)).rejects.toThrow(`Duplicate names or aliases are not allowed. Name: ${name}, Alias: ${alias}`) + expect(result.electronicAddresses).toBeDefined() + expect(result.electronicAddresses.length).toEqual(1) }) it('should update contact by id', async (): Promise => { - const contactName = 'updated_contact' - const contact = { + const contactFirstName = 'updated_contact_first_name' + const contact: Party = { ...defaultContact, - name: contactName, + contact: { + ...defaultContact.contact, + firstName: contactFirstName, + }, } - const result = await agent.cmUpdateContact({ contact }) + const result: Party = await agent.cmUpdateContact({ contact }) - expect(result.name).toEqual(contactName) + expect((result.contact).firstName).toEqual(contactFirstName) }) it('should throw error when updating contact with unknown id', async (): Promise => { const contactId = 'unknownContactId' - const contact = { + const contact: Party = { ...defaultContact, id: contactId, - name: 'new_name', + contact: { + ...defaultContact.contact, + firstName: 'new_first_name', + }, } - await expect(agent.cmUpdateContact({ contact })).rejects.toThrow(`No contact found for id: ${contactId}`) + await expect(agent.cmUpdateContact({ contact })).rejects.toThrow(`No party found for id: ${contactId}`) }) - it('should get identity by id', async () => { - const result = await agent.cmGetIdentity({ identityId: defaultIdentity.id }) + it('should get identity by id', async (): Promise => { + const result: Identity = await agent.cmGetIdentity({ identityId: defaultIdentity.id }) expect(result.id).toEqual(defaultIdentity.id) }) @@ -174,14 +225,14 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro }) it('should get all identities for contact', async (): Promise => { - const result = await agent.cmGetIdentities({ filter: [{ contactId: defaultContact.id }] }) + const result: Array = await agent.cmGetIdentities({ filter: [{ partyId: defaultContact.id }] }) expect(result.length).toBeGreaterThan(0) }) it('should add identity to contact', async (): Promise => { const correlationId = 'new_example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -190,8 +241,8 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro }, } - const result = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) - const contact = await agent.cmGetContact({ contactId: defaultContact.id }) + const result: Identity = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) + const contact: Party = await agent.cmGetContact({ contactId: defaultContact.id }) expect(result).not.toBeNull() expect(contact.identities.length).toEqual(2) @@ -205,7 +256,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro it('should throw error when adding identity with invalid identifier', async (): Promise => { const correlationId = 'missing_connection_add_example' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -221,7 +272,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro it('should throw error when updating identity with invalid identifier', async (): Promise => { const correlationId = 'missing_connection_update_example' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -229,7 +280,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro correlationId, }, } - const result = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) + const result: Identity = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) result.identifier = { ...result.identifier, type: CorrelationIdentifierEnum.URL } await expect(agent.cmUpdateIdentity({ identity: result })).rejects.toThrow(`Identity with correlation type url should contain a connection`) @@ -237,7 +288,7 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro it('should update identity', async (): Promise => { const correlationId = 'new_update_example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: 'update_example_did', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -245,14 +296,135 @@ export default (testContext: { getAgent: () => ConfiguredAgent; setup: () => Pro correlationId: 'update_example_did', }, } - const result = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) + const result: Identity = await agent.cmAddIdentity({ contactId: defaultContact.id, identity }) result.identifier = { ...result.identifier, correlationId } await agent.cmUpdateIdentity({ identity: result }) - const updatedIdentity = await agent.cmGetIdentity({ identityId: result.id }) + const updatedIdentity: Identity = await agent.cmGetIdentity({ identityId: result.id }) expect(updatedIdentity).not.toBeNull() expect(updatedIdentity.identifier.correlationId).toEqual(correlationId) }) + + it('should add relationship', async (): Promise => { + const contact: AddContactArgs = { + //NonPersistedParty + firstName: 'relation_first_name', + middleName: 'relation_middle_name', + lastName: 'relation_last_name', + displayName: 'relation_display_name', + contactType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d285', + name: 'relation_contact_type_name', + description: 'new_description', + }, + // contact: { + // firstName: 'relation_first_name', + // middleName: 'relation_middle_name', + // lastName: 'relation_last_name', + // displayName: 'relation_display_name', + // }, + uri: 'example.com', + } + + const savedContact: Party = await agent.cmAddContact(contact) + + // TODO why does this filter not work on only first name? + const args1: GetPartiesArgs = { + filter: [ + { contact: { firstName: 'default_first_name' } }, + { contact: { middleName: 'default_middle_name' } }, + // { contactOwner: { lastName: 'default_last_name'} }, + ], + } + const otherContacts: Array = await agent.cmGetContacts(args1) + + expect(otherContacts.length).toEqual(1) + + const relationship: PartyRelationship = await agent.cmAddRelationship({ + leftId: savedContact.id, + rightId: otherContacts[0].id, + }) + + expect(relationship).toBeDefined() + + // TODO why does this filter not work on only first name? + const args2: GetPartiesArgs = { + filter: [{ contact: { firstName: 'relation_first_name' } }, { contact: { middleName: 'relation_middle_name' } }], + } + const result: Array = await agent.cmGetContacts(args2) + + expect(result.length).toEqual(1) + expect(result[0].relationships.length).toEqual(1) + expect(result[0].relationships[0].leftId).toEqual(savedContact.id) + expect(result[0].relationships[0].rightId).toEqual(otherContacts[0].id) + }) + + it('should remove relationship', async (): Promise => { + const contact: AddContactArgs = { + //NonPersistedParty + firstName: 'remove_relation_first_name', + middleName: 'remove_relation_middle_name', + lastName: 'remove_relation_last_name', + displayName: 'remove_relation_display_name', + contactType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d286', + name: 'remove_relation_contact_type_name', + description: 'new_description', + }, + // contact: { + // firstName: 'remove_relation_first_name', + // middleName: 'remove_relation_middle_name', + // lastName: 'remove_relation_last_name', + // displayName: 'remove_relation_display_name', + // }, + uri: 'example.com', + } + + const savedContact: Party = await agent.cmAddContact(contact) + + // TODO why does this filter not work on only first name? + const args1: GetPartiesArgs = { + filter: [ + { contact: { firstName: 'default_first_name' } }, + { contact: { middleName: 'default_middle_name' } }, + // { contactOwner: { lastName: 'default_last_name'} }, + ], + } + const otherContacts: Array = await agent.cmGetContacts(args1) + + expect(otherContacts.length).toEqual(1) + + const relationship: PartyRelationship = await agent.cmAddRelationship({ + leftId: savedContact.id, + rightId: otherContacts[0].id, + }) + + expect(relationship).toBeDefined() + + // TODO why does this filter not work on only first name? + const args2: GetPartiesArgs = { + filter: [ + { contact: { firstName: 'relation_first_name' } }, + { contact: { middleName: 'relation_middle_name' } }, + // { contactOwner: { lastName: 'default_last_name'} }, + ], + } + const retrievedContact: Array = await agent.cmGetContacts(args2) + + expect(retrievedContact.length).toEqual(1) + expect(retrievedContact[0].relationships.length).toEqual(1) + // expect(result[0].relationships[0].leftContactId).toEqual(savedContact.id) + // expect(result[0].relationships[0].rightContactId).toEqual(otherContacts[0].id) + + const removeRelationshipResult: boolean = await agent.cmRemoveRelationship({ relationshipId: relationship.id }) + expect(removeRelationshipResult).toBeTruthy() + + const result: Party = await agent.cmGetContact({ contactId: savedContact.id }) + + expect(result.relationships.length).toEqual(0) + }) }) } diff --git a/packages/contact-manager/agent.yml b/packages/contact-manager/agent.yml index 3e22f5048..fb3806515 100644 --- a/packages/contact-manager/agent.yml +++ b/packages/contact-manager/agent.yml @@ -14,6 +14,16 @@ constants: - cmAddIdentity - cmUpdateIdentity - cmRemoveIdentity + - cmGetRelationship + - cmGetRelationships + - cmUpdateRelationship + - cmAddRelationship + - cmRemoveRelationship + - cmGetContactType + - cmGetContactTypes + - cmAddContactType + - cmUpdateContactType + - cmRemoveContactType dbConnection: $require: typeorm?t=function#createConnection diff --git a/packages/contact-manager/src/agent/ContactManager.ts b/packages/contact-manager/src/agent/ContactManager.ts index 47abb9350..561e7d43a 100644 --- a/packages/contact-manager/src/agent/ContactManager.ts +++ b/packages/contact-manager/src/agent/ContactManager.ts @@ -1,20 +1,39 @@ import { IAgentPlugin } from '@veramo/core' import { schema } from '../index' import { - IAddContactArgs, - IUpdateContactArgs, - IGetIdentitiesArgs, - IRemoveContactArgs, - IAddIdentityArgs, + AddContactArgs, + UpdateContactArgs, + GetIdentitiesArgs, + RemoveContactArgs, + AddIdentityArgs, IContactManager, - IGetIdentityArgs, - IRemoveIdentityArgs, - IRequiredContext, - IUpdateIdentityArgs, - IGetContactsArgs, - IGetContactArgs, + GetIdentityArgs, + RemoveIdentityArgs, + RequiredContext, + UpdateIdentityArgs, + GetContactsArgs, + GetContactArgs, + AddRelationshipArgs, + RemoveRelationshipArgs, + GetRelationshipArgs, + GetRelationshipsArgs, + UpdateRelationshipArgs, + AddContactTypeArgs, + GetContactTypeArgs, + GetContactTypesArgs, + RemoveContactTypeArgs, + UpdateContactTypeArgs, } from '../types/IContactManager' -import { IContact, IIdentity, AbstractContactStore } from '@sphereon/ssi-sdk.data-store' +import { + AbstractContactStore, + Party as Contact, + Identity, + PartyRelationship as ContactRelationship, + PartyType as ContactType, + NonPersistedContact, + isNaturalPerson, + isOrganization, +} from '@sphereon/ssi-sdk.data-store' /** * {@inheritDoc IContactManager} @@ -32,6 +51,16 @@ export class ContactManager implements IAgentPlugin { cmAddIdentity: this.cmAddIdentity.bind(this), cmUpdateIdentity: this.cmUpdateIdentity.bind(this), cmRemoveIdentity: this.cmRemoveIdentity.bind(this), + cmAddRelationship: this.cmAddRelationship.bind(this), + cmRemoveRelationship: this.cmRemoveRelationship.bind(this), + cmGetRelationship: this.cmGetRelationship.bind(this), + cmGetRelationships: this.cmGetRelationships.bind(this), + cmUpdateRelationship: this.cmUpdateRelationship.bind(this), + cmGetContactType: this.cmGetContactType.bind(this), + cmGetContactTypes: this.cmGetContactTypes.bind(this), + cmAddContactType: this.cmAddContactType.bind(this), + cmUpdateContactType: this.cmUpdateContactType.bind(this), + cmRemoveContactType: this.cmRemoveContactType.bind(this), } private readonly store: AbstractContactStore @@ -41,52 +70,119 @@ export class ContactManager implements IAgentPlugin { } /** {@inheritDoc IContactManager.cmGetContact} */ - private async cmGetContact(args: IGetContactArgs, context: IRequiredContext): Promise { - return this.store.getContact(args) + private async cmGetContact(args: GetContactArgs, context: RequiredContext): Promise { + return this.store.getParty({ partyId: args.contactId }) } /** {@inheritDoc IContactManager.cmGetContacts} */ - private async cmGetContacts(args?: IGetContactsArgs): Promise> { - return this.store.getContacts(args) + private async cmGetContacts(args?: GetContactsArgs): Promise> { + return this.store.getParties(args) } /** {@inheritDoc IContactManager.cmAddContact} */ - private async cmAddContact(args: IAddContactArgs, context: IRequiredContext): Promise { - return this.store.addContact(args) + private async cmAddContact(args: AddContactArgs, context: RequiredContext): Promise { + return this.store.addParty({ + uri: args.uri, + partyType: args.contactType, + contact: this.getContactInformationFrom(args), + identities: args.identities, + electronicAddresses: args.electronicAddresses, + }) } /** {@inheritDoc IContactManager.cmUpdateContact} */ - private async cmUpdateContact(args: IUpdateContactArgs, context: IRequiredContext): Promise { - return this.store.updateContact(args) + private async cmUpdateContact(args: UpdateContactArgs, context: RequiredContext): Promise { + return this.store.updateParty({ party: args.contact }) } /** {@inheritDoc IContactManager.cmRemoveContact} */ - private async cmRemoveContact(args: IRemoveContactArgs, context: IRequiredContext): Promise { - return this.store.removeContact(args).then(() => true) + private async cmRemoveContact(args: RemoveContactArgs, context: RequiredContext): Promise { + return this.store.removeParty({ partyId: args.contactId }).then(() => true) } /** {@inheritDoc IContactManager.cmGetIdentity} */ - private async cmGetIdentity(args: IGetIdentityArgs, context: IRequiredContext): Promise { + private async cmGetIdentity(args: GetIdentityArgs, context: RequiredContext): Promise { return this.store.getIdentity(args) } /** {@inheritDoc IContactManager.cmGetIdentities} */ - private async cmGetIdentities(args: IGetIdentitiesArgs, context: IRequiredContext): Promise> { + private async cmGetIdentities(args?: GetIdentitiesArgs): Promise> { return this.store.getIdentities(args) } /** {@inheritDoc IContactManager.cmAddIdentity} */ - private async cmAddIdentity(args: IAddIdentityArgs, context: IRequiredContext): Promise { - return this.store.addIdentity(args) + private async cmAddIdentity(args: AddIdentityArgs, context: RequiredContext): Promise { + return this.store.addIdentity({ partyId: args.contactId, identity: args.identity }) } /** {@inheritDoc IContactManager.cmUpdateIdentity} */ - private async cmUpdateIdentity(args: IUpdateIdentityArgs, context: IRequiredContext): Promise { + private async cmUpdateIdentity(args: UpdateIdentityArgs, context: RequiredContext): Promise { return this.store.updateIdentity(args) } /** {@inheritDoc IContactManager.cmRemoveIdentity} */ - private async cmRemoveIdentity(args: IRemoveIdentityArgs, context: IRequiredContext): Promise { - return this.store.removeIdentity(args).then(() => true) // TODO + private async cmRemoveIdentity(args: RemoveIdentityArgs, context: RequiredContext): Promise { + return this.store.removeIdentity(args).then(() => true) + } + + /** {@inheritDoc IContactManager.cmAddRelationship} */ + private async cmAddRelationship(args: AddRelationshipArgs, context: RequiredContext): Promise { + return this.store.addRelationship(args) + } + + /** {@inheritDoc IContactManager.cmRemoveRelationship} */ + private async cmRemoveRelationship(args: RemoveRelationshipArgs, context: RequiredContext): Promise { + return this.store.removeRelationship(args).then(() => true) + } + + /** {@inheritDoc IContactManager.cmGetRelationship} */ + private async cmGetRelationship(args: GetRelationshipArgs, context: RequiredContext): Promise { + return this.store.getRelationship(args) + } + + /** {@inheritDoc IContactManager.cmGetRelationships} */ + private async cmGetRelationships(args?: GetRelationshipsArgs): Promise> { + return this.store.getRelationships(args) + } + + /** {@inheritDoc IContactManager.cmUpdateRelationship} */ + private async cmUpdateRelationship(args: UpdateRelationshipArgs, context: RequiredContext): Promise { + return this.store.updateRelationship(args) + } + + /** {@inheritDoc IContactManager.cmGetContactType} */ + private async cmGetContactType(args: GetContactTypeArgs, context: RequiredContext): Promise { + return this.store.getPartyType({ partyTypeId: args.contactTypeId }) } + + /** {@inheritDoc IContactManager.cmGetContactTypes} */ + private async cmGetContactTypes(args?: GetContactTypesArgs): Promise> { + return this.store.getPartyTypes(args) + } + + /** {@inheritDoc IContactManager.cmAddContactType} */ + private async cmAddContactType(args: AddContactTypeArgs, context: RequiredContext): Promise { + return this.store.addPartyType(args) + } + + /** {@inheritDoc IContactManager.cmUpdateContactType} */ + private async cmUpdateContactType(args: UpdateContactTypeArgs, context: RequiredContext): Promise { + return this.store.updatePartyType({ partyType: args.contactType }) + } + + /** {@inheritDoc IContactManager.cmRemoveContactType} */ + private async cmRemoveContactType(args: RemoveContactTypeArgs, context: RequiredContext): Promise { + return this.store.removePartyType({ partyTypeId: args.contactTypeId }).then(() => true) + } + + private getContactInformationFrom(contact: any): NonPersistedContact { + if (isNaturalPerson(contact)) { + return { firstName: contact.firstName, middleName: contact.middleName, lastName: contact.lastName, displayName: contact.displayName } + } else if (isOrganization(contact)) { + return { legalName: contact.legalName, displayName: contact.displayName } + } + + throw new Error('Contact not supported') + } + } diff --git a/packages/contact-manager/src/types/IContactManager.ts b/packages/contact-manager/src/types/IContactManager.ts index 3a1c1bd3a..ad5d4b044 100644 --- a/packages/contact-manager/src/types/IContactManager.ts +++ b/packages/contact-manager/src/types/IContactManager.ts @@ -1,61 +1,133 @@ import { IAgentContext, IPluginMethodMap } from '@veramo/core' -import { FindContactArgs, FindIdentityArgs, IBasicIdentity, IContact, IIdentity } from '@sphereon/ssi-sdk.data-store' +import { + Identity, + NonPersistedIdentity, + FindRelationshipArgs, + FindIdentityArgs, + NonPersistedContact, + PartyTypeEnum as ContactTypeEnum, + NonPersistedPartyType as NonPersistedContactType, + FindPartyTypeArgs as FindContactTypeArgs, + FindPartyArgs as FindContactArgs, + PartyRelationship as ContactRelationship, + PartyType as ContactType, + Party as Contact, + NonPersistedParty, +} from '@sphereon/ssi-sdk.data-store' export interface IContactManager extends IPluginMethodMap { - cmGetContact(args: IGetContactArgs, context: IRequiredContext): Promise - cmGetContacts(args?: IGetContactsArgs): Promise> - cmAddContact(args: IAddContactArgs, context: IRequiredContext): Promise - cmUpdateContact(args: IUpdateContactArgs, context: IRequiredContext): Promise - cmRemoveContact(args: IRemoveContactArgs, context: IRequiredContext): Promise - cmGetIdentity(args: IGetIdentityArgs, context: IRequiredContext): Promise - cmGetIdentities(args: IGetIdentitiesArgs, context: IRequiredContext): Promise> - cmAddIdentity(args: IAddIdentityArgs, context: IRequiredContext): Promise - cmUpdateIdentity(args: IUpdateIdentityArgs, context: IRequiredContext): Promise - cmRemoveIdentity(args: IRemoveIdentityArgs, context: IRequiredContext): Promise -} - -export interface IGetContactArgs { + cmGetContact(args: GetContactArgs, context: RequiredContext): Promise + cmGetContacts(args?: GetContactsArgs): Promise> + cmAddContact(args: AddContactArgs, context: RequiredContext): Promise + cmUpdateContact(args: UpdateContactArgs, context: RequiredContext): Promise + cmRemoveContact(args: RemoveContactArgs, context: RequiredContext): Promise + cmGetIdentity(args: GetIdentityArgs, context: RequiredContext): Promise + cmGetIdentities(args?: GetIdentitiesArgs): Promise> + cmAddIdentity(args: AddIdentityArgs, context: RequiredContext): Promise + cmUpdateIdentity(args: UpdateIdentityArgs, context: RequiredContext): Promise + cmRemoveIdentity(args: RemoveIdentityArgs, context: RequiredContext): Promise + cmGetRelationship(args: GetRelationshipArgs, context: RequiredContext): Promise + cmGetRelationships(args?: GetRelationshipsArgs): Promise> + cmUpdateRelationship(args: UpdateRelationshipArgs, context: RequiredContext): Promise + cmAddRelationship(args: AddRelationshipArgs, context: RequiredContext): Promise + cmRemoveRelationship(args: RemoveRelationshipArgs, context: RequiredContext): Promise + cmGetContactType(args: GetContactTypeArgs, context: RequiredContext): Promise + cmGetContactTypes(args?: GetContactTypesArgs): Promise> + cmAddContactType(args: AddContactTypeArgs, context: RequiredContext): Promise + cmUpdateContactType(args: UpdateContactTypeArgs, context: RequiredContext): Promise + cmRemoveContactType(args: RemoveContactTypeArgs, context: RequiredContext): Promise +} + +export type GetContactArgs = { contactId: string } -export interface IGetContactsArgs { +export type GetContactsArgs = { filter?: FindContactArgs } -export interface IAddContactArgs { - name: string - alias: string - uri?: string - identities?: Array -} +// export type AddContactArgs = { +// uri?: string +// contactType: NonPersistedContactType +// identities?: Array +// } & NonPersistedNaturalPerson | NonPersistedOrganization -export interface IUpdateContactArgs { - contact: IContact +export type AddContactArgs = Omit & + NonPersistedContact & { + contactType: NonPersistedContactType + } + +export type UpdateContactArgs = { + contact: Contact } -export interface IRemoveContactArgs { +export type RemoveContactArgs = { contactId: string } -export interface IGetIdentityArgs { +export type GetIdentityArgs = { identityId: string } -export interface IGetIdentitiesArgs { +export type GetIdentitiesArgs = { filter?: FindIdentityArgs } -export interface IAddIdentityArgs { +export type AddIdentityArgs = { contactId: string - identity: IBasicIdentity + identity: NonPersistedIdentity } -export interface IUpdateIdentityArgs { - identity: IIdentity +export type UpdateIdentityArgs = { + identity: Identity } -export interface IRemoveIdentityArgs { +export type RemoveIdentityArgs = { identityId: string } -export type IRequiredContext = IAgentContext +export type AddRelationshipArgs = { + leftId: string + rightId: string +} + +export type RemoveRelationshipArgs = { + relationshipId: string +} + +export type GetRelationshipArgs = { + relationshipId: string +} + +export type GetRelationshipsArgs = { + filter: FindRelationshipArgs +} + +export type UpdateRelationshipArgs = { + relationship: Omit +} + +export type AddContactTypeArgs = { + type: ContactTypeEnum + name: string + tenantId: string + description?: string +} + +export type GetContactTypeArgs = { + contactTypeId: string +} + +export type GetContactTypesArgs = { + filter?: FindContactTypeArgs +} + +export type UpdateContactTypeArgs = { + contactType: Omit +} + +export type RemoveContactTypeArgs = { + contactTypeId: string +} + +export type RequiredContext = IAgentContext diff --git a/packages/data-store/package.json b/packages/data-store/package.json index f85d70a58..35d22f0f2 100644 --- a/packages/data-store/package.json +++ b/packages/data-store/package.json @@ -20,6 +20,7 @@ "typeorm": "^0.3.12" }, "devDependencies": { + "pg": "^8.11.3", "sqlite3": "^5.1.6" }, "files": [ diff --git a/packages/data-store/src/__tests__/contact.entities.test.ts b/packages/data-store/src/__tests__/contact.entities.test.ts index d2a69185f..e79eb1910 100644 --- a/packages/data-store/src/__tests__/contact.entities.test.ts +++ b/packages/data-store/src/__tests__/contact.entities.test.ts @@ -1,24 +1,45 @@ -import { DataSource } from 'typeorm' - +import { DataSource, FindOptionsWhere } from 'typeorm' + +import { DataStoreContactEntities, DataStoreMigrations } from '../index' +import { NaturalPersonEntity } from '../entities/contact/NaturalPersonEntity' +import { OrganizationEntity } from '../entities/contact/OrganizationEntity' +import { PartyRelationshipEntity } from '../entities/contact/PartyRelationshipEntity' +import { PartyTypeEntity } from '../entities/contact/PartyTypeEntity' +import { PartyEntity } from '../entities/contact/PartyEntity' +import { IdentityEntity } from '../entities/contact/IdentityEntity' +import { OpenIdConfigEntity } from '../entities/contact/OpenIdConfigEntity' +import { DidAuthConfigEntity } from '../entities/contact/DidAuthConfigEntity' +import { ConnectionEntity } from '../entities/contact/ConnectionEntity' +import { CorrelationIdentifierEntity } from '../entities/contact/CorrelationIdentifierEntity' +import { IdentityMetadataItemEntity } from '../entities/contact/IdentityMetadataItemEntity' +import { BaseContactEntity } from '../entities/contact/BaseContactEntity' import { + NonPersistedParty, + PartyTypeEnum, + NaturalPerson, + Organization, + IdentityRoleEnum, CorrelationIdentifierEnum, - DataStoreContactEntities, - DataStoreMigrations, - contactEntityFrom, + ConnectionTypeEnum, + NonPersistedPartyType, + NonPersistedOrganization, + NonPersistedNaturalPerson, + NonPersistedConnection, + NonPersistedIdentity, + NonPersistedDidAuthConfig, + NonPersistedOpenIdConfig, +} from '../types' +import { connectionEntityFrom, - identityEntityFrom, didAuthConfigEntityFrom, + identityEntityFrom, + naturalPersonEntityFrom, openIdConfigEntityFrom, - ContactEntity, - IdentityEntity, - ConnectionTypeEnum, - OpenIdConfigEntity, - DidAuthConfigEntity, - ConnectionEntity, - CorrelationIdentifierEntity, - IdentityMetadataItemEntity, - IdentityRoleEnum, -} from '../index' + organizationEntityFrom, + partyEntityFrom, + partyRelationshipEntityFrom, + partyTypeEntityFrom, +} from '../utils/contact/MappingUtils' describe('Database entities tests', (): void => { let dbConnection: DataSource @@ -27,7 +48,7 @@ describe('Database entities tests', (): void => { dbConnection = await new DataSource({ type: 'sqlite', database: ':memory:', - //logging: 'all', + logging: 'all', migrationsRun: false, migrations: DataStoreMigrations, synchronize: false, @@ -41,98 +62,333 @@ describe('Database entities tests', (): void => { await (await dbConnection).destroy() }) - it('Should save contact to database', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('Should save person party to database', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) - const fromDb = await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity.id }, }) expect(fromDb).toBeDefined() expect(fromDb?.identities?.length).toEqual(0) - expect(fromDb?.name).toEqual(contact.name) - expect(fromDb?.alias).toEqual(contact.alias) - expect(fromDb?.uri).toEqual(contact.uri) + expect(fromDb?.uri).toEqual(party.uri) + expect(fromDb?.partyType).toBeDefined() + expect(fromDb?.partyType.type).toEqual(party.partyType.type) + expect(fromDb?.partyType.tenantId).toEqual(party.partyType.tenantId) + expect(fromDb?.partyType.name).toEqual(party.partyType.name) + expect(fromDb?.contact).toBeDefined() + expect((fromDb?.contact).firstName).toEqual((party.contact).firstName) + expect((fromDb?.contact).middleName).toEqual((party.contact).middleName) + expect((fromDb?.contact).lastName).toEqual((party.contact).lastName) + expect((fromDb?.contact).displayName).toEqual((party.contact).displayName) }) - it('should throw error when saving contact with blank name', async (): Promise => { - const contact = { - name: '', - alias: 'test_alias', + it('Should save organization party to database', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.ORGANIZATION, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + legalName: 'example_legal_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) + const partyEntity: PartyEntity = partyEntityFrom(party) + await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity.id }, + }) - await expect(dbConnection.getRepository(ContactEntity).save(contactEntity)).rejects.toThrow('Blank names are not allowed') + expect(fromDb).toBeDefined() + expect(fromDb?.identities?.length).toEqual(0) + expect(fromDb?.uri).toEqual(party.uri) + expect(fromDb?.partyType).toBeDefined() + expect(fromDb?.partyType.type).toEqual(party.partyType.type) + expect(fromDb?.partyType.tenantId).toEqual(party.partyType.tenantId) + expect(fromDb?.partyType.name).toEqual(party.partyType.name) + expect(fromDb?.contact).toBeDefined() + expect((fromDb?.contact).legalName).toEqual((party.contact).legalName) + expect((fromDb?.contact).displayName).toEqual((party.contact).displayName) }) - it('should throw error when saving contact with blank alias', async (): Promise => { - const contact = { - name: 'test_name', - alias: '', + it('Should result in party relationship for the owner side only', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) + + const fromDb1: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty1.id }, + }) + + const fromDb2: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty2.id }, + }) + + expect(fromDb1).toBeDefined() + expect(fromDb1?.relationships.length).toEqual(1) + expect(fromDb2).toBeDefined() + expect(fromDb2?.relationships.length).toEqual(0) + }) + + it('should throw error when saving person party with blank first name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: '', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) + const partyEntity: PartyEntity = partyEntityFrom(party) - await expect(dbConnection.getRepository(ContactEntity).save(contactEntity)).rejects.toThrow('Blank aliases are not allowed') + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank first names are not allowed') }) - it('Should enforce unique name for a contact', async (): Promise => { - const contactName = 'non_unique_name' - const contact1 = { - name: contactName, - alias: 'unique_alias1', + it('should throw error when saving person party with blank middle name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: '', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contact1Entity: ContactEntity = contactEntityFrom(contact1) - await dbConnection.getRepository(ContactEntity).save(contact1Entity) - const contact2 = { - name: contactName, - alias: 'unique_alias2', + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank middle names are not allowed') + }) + + it('should throw error when saving person party with blank last name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: '', + displayName: 'example_display_name', + }, } - const contact2Entity: ContactEntity = contactEntityFrom(contact2) - await expect(dbConnection.getRepository(ContactEntity).save(contact2Entity)).rejects.toThrowError( - 'SQLITE_CONSTRAINT: UNIQUE constraint failed: Contact.name' - ) + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank last names are not allowed') }) - it('Should enforce unique alias for a contact', async (): Promise => { - const alias = 'non_unique_alias' - const contact1 = { - name: 'unique_name1', - alias, + it('should throw error when saving person party with blank display name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: '', + }, } - const contact1Entity: ContactEntity = contactEntityFrom(contact1) - await dbConnection.getRepository(ContactEntity).save(contact1Entity) - const contact2 = { - name: 'unique_name2', - alias, + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank display names are not allowed') + }) + + it('should throw error when saving organization party with blank legal name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.ORGANIZATION, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + legalName: '', + displayName: 'example_legal_name', + }, } - const contact2Entity: ContactEntity = contactEntityFrom(contact2) - await expect(dbConnection.getRepository(ContactEntity).save(contact2Entity)).rejects.toThrowError( - 'SQLITE_CONSTRAINT: UNIQUE constraint failed: Contact.alias' - ) + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank legal names are not allowed') + }) + + it('should throw error when saving organization party with blank display name', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.ORGANIZATION, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + legalName: 'example_first_name', + displayName: '', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank display names are not allowed') + }) + + it('should throw error when saving party with blank party type name', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: '', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank names are not allowed') + }) + + it('should throw error when saving party with blank party type description', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + description: '', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError('Blank descriptions are not allowed') + }) + + it('should throw error when saving party with blank party type tenant id', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + + await expect(dbConnection.getRepository(PartyEntity).save(partyEntity)).rejects.toThrowError("Blank tenant id's are not allowed") }) it('Should enforce unique alias for an identity', async (): Promise => { const alias = 'non_unique_alias' - const identity1 = { + const identity1: NonPersistedIdentity = { alias, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -143,7 +399,7 @@ describe('Database entities tests', (): void => { const identity1Entity: IdentityEntity = identityEntityFrom(identity1) await dbConnection.getRepository(IdentityEntity).save(identity1Entity) - const identity2 = { + const identity2: NonPersistedIdentity = { alias: alias, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -159,7 +415,7 @@ describe('Database entities tests', (): void => { it('Should enforce unique correlationId for a identity', async (): Promise => { const correlationId = 'non_unique_correlationId' - const identity1 = { + const identity1: NonPersistedIdentity = { alias: 'unique_alias1', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -170,7 +426,7 @@ describe('Database entities tests', (): void => { const identity1Entity: IdentityEntity = identityEntityFrom(identity1) await dbConnection.getRepository(IdentityEntity).save(identity1Entity) - const identity2 = { + const identity2: NonPersistedIdentity = { alias: 'unique_alias2', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -186,7 +442,7 @@ describe('Database entities tests', (): void => { it('Should save identity to database', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -199,7 +455,7 @@ describe('Database entities tests', (): void => { await dbConnection.getRepository(IdentityEntity).save(identityEntity) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId, @@ -215,7 +471,7 @@ describe('Database entities tests', (): void => { }) it('should throw error when saving identity with blank alias', async (): Promise => { - const identity = { + const identity: NonPersistedIdentity = { alias: '', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -226,11 +482,11 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) - await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrow('Blank aliases are not allowed') + await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrowError('Blank aliases are not allowed') }) it('should throw error when saving identity with blank correlation id', async (): Promise => { - const identity = { + const identity: NonPersistedIdentity = { alias: 'example_did', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -241,12 +497,12 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) - await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrow('Blank correlation ids are not allowed') + await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrowError('Blank correlation ids are not allowed') }) it('should throw error when saving identity with blank metadata label', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -263,12 +519,12 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) - await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrow('Blank metadata labels are not allowed') + await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrowError('Blank metadata labels are not allowed') }) it('should throw error when saving identity with blank metadata value', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -285,12 +541,12 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) - await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrow('Blank metadata values are not allowed') + await expect(dbConnection.getRepository(IdentityEntity).save(identityEntity)).rejects.toThrowError('Blank metadata values are not allowed') }) it('Should save identity with openid connection to database', async (): Promise => { const correlationId = 'example.com' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -306,7 +562,7 @@ describe('Database entities tests', (): void => { issuer: 'https://example.com/app-test', redirectUrl: 'app:/callback', dangerouslyAllowInsecureHttpRequests: true, - clientAuthMethod: 'post' as const, + clientAuthMethod: 'post', }, }, } @@ -315,7 +571,7 @@ describe('Database entities tests', (): void => { await dbConnection.getRepository(IdentityEntity).save(identityEntity) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId, @@ -328,14 +584,14 @@ describe('Database entities tests', (): void => { expect(fromDb?.identifier).toBeDefined() expect(fromDb?.identifier.correlationId).toEqual(identity.identifier.correlationId) expect(fromDb?.identifier.type).toEqual(identity.identifier.type) - expect(fromDb?.connection?.type).toEqual(identity.connection.type) + expect(fromDb?.connection?.type).toEqual(identity.connection?.type) expect(fromDb?.connection?.config).toBeDefined() - expect((fromDb?.connection?.config as OpenIdConfigEntity).clientId).toEqual(identity.connection.config.clientId) + expect((fromDb?.connection?.config).clientId).toEqual((identity.connection?.config).clientId) }) it('Should save identity with didauth connection to database', async (): Promise => { const correlationId = 'example.com' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -362,7 +618,7 @@ describe('Database entities tests', (): void => { await dbConnection.getRepository(IdentityEntity).save(identityEntity) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId, @@ -375,13 +631,15 @@ describe('Database entities tests', (): void => { expect(fromDb?.identifier).toBeDefined() expect(fromDb?.identifier.correlationId).toEqual(identity.identifier.correlationId) expect(fromDb?.identifier.type).toEqual(identity.identifier.type) - expect(fromDb?.connection?.type).toEqual(identity.connection.type) + expect(fromDb?.connection?.type).toEqual(identity.connection?.type) expect(fromDb?.connection?.config).toBeDefined() - expect((fromDb?.connection?.config as DidAuthConfigEntity).identifier).toEqual(identity.connection.config.identifier.did) + expect((fromDb?.connection?.config).identifier).toEqual( + (identity.connection?.config).identifier.did + ) }) it('Should save connection with openid config to database', async (): Promise => { - const connection = { + const connection: NonPersistedConnection = { type: ConnectionTypeEnum.OPENID_CONNECT, config: { clientId: '138d7bf8-c930-4c6e-b928-97d3a4928b01', @@ -390,32 +648,32 @@ describe('Database entities tests', (): void => { issuer: 'https://example.com/app-test', redirectUrl: 'app:/callback', dangerouslyAllowInsecureHttpRequests: true, - clientAuthMethod: 'post' as const, + clientAuthMethod: 'post', }, } - const connectionEntity = connectionEntityFrom(connection) + const connectionEntity: ConnectionEntity = connectionEntityFrom(connection) await dbConnection.getRepository(ConnectionEntity).save(connectionEntity, { transaction: true, }) - const fromDb = await dbConnection.getRepository(ConnectionEntity).findOne({ + const fromDb: ConnectionEntity | null = await dbConnection.getRepository(ConnectionEntity).findOne({ where: { type: connection.type }, }) expect(fromDb).toBeDefined() - const fromDbConfig = await dbConnection.getRepository(OpenIdConfigEntity).findOne({ + const fromDbConfig: OpenIdConfigEntity | null = await dbConnection.getRepository(OpenIdConfigEntity).findOne({ where: { id: fromDb?.id }, }) expect(fromDbConfig).toBeDefined() expect(fromDb?.type).toEqual(connection.type) expect(fromDb?.config).toBeDefined() - expect((fromDb?.config as OpenIdConfigEntity).clientId).toEqual(connection.config.clientId) + expect((fromDb?.config).clientId).toEqual((connection.config).clientId) }) it('Should save connection with didauth config to database', async (): Promise => { - const connection = { + const connection: NonPersistedConnection = { type: ConnectionTypeEnum.SIOPv2, config: { identifier: { @@ -429,55 +687,55 @@ describe('Database entities tests', (): void => { sessionId: 'https://example.com/did:test:138d7bf8-c930-4c6e-b928-97d3a4928b01', }, } - const connectionEntity = connectionEntityFrom(connection) + const connectionEntity: ConnectionEntity = connectionEntityFrom(connection) await dbConnection.getRepository(ConnectionEntity).save(connectionEntity, { transaction: true, }) - const fromDb = await dbConnection.getRepository(ConnectionEntity).findOne({ + const fromDb: ConnectionEntity | null = await dbConnection.getRepository(ConnectionEntity).findOne({ where: { type: connection.type }, }) expect(fromDb).toBeDefined() - const fromDbConfig = await dbConnection.getRepository(DidAuthConfigEntity).findOne({ + const fromDbConfig: DidAuthConfigEntity | null = await dbConnection.getRepository(DidAuthConfigEntity).findOne({ where: { id: fromDb?.id }, }) expect(fromDbConfig).toBeDefined() expect(fromDb?.type).toEqual(connection.type) expect(fromDb?.config).toBeDefined() - expect((fromDb?.config as DidAuthConfigEntity).identifier).toEqual(connection.config.identifier.did) + expect((fromDb?.config).identifier).toEqual((connection.config).identifier.did) }) it('Should save openid config to database', async (): Promise => { const clientId = '138d7bf8-c930-4c6e-b928-97d3a4928b01' - const config = { + const config: NonPersistedOpenIdConfig = { clientId, clientSecret: '03b3955f-d020-4f2a-8a27-4e452d4e27a0', scopes: ['auth'], issuer: 'https://example.com/app-test', redirectUrl: 'app:/callback', dangerouslyAllowInsecureHttpRequests: true, - clientAuthMethod: 'post' as const, + clientAuthMethod: 'post', } - const configEntity = openIdConfigEntityFrom(config) + const configEntity: OpenIdConfigEntity = openIdConfigEntityFrom(config) await dbConnection.getRepository(OpenIdConfigEntity).save(configEntity, { transaction: true, }) - const fromDb = await dbConnection.getRepository(OpenIdConfigEntity).findOne({ + const fromDb: OpenIdConfigEntity | null = await dbConnection.getRepository(OpenIdConfigEntity).findOne({ where: { clientId: config.clientId }, }) expect(fromDb).toBeDefined() - expect((fromDb as OpenIdConfigEntity).clientId).toEqual(config.clientId) + expect((fromDb).clientId).toEqual(config.clientId) }) it('Should save didauth config to database', async (): Promise => { const sessionId = 'https://example.com/did:test:138d7bf8-c930-4c6e-b928-97d3a4928b01' - const config = { + const config: NonPersistedDidAuthConfig = { identifier: { did: 'did:test:138d7bf8-c930-4c6e-b928-97d3a4928b01', provider: 'test_provider', @@ -489,31 +747,62 @@ describe('Database entities tests', (): void => { sessionId, } - const configEntity = didAuthConfigEntityFrom(config) + const configEntity: DidAuthConfigEntity = didAuthConfigEntityFrom(config) await dbConnection.getRepository(DidAuthConfigEntity).save(configEntity, { transaction: true, }) - const fromDb = await dbConnection.getRepository(DidAuthConfigEntity).findOne({ + const fromDb: DidAuthConfigEntity | null = await dbConnection.getRepository(DidAuthConfigEntity).findOne({ where: { sessionId: config.sessionId }, }) expect(fromDb).toBeDefined() - expect((fromDb as DidAuthConfigEntity).identifier).toEqual(config.identifier.did) + expect((fromDb).identifier).toEqual(config.identifier.did) }) - it('Should delete contact and all child relations', async (): Promise => { - const contact = { - name: 'relation_test_name', - alias: 'relation_test_alias', + it('Should delete party and all child relations', async (): Promise => { + const party1: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity1) + + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - const savedContact = await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity2) + + expect(savedParty2).toBeDefined() const correlationId = 'relation_example.com' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -529,7 +818,7 @@ describe('Database entities tests', (): void => { issuer: 'https://example.com/app-test', redirectUrl: 'app:/callback', dangerouslyAllowInsecureHttpRequests: true, - clientAuthMethod: 'post' as const, + clientAuthMethod: 'post', }, }, metadata: [ @@ -541,29 +830,42 @@ describe('Database entities tests', (): void => { } const identityEntity: IdentityEntity = identityEntityFrom(identity) - identityEntity.contact = savedContact + identityEntity.party = savedParty1 + + const savedIdentity: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + + expect(savedIdentity).toBeDefined() + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + const savedRelationship: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) - const savedIdentity = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + expect(savedRelationship).toBeDefined() expect( - await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty1.id }, }) ).toBeDefined() - await dbConnection.getRepository(ContactEntity).delete({ id: savedContact.id }) + await dbConnection.getRepository(PartyEntity).delete({ id: savedParty1.id }) - // check contact + // check party await expect( - await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty1.id }, }) ).toBeNull() // check identity expect( await dbConnection.getRepository(IdentityEntity).findOne({ - where: { alias: correlationId }, + where: { id: savedParty1.id }, }) ).toBeNull() @@ -594,20 +896,52 @@ describe('Database entities tests', (): void => { where: { id: savedIdentity.metadata![0].id }, }) ).toBeNull() + + // check contact + expect( + await dbConnection.getRepository(BaseContactEntity).findOne({ + where: { id: savedParty1.contact.id }, + }) + ).toBeNull() + + // check party type + expect( + await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { id: savedParty1.partyType.id }, + }) + ).toBeDefined() + + // check relation + expect( + await dbConnection.getRepository(PartyRelationshipEntity).findOne({ + where: { id: savedRelationship.id }, + }) + ).toBeNull() }) it('Should delete identity and all child relations', async (): Promise => { - const contact = { - name: 'relation_test_name', - alias: 'relation_test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - const savedContact = await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) + + expect(savedParty).toBeDefined() const correlationId = 'relation_example.com' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -637,13 +971,13 @@ describe('Database entities tests', (): void => { } const identityEntity: IdentityEntity = identityEntityFrom(identity) - identityEntity.contact = savedContact + identityEntity.party = savedParty - const savedIdentity = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + const savedIdentity: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).save(identityEntity) expect( - await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, }) ).toBeDefined() @@ -685,18 +1019,29 @@ describe('Database entities tests', (): void => { ).toBeNull() }) - it('Should not delete contact when deleting identity', async (): Promise => { - const contact = { - name: 'relation_test_name', - alias: 'relation_test_alias', + it('Should not delete party when deleting identity', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - const savedContact = await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) + + expect(savedParty).toBeDefined() const correlationId = 'relation_example.com' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -726,9 +1071,11 @@ describe('Database entities tests', (): void => { } const identityEntity: IdentityEntity = identityEntityFrom(identity) - identityEntity.contact = savedContact + identityEntity.party = savedParty - const savedIdentity = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + const savedIdentity: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + + expect(savedIdentity).toBeDefined() await dbConnection.getRepository(IdentityEntity).delete({ id: savedIdentity.id }) @@ -739,55 +1086,83 @@ describe('Database entities tests', (): void => { }) ).toBeNull() - // check contact + // check party expect( - await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, }) ).toBeDefined() }) - it('Should set creation date when saving contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('Should set creation date when saving party', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) - const fromDb = await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, }) expect(fromDb).toBeDefined() expect(fromDb?.createdAt).toBeDefined() }) - it('Should not update creation date when updating contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('Should not update creation date when updating party', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - const savedContact = await dbConnection.getRepository(ContactEntity).save(contactEntity) - const newContactName = 'new_name' - await dbConnection.getRepository(ContactEntity).save({ ...savedContact, name: newContactName }) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) + + expect(savedParty).toBeDefined() + + const newContactFirstName = 'new_first_name' + await dbConnection.getRepository(PartyEntity).save({ + ...savedParty, + contact: { + ...savedParty.contact, + firstName: newContactFirstName, + }, + }) - const fromDb = await dbConnection.getRepository(ContactEntity).findOne({ - where: { id: savedContact.id }, + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, }) expect(fromDb).toBeDefined() - expect(fromDb?.createdAt).toEqual(savedContact?.createdAt) + expect((fromDb?.contact).firstName).toEqual(newContactFirstName) + expect(fromDb?.createdAt).toEqual(savedParty?.createdAt) }) it('Should set creation date when saving identity', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -799,7 +1174,7 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) await dbConnection.getRepository(IdentityEntity).save(identityEntity) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId, @@ -813,7 +1188,7 @@ describe('Database entities tests', (): void => { it('Should not update creation date when saving identity', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -823,13 +1198,13 @@ describe('Database entities tests', (): void => { } const identityEntity: IdentityEntity = identityEntityFrom(identity) - const savedIdentity = await dbConnection.getRepository(IdentityEntity).save(identityEntity) + const savedIdentity: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).save(identityEntity) const newCorrelationId = 'new_example_did' await dbConnection .getRepository(IdentityEntity) .save({ ...savedIdentity, identifier: { ...savedIdentity.identifier, correlationId: newCorrelationId } }) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId: newCorrelationId, @@ -841,51 +1216,116 @@ describe('Database entities tests', (): void => { expect(fromDb?.createdAt).toEqual(savedIdentity?.createdAt) }) - it('Should set last updated date when saving contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('Should set last updated date when saving party', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) - const fromDb = await dbConnection.getRepository(ContactEntity).findOne({ - where: { name: contact.name }, + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, }) expect(fromDb).toBeDefined() expect(fromDb?.lastUpdatedAt).toBeDefined() }) - it('Should update last updated date when updating contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('Should update last updated date when updating party', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const contactEntity: ContactEntity = contactEntityFrom(contact) - const savedContact = await dbConnection.getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity) + expect(savedParty).toBeDefined() // waiting here to get a different timestamp await new Promise((resolve) => setTimeout(resolve, 2000)) - const newContactName = 'new_name' - await dbConnection.getRepository(ContactEntity).save({ ...savedContact, name: newContactName }) + const newContactFirstName = 'new_first_name' + await dbConnection.getRepository(PartyEntity).save({ + ...savedParty, + // FIXME there is still an issue when updating nested objects, the parent does not update + // https://github.com/typeorm/typeorm/issues/5378 + uri: 'new uri', // TODO remove this to trigger the bug + contact: { + ...savedParty.contact, + firstName: newContactFirstName, + }, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: savedParty.id }, + }) + + expect(fromDb).toBeDefined() + expect((fromDb?.contact).firstName).toEqual(newContactFirstName) + expect(fromDb?.lastUpdatedAt).not.toEqual(savedParty?.lastUpdatedAt) + }) + + it('Should set last updated date when saving party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) + + const fromDb: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { id: savedPartyType.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.lastUpdatedAt).toBeDefined() + }) + + it('Should set last creation date when saving party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) - const fromDb = await dbConnection.getRepository(ContactEntity).findOne({ - where: { id: savedContact.id }, + const fromDb: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { id: savedPartyType.id }, }) expect(fromDb).toBeDefined() - expect(fromDb?.lastUpdatedAt).not.toEqual(savedContact?.lastUpdatedAt) + expect(fromDb?.createdAt).toBeDefined() }) it('Should set last updated date when saving identity', async (): Promise => { const correlationId = 'example_did' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -897,7 +1337,7 @@ describe('Database entities tests', (): void => { const identityEntity: IdentityEntity = identityEntityFrom(identity) await dbConnection.getRepository(IdentityEntity).save(identityEntity) - const fromDb = await dbConnection.getRepository(IdentityEntity).findOne({ + const fromDb: IdentityEntity | null = await dbConnection.getRepository(IdentityEntity).findOne({ where: { identifier: { correlationId, @@ -908,4 +1348,826 @@ describe('Database entities tests', (): void => { expect(fromDb).toBeDefined() expect(fromDb?.lastUpdatedAt).toBeDefined() }) + + it('Should enforce unique type and tenant id combination when saving party type', async (): Promise => { + const tenantId = 'non_unique_value' + const name = 'non_unique_value' + const partyType1: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId, + name, + } + + const partyTypeEntity1: PartyTypeEntity = partyTypeEntityFrom(partyType1) + const savedPartyType1: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity1) + + expect(savedPartyType1).toBeDefined() + + const partyType2: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId, + name, + } + + const partyTypeEntity2: PartyTypeEntity = partyTypeEntityFrom(partyType2) + await expect(dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity2)).rejects.toThrowError( + 'SQLITE_CONSTRAINT: UNIQUE constraint failed: PartyType.type, PartyType.tenant_id' + ) + }) + + it('Should enforce unique name when saving party type', async (): Promise => { + const name = 'non_unique_value' + const partyType1: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name, + } + + const partyTypeEntity1: PartyTypeEntity = partyTypeEntityFrom(partyType1) + const savedPartyType1: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity1) + + expect(savedPartyType1).toBeDefined() + + const partyType2: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name, + } + + const partyTypeEntity2: PartyTypeEntity = partyTypeEntityFrom(partyType2) + await expect(dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity2)).rejects.toThrowError( + 'SQLITE_CONSTRAINT: UNIQUE constraint failed: PartyType.name' + ) + }) + + it('Should enforce unique legal name when saving organization', async (): Promise => { + const legalName = 'non_unique_value' + const organization1: NonPersistedOrganization = { + legalName, + displayName: 'example_display_name', + } + + const organizationEntity1: OrganizationEntity = organizationEntityFrom(organization1) + const savedOrganization1: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).save(organizationEntity1, { + transaction: true, + }) + + expect(savedOrganization1).toBeDefined() + + const organization2: NonPersistedOrganization = { + legalName, + displayName: 'example_display_name', + } + + const organizationEntity2: OrganizationEntity = organizationEntityFrom(organization2) + await expect(dbConnection.getRepository(OrganizationEntity).save(organizationEntity2)).rejects.toThrowError( + 'SQLITE_CONSTRAINT: UNIQUE constraint failed: BaseContact.legal_name' + ) + }) + + it('Should enforce unique legal name when saving organization', async (): Promise => { + const legalName = 'example_legal_name' + const organization1: NonPersistedOrganization = { + legalName, + displayName: 'example_display_name', + } + + const organizationEntity1: OrganizationEntity = organizationEntityFrom(organization1) + const savedOrganization1: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).save(organizationEntity1, { + transaction: true, + }) + + expect(savedOrganization1).toBeDefined() + + const organization2: NonPersistedOrganization = { + legalName, + displayName: 'example_display_name', + } + + const organizationEntity2: OrganizationEntity = organizationEntityFrom(organization2) + await expect(dbConnection.getRepository(OrganizationEntity).save(organizationEntity2)).rejects.toThrowError( + 'SQLITE_CONSTRAINT: UNIQUE constraint failed: BaseContact.legal_name' + ) + }) + + it('Should save party relationship to database', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + expect(savedParty2).toBeDefined() + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) + + // TODO check the relation field + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity1.id }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should set last updated date when saving party relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity1.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.lastUpdatedAt).toBeDefined() + }) + + it('Should set creation date when saving party relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity1.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.createdAt).toBeDefined() + }) + + it('Should save bidirectional party relationships to database', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + expect(savedParty2).toBeDefined() + + const relationship1: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + const savedRelationship1: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).save(relationship1, { + transaction: true, + }) + + expect(savedRelationship1).toBeDefined() + + const relationship2: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty2.id, + rightId: savedParty1.id, + }) + + const savedRelationship2: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).save(relationship2, { + transaction: true, + }) + + expect(savedRelationship2).toBeDefined() + + const fromDb: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).findOne({ + where: { id: savedRelationship2.id }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should enforce unique owner combination for party relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + expect(savedParty2).toBeDefined() + + const relationship1: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + const savedRelationship1: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).save(relationship1, { + transaction: true, + }) + + expect(savedRelationship1).toBeDefined() + + const relationship2: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + await expect(dbConnection.getRepository(PartyRelationshipEntity).save(relationship2)).rejects.toThrowError( + 'SQLITE_CONSTRAINT: UNIQUE constraint failed: PartyRelationship.left_id, PartyRelationship.right_id' + ) + }) + + it('Should save party type to database', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) + + const fromDb: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { id: savedPartyType.id }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should save person to database', async (): Promise => { + const person: NonPersistedNaturalPerson = { + firstName: 'example_first_name', + lastName: 'lastName2', + displayName: 'displayName', + } + + const personEntity: NaturalPersonEntity = naturalPersonEntityFrom(person) + const savedPerson: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).save(personEntity, { + transaction: true, + }) + + const fromDb: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).findOne({ + where: { id: savedPerson.id }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should set last updated date when saving person', async (): Promise => { + const person: NonPersistedNaturalPerson = { + firstName: 'example_first_name', + lastName: 'lastName2', + displayName: 'displayName', + } + + const personEntity: NaturalPersonEntity = naturalPersonEntityFrom(person) + const savedPerson: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).save(personEntity, { + transaction: true, + }) + + const fromDb: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).findOne({ + where: { id: savedPerson.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.lastUpdatedAt).toBeDefined() + }) + + it('Should set creation date when saving person', async (): Promise => { + const person: NonPersistedNaturalPerson = { + firstName: 'example_first_name', + lastName: 'lastName2', + displayName: 'displayName', + } + + const personEntity: NaturalPersonEntity = naturalPersonEntityFrom(person) + const savedPerson: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).save(personEntity, { + transaction: true, + }) + + const fromDb: NaturalPersonEntity | null = await dbConnection.getRepository(NaturalPersonEntity).findOne({ + where: { id: savedPerson.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.createdAt).toBeDefined() + }) + + it('Should save organization to database', async (): Promise => { + const organization: NonPersistedOrganization = { + legalName: 'example_legal_name', + displayName: 'example_display_name', + } + + const organizationEntity: OrganizationEntity = organizationEntityFrom(organization) + const savedOrganization: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).save(organizationEntity, { + transaction: true, + }) + + const fromDb: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).findOne({ + where: { id: savedOrganization.id }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should set last updated date when saving organization', async (): Promise => { + const organization: NonPersistedOrganization = { + legalName: 'example_legal_name', + displayName: 'example_display_name', + } + + const organizationEntity: OrganizationEntity = organizationEntityFrom(organization) + const savedOrganization: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).save(organizationEntity, { + transaction: true, + }) + + const fromDb: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).findOne({ + where: { id: savedOrganization.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.lastUpdatedAt).toBeDefined() + }) + + it('Should set creation date when saving organization', async (): Promise => { + const organization: NonPersistedOrganization = { + legalName: 'example_legal_name', + displayName: 'example_display_name', + } + + const organizationEntity: OrganizationEntity = organizationEntityFrom(organization) + const savedOrganization: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).save(organizationEntity, { + transaction: true, + }) + + const fromDb: OrganizationEntity | null = await dbConnection.getRepository(OrganizationEntity).findOne({ + where: { id: savedOrganization.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.createdAt).toBeDefined() + }) + + it('Should get party based on person information', async (): Promise => { + const firstName = 'example_first_name' + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName, + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { + contact: { + firstName, + } as FindOptionsWhere, //NaturalPersonEntity | OrganizationEntity + }, + }) + + expect(fromDb).toBeDefined() + }) + + it('Should get party based on organization information', async (): Promise => { + const legalName = 'example_legal_name' + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.ORGANIZATION, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + legalName, + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { + contact: { + legalName, + } as FindOptionsWhere, //NaturalPersonEntity | OrganizationEntity + }, + }) + + expect(fromDb).toBeDefined() + }) + + it("Should enforce unique party id's for relationship sides", async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + expect(savedParty).toBeDefined() + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty.id, + rightId: savedParty.id, + }) + + await expect(dbConnection.getRepository(PartyRelationshipEntity).save(relationship)).rejects.toThrowError( + 'Cannot use the same id for both sides of the relationship' + ) + }) + + it('Should delete party relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + + const partyEntity1: PartyEntity = partyEntityFrom(party1) + const savedParty1: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity1, { + transaction: true, + }) + + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + + const partyEntity2: PartyEntity = partyEntityFrom(party2) + const savedParty2: PartyEntity = await dbConnection.getRepository(PartyEntity).save(partyEntity2, { + transaction: true, + }) + + expect(savedParty2).toBeDefined() + + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId: savedParty1.id, + rightId: savedParty2.id, + }) + + const savedRelationship: PartyRelationshipEntity | null = await dbConnection.getRepository(PartyRelationshipEntity).save(relationship, { + transaction: true, + }) + + expect(savedRelationship).toBeDefined() + + await dbConnection.getRepository(PartyRelationshipEntity).delete({ id: savedRelationship.id }) + + await expect( + await dbConnection.getRepository(PartyRelationshipEntity).findOne({ + where: { id: savedRelationship.id }, + }) + ).toBeNull() + }) + + it('Should delete party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) + + expect(savedPartyType).toBeDefined() + + await dbConnection.getRepository(PartyTypeEntity).delete({ id: savedPartyType.id }) + + await expect( + await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { id: savedPartyType.id }, + }) + ).toBeNull() + }) + + it('Should not be able to remove party type when used by parties', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + const savedParty: PartyEntity | null = await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + expect(savedParty).toBeDefined() + + await expect(dbConnection.getRepository(PartyTypeEntity).delete({ id: savedParty.partyType.id })).rejects.toThrowError( + 'FOREIGN KEY constraint failed' + ) + }) + + it('Should save party with existing party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) + + const party: NonPersistedParty = { + uri: 'example.com', + partyType: savedPartyType, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const partyEntity: PartyEntity = partyEntityFrom(party) + partyEntity.partyType = savedPartyType + await dbConnection.getRepository(PartyEntity).save(partyEntity, { + transaction: true, + }) + + const fromDb: PartyEntity | null = await dbConnection.getRepository(PartyEntity).findOne({ + where: { id: partyEntity.id }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.partyType).toBeDefined() + expect(fromDb?.partyType.id).toEqual(savedPartyType.id) + expect(fromDb?.partyType.type).toEqual(savedPartyType.type) + expect(fromDb?.partyType.tenantId).toEqual(savedPartyType.tenantId) + expect(fromDb?.partyType.name).toEqual(savedPartyType.name) + }) + + it('Should not update creation date when saving party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + + const partyTypeEntity: PartyTypeEntity = partyTypeEntityFrom(partyType) + const savedPartyType: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).save(partyTypeEntity) + await dbConnection.getRepository(PartyTypeEntity).save({ ...savedPartyType, type: PartyTypeEnum.ORGANIZATION }) + + const fromDb: PartyTypeEntity | null = await dbConnection.getRepository(PartyTypeEntity).findOne({ + where: { + type: PartyTypeEnum.ORGANIZATION, + }, + }) + + expect(fromDb).toBeDefined() + expect(fromDb?.createdAt).toEqual(savedPartyType?.createdAt) + }) }) diff --git a/packages/data-store/src/__tests__/contact.store.test.ts b/packages/data-store/src/__tests__/contact.store.test.ts index c6afc3d4d..2ade64899 100644 --- a/packages/data-store/src/__tests__/contact.store.test.ts +++ b/packages/data-store/src/__tests__/contact.store.test.ts @@ -1,7 +1,24 @@ import { DataSource } from 'typeorm' - +import { DataStoreMigrations, DataStoreContactEntities } from '../index' import { ContactStore } from '../contact/ContactStore' -import { CorrelationIdentifierEnum, DataStoreContactEntities, DataStoreMigrations, IdentityRoleEnum } from '../index' +import { + IdentityRoleEnum, + PartyTypeEnum, + NonPersistedParty, + Party, + CorrelationIdentifierEnum, + Identity, + NaturalPerson, + NonPersistedNaturalPerson, + NonPersistedIdentity, + GetIdentitiesArgs, + GetPartiesArgs, + NonPersistedPartyRelationship, + PartyRelationship, + PartyType, + GetRelationshipsArgs, + NonPersistedPartyType, +} from '../types' describe('Contact store tests', (): void => { let dbConnection: DataSource @@ -11,7 +28,7 @@ describe('Contact store tests', (): void => { dbConnection = await new DataSource({ type: 'sqlite', database: ':memory:', - //logging: 'all', + logging: 'all', migrationsRun: false, migrations: DataStoreMigrations, synchronize: false, @@ -26,70 +43,138 @@ describe('Contact store tests', (): void => { await (await dbConnection).destroy() }) - it('should get contact by id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get party by id', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() - const result = await contactStore.getContact({ contactId: savedContact.id }) + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() + + const result: Party = await contactStore.getParty({ partyId: savedParty.id }) expect(result).toBeDefined() }) - it('should throw error when getting contact with unknown id', async (): Promise => { - const contactId = 'unknownContactId' + it('should throw error when getting party with unknown id', async (): Promise => { + const partyId = 'unknownPartyId' - await expect(contactStore.getContact({ contactId })).rejects.toThrow(`No contact found for id: ${contactId}`) + await expect(contactStore.getParty({ partyId })).rejects.toThrow(`No party found for id: ${partyId}`) }) - it('should get all contacts', async (): Promise => { - const contact1 = { - name: 'test_name1', - alias: 'test_alias1', - uri: 'example.com1', + it('should get all parties', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, } - const savedContact1 = await contactStore.addContact(contact1) - expect(savedContact1).toBeDefined() + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() - const contact2 = { - name: 'test_name2', - alias: 'test_alias2', - uri: 'example.com2', + const party2: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, } - const savedContact2 = await contactStore.addContact(contact2) - expect(savedContact2).toBeDefined() + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() - const result = await contactStore.getContacts() + const result: Array = await contactStore.getParties() + expect(result).toBeDefined() expect(result.length).toEqual(2) }) - it('should get contacts by filter', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get parties by filter', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const args = { - filter: [{ name: 'test_name' }, { alias: 'test_alias' }, { uri: 'example.com' }], + const args: GetPartiesArgs = { + filter: [ + { + contact: { + firstName: (party.contact).firstName, + }, + }, + { + contact: { + middleName: (party.contact).middleName, + }, + }, + { + contact: { + lastName: (party.contact).lastName, + }, + }, + { + contact: { + displayName: (party.contact).displayName, + }, + }, + ], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result.length).toEqual(1) }) - it('should get whole contacts by filter', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get whole parties by filter', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, identities: [ { alias: 'test_alias1', @@ -116,11 +201,17 @@ describe('Contact store tests', (): void => { }, }, ], + electronicAddresses: [ + { + type: 'email', + electronicAddress: 'sphereon@sphereon.com', + }, + ], } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const args = { + const args: GetPartiesArgs = { filter: [ { identities: { @@ -131,90 +222,178 @@ describe('Contact store tests', (): void => { }, ], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result[0].identities.length).toEqual(3) + expect(result[0].electronicAddresses.length).toEqual(1) }) - it('should get contacts by name', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get parties by name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'something', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() - const args = { - filter: [{ name: 'test_name' }], + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() + + const args: GetPartiesArgs = { + filter: [ + { + contact: { + firstName: (party.contact).firstName, + }, + }, + { + contact: { + middleName: (party.contact).middleName, + }, + }, + { + contact: { + lastName: (party.contact).lastName, + }, + }, + ], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result.length).toEqual(1) }) - it('should get contacts by alias', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get parties by display name', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() - const args = { - filter: [{ alias: 'test_alias' }], + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() + + const args: GetPartiesArgs = { + filter: [ + { + contact: { + displayName: (party.contact).displayName, + }, + }, + ], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result.length).toEqual(1) }) - it('should get contacts by uri', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get parties by uri', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const args = { + const args: GetPartiesArgs = { filter: [{ uri: 'example.com' }], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result.length).toEqual(1) }) - it('should return no contacts if filter does not match', async (): Promise => { - const args = { - filter: [{ name: 'no_match_contact' }, { alias: 'no_match_contact_alias' }, { uri: 'no_match_example.com' }], + it('should return no parties if filter does not match', async (): Promise => { + const args: GetPartiesArgs = { + filter: [ + { + contact: { + firstName: 'no_match_firstName', + }, + }, + { + contact: { + middleName: 'no_match_middleName', + }, + }, + { + contact: { + lastName: 'no_match_lastName', + }, + }, + ], } - const result = await contactStore.getContacts(args) + const result: Array = await contactStore.getParties(args) expect(result.length).toEqual(0) }) - it('should add contact without identities', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should add party without identities', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const result = await contactStore.addContact(contact) + const result: Party = await contactStore.addParty(party) - expect(result.name).toEqual(contact.name) - expect(result.alias).toEqual(contact.alias) - expect(result.uri).toEqual(contact.uri) + expect(result).toBeDefined() + expect((result.contact).firstName).toEqual((party.contact).firstName) + expect((result.contact).middleName).toEqual((party.contact).middleName) + expect((result.contact).lastName).toEqual((party.contact).lastName) + expect(result.identities.length).toEqual(0) }) - it('should add contact with identities', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should add party with identities', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, identities: [ { alias: 'test_alias1', @@ -235,25 +414,35 @@ describe('Contact store tests', (): void => { ], } - const result = await contactStore.addContact(contact) + const result: Party = await contactStore.addParty(party) - expect(result.name).toEqual(contact.name) - expect(result.alias).toEqual(contact.alias) - expect(result.uri).toEqual(contact.uri) + expect(result).toBeDefined() + expect((result.contact).firstName).toEqual((party.contact).firstName) + expect((result.contact).middleName).toEqual((party.contact).middleName) + expect((result.contact).lastName).toEqual((party.contact).lastName) expect(result.identities.length).toEqual(2) }) - it('should throw error when adding contact with invalid identity', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should throw error when adding party with invalid identity', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'something', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, identities: [ { alias: 'test_alias1', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { - type: CorrelationIdentifierEnum.DID, + type: CorrelationIdentifierEnum.URL, correlationId: 'example_did1', }, }, @@ -268,64 +457,28 @@ describe('Contact store tests', (): void => { ], } - const result = await contactStore.addContact(contact) - - expect(result.name).toEqual(contact.name) - expect(result.alias).toEqual(contact.alias) - expect(result.uri).toEqual(contact.uri) - expect(result.identities.length).toEqual(2) - }) - - it('should throw error when adding contact with duplicate name', async (): Promise => { - const name = 'test_name' - const contact1 = { - name, - alias: 'test_alias', - uri: 'example.com', - } - const savedContact1 = await contactStore.addContact(contact1) - expect(savedContact1).toBeDefined() - - const alias = 'test_alias2' - const contact2 = { - name, - alias, - uri: 'example.com', - } - - await expect(contactStore.addContact(contact2)).rejects.toThrow(`Duplicate names or aliases are not allowed. Name: ${name}, Alias: ${alias}`) + await expect(contactStore.addParty(party)).rejects.toThrow(`Identity with correlation type url should contain a connection`) }) - it('should throw error when adding contact with duplicate alias', async (): Promise => { - const alias = 'test_alias' - const contact1 = { - name: 'test_name', - alias, - uri: 'example.com', - } - const savedContact1 = await contactStore.addContact(contact1) - expect(savedContact1).toBeDefined() - - const name = 'test_name2' - const contact2 = { - name, - alias, - uri: 'example.com', - } - - await expect(contactStore.addContact(contact2)).rejects.toThrow(`Duplicate names or aliases are not allowed. Name: ${name}, Alias: ${alias}`) - }) - - it('should update contact by id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should update party by id', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity1 = { + const identity1: NonPersistedIdentity = { alias: 'test_alias1', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -333,10 +486,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did1', }, } - const savedIdentity1 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity1 }) + const savedIdentity1: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity1 }) expect(savedIdentity1).toBeDefined() - const identity2 = { + const identity2: NonPersistedIdentity = { alias: 'test_alias2', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -344,50 +497,77 @@ describe('Contact store tests', (): void => { correlationId: 'example_did2', }, } - const savedIdentity2 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity2 }) + const savedIdentity2: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity2 }) expect(savedIdentity2).toBeDefined() - const contactName = 'updated_name' - const updatedContact = { - ...savedContact, - name: contactName, + const contactFirstName = 'updated_first_name' + const updatedParty: Party = { + ...savedParty, + contact: { + ...savedParty.contact, + firstName: contactFirstName, + }, } - await contactStore.updateContact({ contact: updatedContact }) - const result = await contactStore.getContact({ contactId: savedContact.id }) + await contactStore.updateParty({ party: updatedParty }) + const result: Party = await contactStore.getParty({ partyId: savedParty.id }) - expect(result.name).toEqual(contactName) + expect(result).toBeDefined() + expect((result.contact).firstName).toEqual(contactFirstName) expect(result.identities.length).toEqual(2) }) - it('should throw error when updating contact with unknown id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should throw error when updating party with unknown id', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() - - const contactId = 'unknownContactId' - const updatedContact = { - ...savedContact, - id: contactId, - name: 'new_name', + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() + + const partyId = 'unknownPartyId' + const contactFirstName = 'updated_first_name' + const updatedParty: Party = { + ...savedParty, + id: partyId, + contact: { + ...savedParty.contact, + firstName: contactFirstName, + }, } - await expect(contactStore.updateContact({ contact: updatedContact })).rejects.toThrow(`No contact found for id: ${contactId}`) + + await expect(contactStore.updateParty({ party: updatedParty })).rejects.toThrow(`No party found for id: ${partyId}`) }) it('should get identity by id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity = { + const identity: NonPersistedIdentity = { alias: 'test_alias', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -395,24 +575,33 @@ describe('Contact store tests', (): void => { correlationId: 'example_did', }, } - const savedIdentity = await contactStore.addIdentity({ contactId: savedContact.id, identity }) + const savedIdentity: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity }) expect(savedIdentity).toBeDefined() - const result = await contactStore.getIdentity({ identityId: savedIdentity.id }) + const result: Identity = await contactStore.getIdentity({ identityId: savedIdentity.id }) expect(result).toBeDefined() }) it('should get holderDID identity by id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity = { + const identity: NonPersistedIdentity = { alias: 'test_alias', roles: [IdentityRoleEnum.HOLDER], identifier: { @@ -420,10 +609,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did', }, } - const savedIdentity = await contactStore.addIdentity({ contactId: savedContact.id, identity }) + const savedIdentity: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity }) expect(savedIdentity).toBeDefined() - const result = await contactStore.getIdentity({ identityId: savedIdentity.id }) + const result: Identity = await contactStore.getIdentity({ identityId: savedIdentity.id }) expect(result).toBeDefined() }) @@ -435,15 +624,24 @@ describe('Contact store tests', (): void => { }) it('should get all identities for contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity1 = { + const identity1: NonPersistedIdentity = { alias: 'test_alias1', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -451,10 +649,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did1', }, } - const savedIdentity1 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity1 }) + const savedIdentity1: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity1 }) expect(savedIdentity1).toBeDefined() - const identity2 = { + const identity2: NonPersistedIdentity = { alias: 'test_alias2', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -462,28 +660,37 @@ describe('Contact store tests', (): void => { correlationId: 'example_did2', }, } - const savedIdentity2 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity2 }) + const savedIdentity2: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity2 }) expect(savedIdentity2).toBeDefined() - const args = { - filter: [{ contactId: savedContact.id }], + const args: GetIdentitiesArgs = { + filter: [{ partyId: savedParty.id }], } - const result = await contactStore.getIdentities(args) + const result: Array = await contactStore.getIdentities(args) expect(result.length).toEqual(2) }) it('should get all identities', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity1 = { + const identity1: NonPersistedIdentity = { alias: 'test_alias1', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -491,10 +698,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did1', }, } - const savedIdentity1 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity1 }) + const savedIdentity1: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity1 }) expect(savedIdentity1).toBeDefined() - const identity2 = { + const identity2: NonPersistedIdentity = { alias: 'test_alias2', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -502,25 +709,34 @@ describe('Contact store tests', (): void => { correlationId: 'example_did2', }, } - const savedIdentity2 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity2 }) + const savedIdentity2: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity2 }) expect(savedIdentity2).toBeDefined() - const result = await contactStore.getIdentities() + const result: Array = await contactStore.getIdentities() expect(result.length).toEqual(2) }) it('should get identities by filter', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() const alias = 'test_alias1' - const identity1 = { + const identity1: NonPersistedIdentity = { alias, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -528,10 +744,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did1', }, } - const savedIdentity1 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity1 }) + const savedIdentity1: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity1 }) expect(savedIdentity1).toBeDefined() - const identity2 = { + const identity2: NonPersistedIdentity = { alias: 'test_alias2', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -539,29 +755,38 @@ describe('Contact store tests', (): void => { correlationId: 'example_did2', }, } - const savedIdentity2 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity2 }) + const savedIdentity2: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity2 }) expect(savedIdentity2).toBeDefined() - const args = { + const args: GetIdentitiesArgs = { filter: [{ alias }], } - const result = await contactStore.getIdentities(args) + const result: Array = await contactStore.getIdentities(args) expect(result.length).toEqual(1) }) it('should get whole identities by filter', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() const alias = 'test_alias1' - const identity1 = { + const identity1: NonPersistedIdentity = { alias, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -579,29 +804,38 @@ describe('Contact store tests', (): void => { }, ], } - const savedIdentity1 = await contactStore.addIdentity({ contactId: savedContact.id, identity: identity1 }) + const savedIdentity1: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity: identity1 }) expect(savedIdentity1).toBeDefined() - const args = { + const args: GetIdentitiesArgs = { filter: [{ metadata: { label: 'label1' } }], } - const result = await contactStore.getIdentities(args) + const result: Array = await contactStore.getIdentities(args) expect(result[0]).toBeDefined() expect(result[0].metadata!.length).toEqual(2) }) it('should add identity to contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity = { + const identity: NonPersistedIdentity = { alias: 'test_alias', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -609,10 +843,10 @@ describe('Contact store tests', (): void => { correlationId: 'example_did', }, } - const savedIdentity = await contactStore.addIdentity({ contactId: savedContact.id, identity }) + const savedIdentity: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity }) expect(savedIdentity).toBeDefined() - const result = await contactStore.getContact({ contactId: savedContact.id }) + const result: Party = await contactStore.getParty({ partyId: savedParty.id }) expect(result.identities.length).toEqual(1) }) @@ -623,16 +857,25 @@ describe('Contact store tests', (): void => { }) it('should throw error when adding identity with invalid identifier', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() const correlationId = 'missing_connection_example' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -641,22 +884,31 @@ describe('Contact store tests', (): void => { }, } - await expect(contactStore.addIdentity({ contactId: savedContact.id, identity })).rejects.toThrow( + await expect(contactStore.addIdentity({ partyId: savedParty.id, identity })).rejects.toThrow( `Identity with correlation type url should contain a connection` ) }) it('should throw error when updating identity with invalid identifier', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() const correlationId = 'missing_connection_example' - const identity = { + const identity: NonPersistedIdentity = { alias: correlationId, roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER, IdentityRoleEnum.HOLDER], identifier: { @@ -664,7 +916,7 @@ describe('Contact store tests', (): void => { correlationId, }, } - const storedIdentity = await contactStore.addIdentity({ contactId: savedContact.id, identity }) + const storedIdentity: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity }) storedIdentity.identifier = { ...storedIdentity.identifier, type: CorrelationIdentifierEnum.URL } await expect(contactStore.updateIdentity({ identity: storedIdentity })).rejects.toThrow( @@ -673,15 +925,24 @@ describe('Contact store tests', (): void => { }) it('should update identity by id', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, } - const savedContact = await contactStore.addContact(contact) - expect(savedContact).toBeDefined() + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() - const identity = { + const identity: NonPersistedIdentity = { alias: 'example_did', roles: [IdentityRoleEnum.ISSUER, IdentityRoleEnum.VERIFIER], identifier: { @@ -689,22 +950,31 @@ describe('Contact store tests', (): void => { correlationId: 'example_did', }, } - const storedIdentity = await contactStore.addIdentity({ contactId: savedContact.id, identity }) + const storedIdentity: Identity = await contactStore.addIdentity({ partyId: savedParty.id, identity }) const correlationId = 'new_update_example_did' storedIdentity.identifier = { ...storedIdentity.identifier, correlationId } await contactStore.updateIdentity({ identity: storedIdentity }) - const result = await contactStore.getIdentity({ identityId: storedIdentity.id }) + const result: Identity = await contactStore.getIdentity({ identityId: storedIdentity.id }) - expect(result).not.toBeNull() + expect(result).toBeDefined() expect(result.identifier.correlationId).toEqual(correlationId) }) - it('should get aggregate of identity roles on contact', async (): Promise => { - const contact = { - name: 'test_name', - alias: 'test_alias', + it('should get aggregate of identity roles on party', async (): Promise => { + const party: NonPersistedParty = { uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, identities: [ { alias: 'test_alias1', @@ -733,11 +1003,777 @@ describe('Contact store tests', (): void => { ], } - const savedContact = await contactStore.addContact(contact) - const result = await contactStore.getContact({ contactId: savedContact.id }) + const savedParty: Party = await contactStore.addParty(party) + const result: Party = await contactStore.getParty({ partyId: savedParty.id }) expect(result.roles).toBeDefined() expect(result.roles.length).toEqual(3) expect(result.roles).toEqual([IdentityRoleEnum.VERIFIER, IdentityRoleEnum.ISSUER, IdentityRoleEnum.HOLDER]) }) + + it('should add relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + await contactStore.addRelationship(relationship) + + const result: Party = await contactStore.getParty({ partyId: savedParty1.id }) + + expect(result).toBeDefined() + expect(result.relationships.length).toEqual(1) + expect(result.relationships[0].leftId).toEqual(savedParty1.id) + expect(result.relationships[0].rightId).toEqual(savedParty2.id) + }) + + it('should get relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + + const result: PartyRelationship = await contactStore.getRelationship({ relationshipId: savedRelationship.id }) + + expect(result).toBeDefined() + expect(result.leftId).toEqual(savedParty1.id) + expect(result.rightId).toEqual(savedParty2.id) + }) + + it('should throw error when getting relationship with unknown id', async (): Promise => { + const relationshipId = 'unknownRelationshipId' + + await expect(contactStore.getRelationship({ relationshipId })).rejects.toThrow(`No relationship found for id: ${relationshipId}`) + }) + + it('should get all relationships', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship1: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + await contactStore.addRelationship(relationship1) + + const relationship2: NonPersistedPartyRelationship = { + leftId: savedParty2.id, + rightId: savedParty1.id, + } + await contactStore.addRelationship(relationship2) + + const result: Array = await contactStore.getRelationships() + + expect(result).toBeDefined() + expect(result.length).toEqual(2) + }) + + it('should get relationships by filter', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship1: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + await contactStore.addRelationship(relationship1) + + const relationship2: NonPersistedPartyRelationship = { + leftId: savedParty2.id, + rightId: savedParty1.id, + } + await contactStore.addRelationship(relationship2) + + const args: GetRelationshipsArgs = { + filter: [ + { + leftId: savedParty1.id, + rightId: savedParty2.id, + }, + ], + } + + const result: Array = await contactStore.getRelationships(args) + + expect(result).toBeDefined() + expect(result.length).toEqual(1) + }) + + it('should remove relationship', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + expect(savedRelationship).toBeDefined() + + await contactStore.removeRelationship({ relationshipId: savedRelationship.id }) + + const result: Party = await contactStore.getParty({ partyId: savedParty1.id }) + + expect(result).toBeDefined() + expect(result?.relationships?.length).toEqual(0) + }) + + it('should throw error when removing relationship with unknown id', async (): Promise => { + const relationshipId = 'unknownRelationshipId' + + await expect(contactStore.removeRelationship({ relationshipId })).rejects.toThrow(`No relationship found for id: ${relationshipId}`) + }) + + it('should return no relationships if filter does not match', async (): Promise => { + const args: GetRelationshipsArgs = { + filter: [ + { + leftId: 'unknown_id', + }, + ], + } + const result: Array = await contactStore.getRelationships(args) + + expect(result.length).toEqual(0) + }) + + it('should update relationship by id', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const party3: NonPersistedParty = { + uri: 'example3.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d287', + name: 'example_name3', + }, + contact: { + firstName: 'example_first_name3', + middleName: 'example_middle_name3', + lastName: 'example_last_name3', + displayName: 'example_display_name3', + }, + } + const savedParty3: Party = await contactStore.addParty(party3) + expect(savedParty3).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + + const updatedRelationship: PartyRelationship = { + ...savedRelationship, + rightId: savedParty3.id, + } + + await contactStore.updateRelationship({ relationship: updatedRelationship }) + + const result: Party = await contactStore.getParty({ partyId: savedParty1.id }) + + expect(result).toBeDefined() + expect(result.relationships.length).toEqual(1) + expect(result.relationships[0].leftId).toEqual(savedParty1.id) + expect(result.relationships[0].rightId).toEqual(savedParty3.id) + }) + + it('should throw error when updating relationship with unknown id', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + + const relationshipId = 'unknownRelationshipId' + const updatedRelationship: PartyRelationship = { + ...savedRelationship, + id: relationshipId, + rightId: savedParty2.id, + } + + await expect(contactStore.updateRelationship({ relationship: updatedRelationship })).rejects.toThrow( + `No party relationship found for id: ${relationshipId}` + ) + }) + + it('should throw error when updating relationship with unknown right side id', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + + const partyId = 'unknownPartyId' + const updatedRelationship: PartyRelationship = { + ...savedRelationship, + rightId: partyId, + } + + await expect(contactStore.updateRelationship({ relationship: updatedRelationship })).rejects.toThrow( + `No party found for right side of the relationship, party id: ${partyId}` + ) + }) + + it('should throw error when updating relationship with unknown left side id', async (): Promise => { + const party1: NonPersistedParty = { + uri: 'example1.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name1', + }, + contact: { + firstName: 'example_first_name1', + middleName: 'example_middle_name1', + lastName: 'example_last_name1', + displayName: 'example_display_name1', + }, + } + const savedParty1: Party = await contactStore.addParty(party1) + expect(savedParty1).toBeDefined() + + const party2: NonPersistedParty = { + uri: 'example2.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name2', + }, + contact: { + firstName: 'example_first_name2', + middleName: 'example_middle_name2', + lastName: 'example_last_name2', + displayName: 'example_display_name2', + }, + } + const savedParty2: Party = await contactStore.addParty(party2) + expect(savedParty2).toBeDefined() + + const relationship: NonPersistedPartyRelationship = { + leftId: savedParty1.id, + rightId: savedParty2.id, + } + const savedRelationship: PartyRelationship = await contactStore.addRelationship(relationship) + + const partyId = 'unknownPartyId' + const updatedRelationship: PartyRelationship = { + ...savedRelationship, + leftId: partyId, + } + + await expect(contactStore.updateRelationship({ relationship: updatedRelationship })).rejects.toThrow( + `No party found for left side of the relationship, party id: ${partyId}` + ) + }) + + it('should add party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name', + description: 'example_description', + } + + const savedPartyType: PartyType = await contactStore.addPartyType(partyType) + const result: PartyType = await contactStore.getPartyType({ partyTypeId: savedPartyType.id }) + + expect(result).toBeDefined() + expect(result.name).toEqual(partyType.name) + expect(result.type).toEqual(partyType.type) + expect(result.tenantId).toEqual(partyType.tenantId) + expect(result.description).toEqual(partyType.description) + expect(result.lastUpdatedAt).toBeDefined() + expect(result.createdAt).toBeDefined() + }) + + it('should get party types by filter', async (): Promise => { + const partyType1: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name1', + description: 'example_description1', + } + const savedPartyType1: PartyType = await contactStore.addPartyType(partyType1) + expect(savedPartyType1).toBeDefined() + + const partyType2: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d287', + name: 'example_name2', + description: 'example_description2', + } + const savedPartyType2: PartyType = await contactStore.addPartyType(partyType2) + expect(savedPartyType2).toBeDefined() + + const result: Array = await contactStore.getPartyTypes({ + filter: [ + { + type: PartyTypeEnum.NATURAL_PERSON, + name: 'example_name1', + description: 'example_description1', + }, + ], + }) + + expect(result).toBeDefined() + expect(result.length).toEqual(1) + }) + + it('should return no party types if filter does not match', async (): Promise => { + const partyType1: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name1', + description: 'example_description1', + } + const savedPartyType1: PartyType = await contactStore.addPartyType(partyType1) + expect(savedPartyType1).toBeDefined() + + const partyType2: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d287', + name: 'example_name2', + description: 'example_description2', + } + const savedPartyType2: PartyType = await contactStore.addPartyType(partyType2) + expect(savedPartyType2).toBeDefined() + + const result: Array = await contactStore.getPartyTypes({ + filter: [ + { + type: PartyTypeEnum.NATURAL_PERSON, + name: 'unknown_name', + description: 'unknown_description', + }, + ], + }) + + expect(result).toBeDefined() + expect(result.length).toEqual(0) + }) + + it('should throw error when updating party type with unknown id', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name', + description: 'example_description', + } + const savedPartyType: PartyType = await contactStore.addPartyType(partyType) + expect(savedPartyType).toBeDefined() + + const partyTypeId = 'unknownPartyTypeId' + const updatedPartyType: PartyType = { + ...savedPartyType, + id: partyTypeId, + description: 'new_example_description', + } + + await expect(contactStore.updatePartyType({ partyType: updatedPartyType })).rejects.toThrow(`No party type found for id: ${partyTypeId}`) + }) + + it('should update party type by id', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name', + description: 'example_description', + } + const savedPartyType: PartyType = await contactStore.addPartyType(partyType) + expect(savedPartyType).toBeDefined() + + const newDescription = 'new_example_description' + const updatedPartyType: PartyType = { + ...savedPartyType, + description: newDescription, + } + + const result: PartyType = await contactStore.updatePartyType({ partyType: updatedPartyType }) + + expect(result).toBeDefined() + expect(result.description).toEqual(newDescription) + }) + + it('should throw error when removing party type with unknown id', async (): Promise => { + const partyTypeId = 'unknownPartyTypeId' + + await expect(contactStore.removePartyType({ partyTypeId })).rejects.toThrow(`No party type found for id: ${partyTypeId}`) + }) + + it('should remove party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d288', + name: 'example_name', + description: 'example_description', + } + const savedPartyType: PartyType = await contactStore.addPartyType(partyType) + expect(savedPartyType).toBeDefined() + + const resultPartyType: PartyType = await contactStore.getPartyType({ partyTypeId: savedPartyType.id }) + expect(resultPartyType).toBeDefined() + + const includingMigrationPartyTypes: Array = await contactStore.getPartyTypes() + expect(includingMigrationPartyTypes.length).toEqual(2) + + await contactStore.removePartyType({ partyTypeId: savedPartyType.id }) + + const result: Array = await contactStore.getPartyTypes() + + expect(result).toBeDefined() + expect(result.length).toEqual(1) + }) + + it('should throw error when removing party type attached to contact', async (): Promise => { + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + const savedParty: Party = await contactStore.addParty(party) + expect(savedParty).toBeDefined() + + await expect(contactStore.removePartyType({ partyTypeId: savedParty.partyType.id })).rejects.toThrow( + `Unable to remove party type with id: ${savedParty.partyType.id}. Party type is in use` + ) + }) + + it('Should save party with existing party type', async (): Promise => { + const partyType: NonPersistedPartyType = { + type: PartyTypeEnum.NATURAL_PERSON, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + } + const savedPartyType: PartyType = await contactStore.addPartyType(partyType) + expect(savedPartyType).toBeDefined() + + const party: NonPersistedParty = { + uri: 'example.com', + partyType: savedPartyType, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + const result: Party = await contactStore.addParty(party) + + expect(result).toBeDefined() + expect(result?.partyType).toBeDefined() + expect(result?.partyType.id).toEqual(savedPartyType.id) + expect(result?.partyType.type).toEqual(savedPartyType.type) + expect(result?.partyType.tenantId).toEqual(savedPartyType.tenantId) + expect(result?.partyType.name).toEqual(savedPartyType.name) + }) + + it('should throw error when adding person party with wrong contact type', async (): Promise => { + const partyType = PartyTypeEnum.ORGANIZATION + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: partyType, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + firstName: 'example_first_name', + middleName: 'example_middle_name', + lastName: 'example_last_name', + displayName: 'example_display_name', + }, + } + + await expect(contactStore.addParty(party)).rejects.toThrow(`Party type ${partyType}, does not match for provided contact`) + }) + + it('should throw error when adding organization party with wrong contact type', async (): Promise => { + const partyType = PartyTypeEnum.NATURAL_PERSON + const party: NonPersistedParty = { + uri: 'example.com', + partyType: { + type: partyType, + tenantId: '0605761c-4113-4ce5-a6b2-9cbae2f9d289', + name: 'example_name', + }, + contact: { + legalName: 'example_legal_name', + displayName: 'example_display_name', + }, + } + + await expect(contactStore.addParty(party)).rejects.toThrow(`Party type ${partyType}, does not match for provided contact`) + }) }) diff --git a/packages/data-store/src/contact/AbstractContactStore.ts b/packages/data-store/src/contact/AbstractContactStore.ts index de8a3c5ff..09d04afc3 100644 --- a/packages/data-store/src/contact/AbstractContactStore.ts +++ b/packages/data-store/src/contact/AbstractContactStore.ts @@ -1,27 +1,49 @@ import { - IContact, - IIdentity, - IAddIdentityArgs, - IGetIdentityArgs, - IGetIdentitiesArgs, - IGetContactArgs, - IRemoveIdentityArgs, - IUpdateIdentityArgs, - IAddContactArgs, - IGetContactsArgs, - IRemoveContactArgs, - IUpdateContactArgs, + Party, + Identity, + AddIdentityArgs, + GetIdentityArgs, + GetIdentitiesArgs, + GetPartyArgs, + RemoveIdentityArgs, + UpdateIdentityArgs, + AddPartyArgs, + GetPartiesArgs, + RemovePartyArgs, + UpdatePartyArgs, + AddRelationshipArgs, + PartyRelationship, + RemoveRelationshipArgs, + AddPartyTypeArgs, + PartyType, + GetPartyTypeArgs, + UpdatePartyTypeArgs, + GetPartyTypesArgs, + RemovePartyTypeArgs, + GetRelationshipsArgs, + GetRelationshipArgs, + UpdateRelationshipArgs, } from '../types' export abstract class AbstractContactStore { - abstract getContact(args: IGetContactArgs): Promise - abstract getContacts(args?: IGetContactsArgs): Promise> - abstract addContact(args: IAddContactArgs): Promise - abstract updateContact(args: IUpdateContactArgs): Promise - abstract removeContact(args: IRemoveContactArgs): Promise - abstract getIdentity(args: IGetIdentityArgs): Promise - abstract getIdentities(args: IGetIdentitiesArgs): Promise> - abstract addIdentity(args: IAddIdentityArgs): Promise - abstract updateIdentity(args: IUpdateIdentityArgs): Promise - abstract removeIdentity(args: IRemoveIdentityArgs): Promise + abstract getParty(args: GetPartyArgs): Promise + abstract getParties(args?: GetPartiesArgs): Promise> + abstract addParty(args: AddPartyArgs): Promise + abstract updateParty(args: UpdatePartyArgs): Promise + abstract removeParty(args: RemovePartyArgs): Promise + abstract getIdentity(args: GetIdentityArgs): Promise + abstract getIdentities(args?: GetIdentitiesArgs): Promise> + abstract addIdentity(args: AddIdentityArgs): Promise + abstract updateIdentity(args: UpdateIdentityArgs): Promise + abstract removeIdentity(args: RemoveIdentityArgs): Promise + abstract getRelationship(args: GetRelationshipArgs): Promise + abstract getRelationships(args?: GetRelationshipsArgs): Promise> + abstract addRelationship(args: AddRelationshipArgs): Promise + abstract updateRelationship(args: UpdateRelationshipArgs): Promise + abstract removeRelationship(args: RemoveRelationshipArgs): Promise + abstract getPartyType(args: GetPartyTypeArgs): Promise + abstract getPartyTypes(args?: GetPartyTypesArgs): Promise> + abstract addPartyType(args: AddPartyTypeArgs): Promise + abstract updatePartyType(args: UpdatePartyTypeArgs): Promise + abstract removePartyType(args: RemovePartyTypeArgs): Promise } diff --git a/packages/data-store/src/contact/ContactStore.ts b/packages/data-store/src/contact/ContactStore.ts index 55d61855e..8b777c1c3 100644 --- a/packages/data-store/src/contact/ContactStore.ts +++ b/packages/data-store/src/contact/ContactStore.ts @@ -1,40 +1,62 @@ -import Debug from 'debug' import { OrPromise } from '@sphereon/ssi-types' +import { DataSource, In, Repository } from 'typeorm' +import Debug from 'debug' import { AbstractContactStore } from './AbstractContactStore' -import { - IAddIdentityArgs, - IGetIdentityArgs, - IGetIdentitiesArgs, - IGetContactArgs, - IRemoveIdentityArgs, - IUpdateIdentityArgs, - IGetContactsArgs, - IAddContactArgs, - IUpdateContactArgs, - IRemoveContactArgs, - BasicConnectionConfig, - ConnectionConfig, - ConnectionTypeEnum, - CorrelationIdentifierEnum, - IConnection, - IContact, - ICorrelationIdentifier, - IDidAuthConfig, - IIdentity, - IMetadataItem, - IOpenIdConfig, -} from '../types' -import { ContactEntity, contactEntityFrom } from '../entities/contact/ContactEntity' -import { IdentityEntity, identityEntityFrom } from '../entities/contact/IdentityEntity' +import { PartyEntity } from '../entities/contact/PartyEntity' +import { IdentityEntity } from '../entities/contact/IdentityEntity' import { IdentityMetadataItemEntity } from '../entities/contact/IdentityMetadataItemEntity' import { CorrelationIdentifierEntity } from '../entities/contact/CorrelationIdentifierEntity' import { ConnectionEntity } from '../entities/contact/ConnectionEntity' import { BaseConfigEntity } from '../entities/contact/BaseConfigEntity' -import { OpenIdConfigEntity } from '../entities/contact/OpenIdConfigEntity' -import { DidAuthConfigEntity } from '../entities/contact/DidAuthConfigEntity' -import { DataSource, In } from 'typeorm' +import { PartyRelationshipEntity } from '../entities/contact/PartyRelationshipEntity' +import { PartyTypeEntity } from '../entities/contact/PartyTypeEntity' +import { + identityEntityFrom, + identityFrom, + isDidAuthConfig, + isNaturalPerson, + isOpenIdConfig, + isOrganization, + partyEntityFrom, + partyFrom, + partyRelationshipEntityFrom, + partyRelationshipFrom, + partyTypeEntityFrom, + partyTypeFrom, +} from '../utils/contact/MappingUtils' +import { + AddIdentityArgs, + GetIdentityArgs, + GetIdentitiesArgs, + GetPartyArgs, + RemoveIdentityArgs, + UpdateIdentityArgs, + GetPartiesArgs, + AddPartyArgs, + UpdatePartyArgs, + RemovePartyArgs, + NonPersistedConnectionConfig, + ConnectionTypeEnum, + CorrelationIdentifierEnum, + Party, + Identity, + RemoveRelationshipArgs, + PartyRelationship, + AddRelationshipArgs, + GetRelationshipArgs, + AddPartyTypeArgs, + PartyType, + GetPartyTypeArgs, + GetPartyTypesArgs, + UpdatePartyTypeArgs, + RemovePartyTypeArgs, + GetRelationshipsArgs, + UpdateRelationshipArgs, + PartyTypeEnum, + NonPersistedContact, +} from '../types' -const debug = Debug('sphereon:ssi-sdk:contact-store') +const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:contact-store') export class ContactStore extends AbstractContactStore { private readonly dbConnection: OrPromise @@ -44,41 +66,40 @@ export class ContactStore extends AbstractContactStore { this.dbConnection = dbConnection } - getContact = async ({ contactId }: IGetContactArgs): Promise => { - const result = await (await this.dbConnection).getRepository(ContactEntity).findOne({ - where: { id: contactId }, + getParty = async ({ partyId }: GetPartyArgs): Promise => { + const result: PartyEntity | null = await (await this.dbConnection).getRepository(PartyEntity).findOne({ + where: { id: partyId }, }) if (!result) { - return Promise.reject(Error(`No contact found for id: ${contactId}`)) + return Promise.reject(Error(`No party found for id: ${partyId}`)) } - return this.contactFrom(result) + return partyFrom(result) } - getContacts = async (args?: IGetContactsArgs): Promise> => { - const initialResult = await (await this.dbConnection).getRepository(ContactEntity).find({ + getParties = async (args?: GetPartiesArgs): Promise> => { + const partyRepository: Repository = (await this.dbConnection).getRepository(PartyEntity) + const initialResult: Array = await partyRepository.find({ ...(args?.filter && { where: args?.filter }), }) - const result = await (await this.dbConnection).getRepository(ContactEntity).find({ + const result: Array = await partyRepository.find({ where: { - id: In(initialResult.map((contact: ContactEntity) => contact.id)), + id: In(initialResult.map((party: PartyEntity) => party.id)), }, }) - return result.map((contact: ContactEntity) => this.contactFrom(contact)) + return result.map((party: PartyEntity) => partyFrom(party)) } - addContact = async (args: IAddContactArgs): Promise => { - const { name, alias, uri, identities } = args + addParty = async (args: AddPartyArgs): Promise => { + const { identities, contact, partyType } = args - const result = await (await this.dbConnection).getRepository(ContactEntity).findOne({ - where: [{ name }, { alias }], - }) + const partyRepository: Repository = (await this.dbConnection).getRepository(PartyEntity) - if (result) { - return Promise.reject(Error(`Duplicate names or aliases are not allowed. Name: ${name}, Alias: ${alias}`)) + if (!this.hasCorrectPartyType(partyType.type, contact)) { + return Promise.reject(Error(`Party type ${partyType.type}, does not match for provided contact`)) } for (const identity of identities ?? []) { @@ -87,103 +108,64 @@ export class ContactStore extends AbstractContactStore { return Promise.reject(Error(`Identity with correlation type ${CorrelationIdentifierEnum.URL} should contain a connection`)) } - if (!this.hasCorrectConfig(identity.connection.type, identity.connection.config)) { + if (!this.hasCorrectConnectionConfig(identity.connection.type, identity.connection.config)) { return Promise.reject(Error(`Connection type ${identity.connection.type}, does not match for provided config`)) } } } - const contactEntity = contactEntityFrom({ name, alias, uri, identities }) - debug('Adding contact', name) - const createdResult = await (await this.dbConnection).getRepository(ContactEntity).save(contactEntity) + const partyEntity: PartyEntity = partyEntityFrom(args) + debug('Adding party', args) + const createdResult: PartyEntity = await partyRepository.save(partyEntity) - return this.contactFrom(createdResult) + return partyFrom(createdResult) } - updateContact = async ({ contact }: IUpdateContactArgs): Promise => { - const result = await (await this.dbConnection).getRepository(ContactEntity).findOne({ - where: { id: contact.id }, + updateParty = async ({ party }: UpdatePartyArgs): Promise => { + const partyRepository: Repository = (await this.dbConnection).getRepository(PartyEntity) + const result: PartyEntity | null = await partyRepository.findOne({ + where: { id: party.id }, }) if (!result) { - return Promise.reject(Error(`No contact found for id: ${contact.id}`)) + return Promise.reject(Error(`No party found for id: ${party.id}`)) } - const updatedContact = { - ...contact, + const updatedParty = { + ...party, identities: result.identities, + type: result.partyType, + relationships: result.relationships, + electronicAddresses: result.electronicAddresses, } - debug('Updating contact', contact) - const updatedResult = await (await this.dbConnection).getRepository(ContactEntity).save(updatedContact, { transaction: true }) + debug('Updating party', party) + const updatedResult: PartyEntity = await partyRepository.save(updatedParty, { transaction: true }) - return this.contactFrom(updatedResult) + return partyFrom(updatedResult) } - removeContact = async ({ contactId }: IRemoveContactArgs): Promise => { - debug('Removing contact', contactId) - ;(await this.dbConnection) - .getRepository(ContactEntity) - .findOneById(contactId) - .then(async (contact: ContactEntity | null) => { - if (!contact) { - await Promise.reject(Error(`Unable to find the contact with id to remove: ${contactId}`)) + removeParty = async ({ partyId }: RemovePartyArgs): Promise => { + const partyRepository: Repository = (await this.dbConnection).getRepository(PartyEntity) + debug('Removing party', partyId) + partyRepository + .findOneById(partyId) + .then(async (party: PartyEntity | null): Promise => { + if (!party) { + await Promise.reject(Error(`Unable to find the party with id to remove: ${partyId}`)) } else { - await this.deleteIdentities(contact.identities) + await this.deleteIdentities(party.identities) - await ( - await this.dbConnection - ) - .getRepository(ContactEntity) - .delete({ id: contactId }) - .catch((error) => Promise.reject(Error(`Unable to remove contact with id: ${contactId}. ${error}`))) + await partyRepository + .delete({ id: partyId }) + .catch((error) => Promise.reject(Error(`Unable to remove party with id: ${partyId}. ${error}`))) } }) - .catch((error) => Promise.reject(Error(`Unable to remove contact with id: ${contactId}. ${error}`))) + .catch((error) => Promise.reject(Error(`Unable to remove party with id: ${partyId}. ${error}`))) } - private async deleteIdentities(identities: Array): Promise { - debug('Removing identities', identities) - - identities.map(async (identity: IdentityEntity) => { - await ( - await this.dbConnection - ) - .getRepository(CorrelationIdentifierEntity) - .delete(identity.identifier.id) - .catch((error) => Promise.reject(Error(`Unable to remove identity.identifier with id: ${identity.identifier.id}. ${error}`))) - - if (identity.connection) { - await (await this.dbConnection).getRepository(BaseConfigEntity).delete(identity.connection.config.id) - - await ( - await this.dbConnection - ) - .getRepository(ConnectionEntity) - .delete(identity.connection.id) - .catch((error) => Promise.reject(Error(`Unable to remove identity.connection with id. ${error}`))) - } - - if (identity.metadata) { - identity.metadata.map(async (metadataItem: IdentityMetadataItemEntity) => { - await ( - await this.dbConnection - ) - .getRepository(IdentityMetadataItemEntity) - .delete(metadataItem.id) - .catch((error) => Promise.reject(Error(`Unable to remove metadataItem.id with id ${metadataItem.id}. ${error}`))) - }) - } - - ;(await this.dbConnection) - .getRepository(IdentityEntity) - .delete(identity.id) - .catch((error) => Promise.reject(Error(`Unable to remove metadataItem.id with id ${identity.id}. ${error}`))) - }) - } - - getIdentity = async ({ identityId }: IGetIdentityArgs): Promise => { - const result = await (await this.dbConnection).getRepository(IdentityEntity).findOne({ + getIdentity = async ({ identityId }: GetIdentityArgs): Promise => { + const result: IdentityEntity | null = await (await this.dbConnection).getRepository(IdentityEntity).findOne({ where: { id: identityId }, }) @@ -191,30 +173,31 @@ export class ContactStore extends AbstractContactStore { return Promise.reject(Error(`No identity found for id: ${identityId}`)) } - return this.identityFrom(result) + return identityFrom(result) } - getIdentities = async (args?: IGetIdentitiesArgs): Promise> => { - const initialResult = await (await this.dbConnection).getRepository(IdentityEntity).find({ + getIdentities = async (args?: GetIdentitiesArgs): Promise> => { + const identityRepository: Repository = (await this.dbConnection).getRepository(IdentityEntity) + const initialResult: Array = await identityRepository.find({ ...(args?.filter && { where: args?.filter }), }) - const result = await (await this.dbConnection).getRepository(IdentityEntity).find({ + const result: Array = await identityRepository.find({ where: { id: In(initialResult.map((identity: IdentityEntity) => identity.id)), }, }) - return result.map((identity: IdentityEntity) => this.identityFrom(identity)) + return result.map((identity: IdentityEntity) => identityFrom(identity)) } - addIdentity = async ({ identity, contactId }: IAddIdentityArgs): Promise => { - const contact = await (await this.dbConnection).getRepository(ContactEntity).findOne({ - where: { id: contactId }, + addIdentity = async ({ identity, partyId }: AddIdentityArgs): Promise => { + const party: PartyEntity | null = await (await this.dbConnection).getRepository(PartyEntity).findOne({ + where: { id: partyId }, }) - if (!contact) { - return Promise.reject(Error(`No contact found for id: ${contactId}`)) + if (!party) { + return Promise.reject(Error(`No party found for id: ${partyId}`)) } if (identity.identifier.type === CorrelationIdentifierEnum.URL) { @@ -222,23 +205,24 @@ export class ContactStore extends AbstractContactStore { return Promise.reject(Error(`Identity with correlation type ${CorrelationIdentifierEnum.URL} should contain a connection`)) } - if (!this.hasCorrectConfig(identity.connection.type, identity.connection.config)) { + if (!this.hasCorrectConnectionConfig(identity.connection.type, identity.connection.config)) { return Promise.reject(Error(`Connection type ${identity.connection.type}, does not match for provided config`)) } } - const identityEntity = identityEntityFrom(identity) - identityEntity.contact = contact + const identityEntity: IdentityEntity = identityEntityFrom(identity) + identityEntity.party = party debug('Adding identity', identity) - const result = await (await this.dbConnection).getRepository(IdentityEntity).save(identityEntity, { + const result: IdentityEntity = await (await this.dbConnection).getRepository(IdentityEntity).save(identityEntity, { transaction: true, }) - return this.identityFrom(result) + return identityFrom(result) } - updateIdentity = async ({ identity }: IUpdateIdentityArgs): Promise => { - const result = await (await this.dbConnection).getRepository(IdentityEntity).findOne({ + updateIdentity = async ({ identity }: UpdateIdentityArgs): Promise => { + const identityRepository: Repository = (await this.dbConnection).getRepository(IdentityEntity) + const result: IdentityEntity | null = await identityRepository.findOne({ where: { id: identity.id }, }) @@ -251,19 +235,19 @@ export class ContactStore extends AbstractContactStore { return Promise.reject(Error(`Identity with correlation type ${CorrelationIdentifierEnum.URL} should contain a connection`)) } - if (!this.hasCorrectConfig(identity.connection.type, identity.connection.config)) { + if (!this.hasCorrectConnectionConfig(identity.connection.type, identity.connection.config)) { return Promise.reject(Error(`Connection type ${identity.connection.type}, does not match for provided config`)) } } debug('Updating identity', identity) - const updatedResult = await (await this.dbConnection).getRepository(IdentityEntity).save(identity, { transaction: true }) + const updatedResult: IdentityEntity = await identityRepository.save(identity, { transaction: true }) - return this.identityFrom(updatedResult) + return identityFrom(updatedResult) } - removeIdentity = async ({ identityId }: IRemoveIdentityArgs): Promise => { - const identity = await (await this.dbConnection).getRepository(IdentityEntity).findOne({ + removeIdentity = async ({ identityId }: RemoveIdentityArgs): Promise => { + const identity: IdentityEntity | null = await (await this.dbConnection).getRepository(IdentityEntity).findOne({ where: { id: identityId }, }) @@ -276,96 +260,236 @@ export class ContactStore extends AbstractContactStore { await this.deleteIdentities([identity]) } - private contactFrom = (contact: ContactEntity): IContact => { - return { - id: contact.id, - name: contact.name, - alias: contact.alias, - uri: contact.uri, - roles: [...new Set(contact.identities?.flatMap((identity) => identity.roles))] ?? [], - identities: contact.identities ? contact.identities.map((identity: IdentityEntity) => this.identityFrom(identity)) : [], - createdAt: contact.createdAt, - lastUpdatedAt: contact.lastUpdatedAt, + addRelationship = async ({ leftId, rightId }: AddRelationshipArgs): Promise => { + return this.assertRelationshipSides(leftId, rightId).then(async (): Promise => { + const relationship: PartyRelationshipEntity = partyRelationshipEntityFrom({ + leftId, + rightId, + }) + debug('Adding party relationship', relationship) + + const createdResult: PartyRelationshipEntity = await (await this.dbConnection).getRepository(PartyRelationshipEntity).save(relationship) + + return partyRelationshipFrom(createdResult) + }) + } + + getRelationship = async ({ relationshipId }: GetRelationshipArgs): Promise => { + const result: PartyRelationshipEntity | null = await (await this.dbConnection).getRepository(PartyRelationshipEntity).findOne({ + where: { id: relationshipId }, + }) + + if (!result) { + return Promise.reject(Error(`No relationship found for id: ${relationshipId}`)) } + + return partyRelationshipFrom(result) } - private identityFrom = (identity: IdentityEntity): IIdentity => { - return { - id: identity.id, - alias: identity.alias, - roles: identity.roles, - identifier: this.correlationIdentifierFrom(identity.identifier), - ...(identity.connection && { connection: this.connectionFrom(identity.connection) }), - metadata: identity.metadata ? identity.metadata.map((item: IdentityMetadataItemEntity) => this.metadataItemFrom(item)) : [], - createdAt: identity.createdAt, - lastUpdatedAt: identity.createdAt, + getRelationships = async (args?: GetRelationshipsArgs): Promise> => { + const partyRelationshipRepository: Repository = (await this.dbConnection).getRepository(PartyRelationshipEntity) + const initialResult: Array = await partyRelationshipRepository.find({ + ...(args?.filter && { where: args?.filter }), + }) + + const result: Array = await partyRelationshipRepository.find({ + where: { + id: In(initialResult.map((partyRelationship: PartyRelationshipEntity) => partyRelationship.id)), + }, + }) + + return result.map((partyRelationship: PartyRelationshipEntity) => partyRelationshipFrom(partyRelationship)) + } + + updateRelationship = async ({ relationship }: UpdateRelationshipArgs): Promise => { + const partyRelationshipRepository: Repository = (await this.dbConnection).getRepository(PartyRelationshipEntity) + const result: PartyRelationshipEntity | null = await partyRelationshipRepository.findOne({ + where: { id: relationship.id }, + }) + + if (!result) { + return Promise.reject(Error(`No party relationship found for id: ${relationship.id}`)) + } + + return this.assertRelationshipSides(relationship.leftId, relationship.rightId).then(async (): Promise => { + debug('Updating party relationship', relationship) + const updatedResult: PartyRelationshipEntity = await partyRelationshipRepository.save(relationship, { transaction: true }) + + return partyRelationshipFrom(updatedResult) + }) + } + + removeRelationship = async ({ relationshipId }: RemoveRelationshipArgs): Promise => { + const partyRelationshipRepository: Repository = (await this.dbConnection).getRepository(PartyRelationshipEntity) + const relationship: PartyRelationshipEntity | null = await partyRelationshipRepository.findOne({ + where: { id: relationshipId }, + }) + + if (!relationship) { + return Promise.reject(Error(`No relationship found for id: ${relationshipId}`)) } + + debug('Removing relationship', relationshipId) + + await partyRelationshipRepository.delete(relationshipId) + } + + addPartyType = async (args: AddPartyTypeArgs): Promise => { + const partyEntity: PartyTypeEntity = partyTypeEntityFrom(args) + debug('Adding party type', args) + const createdResult: PartyTypeEntity = await (await this.dbConnection).getRepository(PartyTypeEntity).save(partyEntity) + + return partyTypeFrom(createdResult) } - private correlationIdentifierFrom = (identifier: CorrelationIdentifierEntity): ICorrelationIdentifier => { - return { - id: identifier.id, - type: identifier.type, - correlationId: identifier.correlationId, + getPartyType = async ({ partyTypeId }: GetPartyTypeArgs): Promise => { + const result: PartyTypeEntity | null = await (await this.dbConnection).getRepository(PartyTypeEntity).findOne({ + where: { id: partyTypeId }, + }) + + if (!result) { + return Promise.reject(Error(`No party type found for id: ${partyTypeId}`)) } + + return partyTypeFrom(result) } - private metadataItemFrom = (item: IdentityMetadataItemEntity): IMetadataItem => { - return { - id: item.id, - label: item.label, - value: item.value, + getPartyTypes = async (args?: GetPartyTypesArgs): Promise> => { + const partyTypeRepository: Repository = (await this.dbConnection).getRepository(PartyTypeEntity) + const initialResult: Array = await partyTypeRepository.find({ + ...(args?.filter && { where: args?.filter }), + }) + + const result: Array = await partyTypeRepository.find({ + where: { + id: In(initialResult.map((partyType: PartyTypeEntity) => partyType.id)), + }, + }) + + return result.map((partyType: PartyTypeEntity) => partyTypeFrom(partyType)) + } + + updatePartyType = async ({ partyType }: UpdatePartyTypeArgs): Promise => { + const partyTypeRepository: Repository = (await this.dbConnection).getRepository(PartyTypeEntity) + const result: PartyTypeEntity | null = await partyTypeRepository.findOne({ + where: { id: partyType.id }, + }) + + if (!result) { + return Promise.reject(Error(`No party type found for id: ${partyType.id}`)) } + + debug('Updating party type', partyType) + const updatedResult: PartyTypeEntity = await partyTypeRepository.save(partyType, { transaction: true }) + + return partyTypeFrom(updatedResult) } - private connectionFrom = (connection: ConnectionEntity): IConnection => { - return { - id: connection.id, - type: connection.type, - config: this.configFrom(connection.type, connection.config), + removePartyType = async ({ partyTypeId }: RemovePartyTypeArgs): Promise => { + const parties: Array = await (await this.dbConnection).getRepository(PartyEntity).find({ + where: { + partyType: { + id: partyTypeId, + }, + }, + }) + + if (parties.length > 0) { + return Promise.reject(Error(`Unable to remove party type with id: ${partyTypeId}. Party type is in use`)) } + + const partyTypeRepository: Repository = (await this.dbConnection).getRepository(PartyTypeEntity) + const partyType: PartyTypeEntity | null = await partyTypeRepository.findOne({ + where: { id: partyTypeId }, + }) + + if (!partyType) { + return Promise.reject(Error(`No party type found for id: ${partyTypeId}`)) + } + + debug('Removing party type', partyTypeId) + + await partyTypeRepository.delete(partyTypeId) } - private configFrom = (type: ConnectionTypeEnum, config: BaseConfigEntity): ConnectionConfig => { + private hasCorrectConnectionConfig(type: ConnectionTypeEnum, config: NonPersistedConnectionConfig): boolean { switch (type) { case ConnectionTypeEnum.OPENID_CONNECT: - return { - id: (config as OpenIdConfigEntity).id, - clientId: (config as OpenIdConfigEntity).clientId, - clientSecret: (config as OpenIdConfigEntity).clientSecret, - scopes: (config as OpenIdConfigEntity).scopes, - issuer: (config as OpenIdConfigEntity).issuer!, // TODO fixme - redirectUrl: (config as OpenIdConfigEntity).redirectUrl, - dangerouslyAllowInsecureHttpRequests: (config as OpenIdConfigEntity).dangerouslyAllowInsecureHttpRequests, - clientAuthMethod: (config as OpenIdConfigEntity).clientAuthMethod, - } + return isOpenIdConfig(config) case ConnectionTypeEnum.SIOPv2: - return { - id: (config as DidAuthConfigEntity).id, - identifier: { did: (config as DidAuthConfigEntity).identifier, provider: '', keys: [], services: [] }, - stateId: '', - redirectUrl: (config as DidAuthConfigEntity).redirectUrl, - sessionId: (config as DidAuthConfigEntity).sessionId, - } + return isDidAuthConfig(config) default: throw new Error('Connection type not supported') } } - private hasCorrectConfig(type: ConnectionTypeEnum, config: BasicConnectionConfig): boolean { + private hasCorrectPartyType(type: PartyTypeEnum, contact: NonPersistedContact): boolean { switch (type) { - case ConnectionTypeEnum.OPENID_CONNECT: - return this.isOpenIdConfig(config) - case ConnectionTypeEnum.SIOPv2: - return this.isDidAuthConfig(config) + case PartyTypeEnum.NATURAL_PERSON: + return isNaturalPerson(contact) + case PartyTypeEnum.ORGANIZATION: + return isOrganization(contact) default: - throw new Error('Connection type not supported') + throw new Error('Party type not supported') } } - private isOpenIdConfig = (config: BasicConnectionConfig): config is IOpenIdConfig => - 'clientSecret' in config && 'issuer' in config && 'redirectUrl' in config + private async deleteIdentities(identities: Array): Promise { + debug('Removing identities', identities) - private isDidAuthConfig = (config: BasicConnectionConfig): config is IDidAuthConfig => - 'identifier' in config && 'redirectUrl' in config && 'sessionId' in config + identities.map(async (identity: IdentityEntity): Promise => { + await ( + await this.dbConnection + ) + .getRepository(CorrelationIdentifierEntity) + .delete(identity.identifier.id) + .catch((error) => Promise.reject(Error(`Unable to remove identity.identifier with id: ${identity.identifier.id}. ${error}`))) + + if (identity.connection) { + await (await this.dbConnection).getRepository(BaseConfigEntity).delete(identity.connection.config.id) + + await ( + await this.dbConnection + ) + .getRepository(ConnectionEntity) + .delete(identity.connection.id) + .catch((error) => Promise.reject(Error(`Unable to remove identity.connection with id. ${error}`))) + } + + if (identity.metadata) { + identity.metadata.map(async (metadataItem: IdentityMetadataItemEntity): Promise => { + await ( + await this.dbConnection + ) + .getRepository(IdentityMetadataItemEntity) + .delete(metadataItem.id) + .catch((error) => Promise.reject(Error(`Unable to remove metadataItem.id with id ${metadataItem.id}. ${error}`))) + }) + } + + ;(await this.dbConnection) + .getRepository(IdentityEntity) + .delete(identity.id) + .catch((error) => Promise.reject(Error(`Unable to remove metadataItem.id with id ${identity.id}. ${error}`))) + }) + } + + private async assertRelationshipSides(leftId: string, rightId: string): Promise { + const partyRepository: Repository = (await this.dbConnection).getRepository(PartyEntity) + const leftParty: PartyEntity | null = await partyRepository.findOne({ + where: { id: leftId }, + }) + + if (!leftParty) { + return Promise.reject(Error(`No party found for left side of the relationship, party id: ${leftId}`)) + } + + const rightParty: PartyEntity | null = await partyRepository.findOne({ + where: { id: rightId }, + }) + + if (!rightParty) { + return Promise.reject(Error(`No party found for right side of the relationship, party id: ${rightId}`)) + } + } } diff --git a/packages/data-store/src/entities/contact/BaseConfigEntity.ts b/packages/data-store/src/entities/contact/BaseConfigEntity.ts index c59f4d945..aa371f67f 100644 --- a/packages/data-store/src/entities/contact/BaseConfigEntity.ts +++ b/packages/data-store/src/entities/contact/BaseConfigEntity.ts @@ -1,8 +1,16 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, TableInheritance } from 'typeorm' +import { BaseEntity, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, TableInheritance } from 'typeorm' +import { ConnectionEntity } from './ConnectionEntity' -@Entity('BaseConfigEntity') // FIXME rename it to 'BaseConfig' +@Entity('BaseConfig') @TableInheritance({ column: { type: 'varchar', name: 'type' } }) export abstract class BaseConfigEntity extends BaseEntity { @PrimaryGeneratedColumn('uuid') id!: string + + @OneToOne(() => ConnectionEntity, (connection: ConnectionEntity) => connection.config, { + cascade: ['insert', 'update'], + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'connection_id' }) + connection?: ConnectionEntity } diff --git a/packages/data-store/src/entities/contact/BaseContactEntity.ts b/packages/data-store/src/entities/contact/BaseContactEntity.ts new file mode 100644 index 000000000..d0f548eb0 --- /dev/null +++ b/packages/data-store/src/entities/contact/BaseContactEntity.ts @@ -0,0 +1,30 @@ +import { BaseEntity, + BeforeInsert, + BeforeUpdate, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, TableInheritance, UpdateDateColumn } from 'typeorm' +import { PartyEntity } from './PartyEntity' + +@Entity('BaseContact') +@TableInheritance({ column: { type: 'varchar', name: 'type' } }) +export abstract class BaseContactEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @CreateDateColumn({ name: 'created_at', nullable: false }) + createdAt!: Date + + @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) + lastUpdatedAt!: Date + + @OneToOne(() => PartyEntity, (party: PartyEntity) => party.contact, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'party_id' }) + party!: PartyEntity + + // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. + @BeforeInsert() + @BeforeUpdate() + updateUpdatedDate(): void { + this.lastUpdatedAt = new Date() + } +} diff --git a/packages/data-store/src/entities/contact/ConnectionEntity.ts b/packages/data-store/src/entities/contact/ConnectionEntity.ts index b577ed3c3..1c4daecf0 100644 --- a/packages/data-store/src/entities/contact/ConnectionEntity.ts +++ b/packages/data-store/src/entities/contact/ConnectionEntity.ts @@ -1,9 +1,9 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, BaseEntity } from 'typeorm' import { BaseConfigEntity } from './BaseConfigEntity' -import { BasicConnectionConfig, ConnectionTypeEnum, IBasicConnection, IDidAuthConfig, IOpenIdConfig } from '../../types' +import { ConnectionTypeEnum } from '../../types' import { IdentityEntity } from './IdentityEntity' -import { OpenIdConfigEntity, openIdConfigEntityFrom } from './OpenIdConfigEntity' -import { DidAuthConfigEntity, didAuthConfigEntityFrom } from './DidAuthConfigEntity' +import { OpenIdConfigEntity } from './OpenIdConfigEntity' +import { DidAuthConfigEntity } from './DidAuthConfigEntity' @Entity('Connection') export class ConnectionEntity extends BaseEntity { @@ -24,25 +24,6 @@ export class ConnectionEntity extends BaseEntity { @OneToOne(() => IdentityEntity, (identity: IdentityEntity) => identity.connection, { onDelete: 'CASCADE', }) - @JoinColumn({ name: 'identityId' }) + @JoinColumn({ name: 'identity_id' }) identity!: IdentityEntity } - -export const connectionEntityFrom = (connection: IBasicConnection): ConnectionEntity => { - const connectionEntity = new ConnectionEntity() - connectionEntity.type = connection.type - connectionEntity.config = configEntityFrom(connection.type, connection.config) - - return connectionEntity -} - -const configEntityFrom = (type: ConnectionTypeEnum, config: BasicConnectionConfig): BaseConfigEntity => { - switch (type) { - case ConnectionTypeEnum.OPENID_CONNECT: - return openIdConfigEntityFrom(config as IOpenIdConfig) - case ConnectionTypeEnum.SIOPv2: - return didAuthConfigEntityFrom(config as IDidAuthConfig) - default: - throw new Error('Connection type not supported') - } -} diff --git a/packages/data-store/src/entities/contact/ContactEntity.ts b/packages/data-store/src/entities/contact/ContactEntity.ts deleted file mode 100644 index f4f8061ef..000000000 --- a/packages/data-store/src/entities/contact/ContactEntity.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - BaseEntity, - BeforeInsert, - BeforeUpdate, - Column, - CreateDateColumn, - Entity, - JoinColumn, - OneToMany, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' -import { IBasicContact, IBasicIdentity } from '../../types' -import { IdentityEntity, identityEntityFrom } from './IdentityEntity' -import { IsNotEmpty, validate, ValidationError } from 'class-validator' - -@Entity('Contact') -export class ContactEntity extends BaseEntity { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column({ name: 'name', length: 255, nullable: false, unique: true }) - @IsNotEmpty({ message: 'Blank names are not allowed' }) - name!: string - - @Column({ name: 'alias', length: 255, nullable: false, unique: true }) - @IsNotEmpty({ message: 'Blank aliases are not allowed' }) - alias!: string - - @Column({ name: 'uri', length: 255 }) - uri?: string - - @OneToMany(() => IdentityEntity, (identity: IdentityEntity) => identity.contact, { - cascade: true, - onDelete: 'CASCADE', - eager: true, - nullable: false, - }) - @JoinColumn({ name: 'identityId' }) - identities!: Array - - @CreateDateColumn({ name: 'created_at', nullable: false }) - createdAt!: Date - - @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) - lastUpdatedAt!: Date - - // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. - @BeforeInsert() - @BeforeUpdate() - updateUpdatedDate() { - this.lastUpdatedAt = new Date() - } - - @BeforeInsert() - @BeforeUpdate() - async validate() { - const validation: Array = await validate(this) - if (validation.length > 0) { - return Promise.reject(Error(Object.values(validation[0].constraints!)[0])) - } - return - } -} - -export const contactEntityFrom = (args: IBasicContact): ContactEntity => { - const contactEntity = new ContactEntity() - contactEntity.name = args.name - contactEntity.alias = args.alias - contactEntity.uri = args.uri - contactEntity.identities = args.identities ? args.identities.map((identity: IBasicIdentity) => identityEntityFrom(identity)) : [] - - return contactEntity -} diff --git a/packages/data-store/src/entities/contact/CorrelationIdentifierEntity.ts b/packages/data-store/src/entities/contact/CorrelationIdentifierEntity.ts index caac9a437..5bcd4abd6 100644 --- a/packages/data-store/src/entities/contact/CorrelationIdentifierEntity.ts +++ b/packages/data-store/src/entities/contact/CorrelationIdentifierEntity.ts @@ -1,7 +1,8 @@ import { Entity, Column, PrimaryGeneratedColumn, BaseEntity, OneToOne, JoinColumn, BeforeInsert, BeforeUpdate } from 'typeorm' -import { CorrelationIdentifierEnum, BasicCorrelationIdentifier } from '../../types' +import { CorrelationIdentifierEnum, ValidationConstraint } from '../../types' import { IdentityEntity } from './IdentityEntity' import { IsNotEmpty, validate, ValidationError } from 'class-validator' +import { getConstraint } from '../../utils/ValidatorUtils' @Entity('CorrelationIdentifier') export class CorrelationIdentifierEntity extends BaseEntity { @@ -18,24 +19,19 @@ export class CorrelationIdentifierEntity extends BaseEntity { @OneToOne(() => IdentityEntity, (identity: IdentityEntity) => identity.identifier, { onDelete: 'CASCADE', }) - @JoinColumn({ name: 'identityId' }) + @JoinColumn({ name: 'identity_id' }) identity!: IdentityEntity @BeforeInsert() @BeforeUpdate() - async validate() { + async validate(): Promise { const validation: Array = await validate(this) if (validation.length > 0) { - return Promise.reject(Error(Object.values(validation[0].constraints!)[0])) + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } } - return } } - -export const correlationIdentifierEntityFrom = (identifier: BasicCorrelationIdentifier): CorrelationIdentifierEntity => { - const identifierEntity = new CorrelationIdentifierEntity() - identifierEntity.type = identifier.type - identifierEntity.correlationId = identifier.correlationId - - return identifierEntity -} diff --git a/packages/data-store/src/entities/contact/DidAuthConfigEntity.ts b/packages/data-store/src/entities/contact/DidAuthConfigEntity.ts index 93b0a4b9a..29685ba61 100644 --- a/packages/data-store/src/entities/contact/DidAuthConfigEntity.ts +++ b/packages/data-store/src/entities/contact/DidAuthConfigEntity.ts @@ -1,7 +1,5 @@ -import { ChildEntity, Column, JoinColumn, OneToOne } from 'typeorm' +import { ChildEntity, Column } from 'typeorm' import { BaseConfigEntity } from './BaseConfigEntity' -import { BasicDidAuthConfig } from '../../types' -import { ConnectionEntity } from './ConnectionEntity' @ChildEntity('DidAuthConfig') export class DidAuthConfigEntity extends BaseConfigEntity { @@ -13,19 +11,4 @@ export class DidAuthConfigEntity extends BaseConfigEntity { @Column({ name: 'session_id', length: 255, nullable: false }) sessionId!: string - - @OneToOne(() => ConnectionEntity, (connection: ConnectionEntity) => connection.config, { - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'connectionId' }) - connection?: ConnectionEntity -} - -export const didAuthConfigEntityFrom = (config: BasicDidAuthConfig): DidAuthConfigEntity => { - const didAuthConfig = new DidAuthConfigEntity() - didAuthConfig.identifier = config.identifier.did - didAuthConfig.redirectUrl = config.redirectUrl - didAuthConfig.sessionId = config.sessionId - - return didAuthConfig } diff --git a/packages/data-store/src/entities/contact/ElectronicAddressEntity.ts b/packages/data-store/src/entities/contact/ElectronicAddressEntity.ts new file mode 100644 index 000000000..ba633bd99 --- /dev/null +++ b/packages/data-store/src/entities/contact/ElectronicAddressEntity.ts @@ -0,0 +1,60 @@ +import { IsNotEmpty, validate, ValidationError } from 'class-validator' +import { + Entity, + Column, + PrimaryGeneratedColumn, + BaseEntity, + ManyToOne, + BeforeInsert, + BeforeUpdate, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm' +import { PartyEntity } from './PartyEntity' +import { getConstraint } from '../../utils/ValidatorUtils' +import { ElectronicAddressType, ValidationConstraint } from '../../types' + +@Entity('ElectronicAddress') +export class ElectronicAddressEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @Column({ name: 'type', length: 255, nullable: false }) + @IsNotEmpty({ message: 'Blank electronic address types are not allowed' }) + type!: ElectronicAddressType + + @Column({ name: 'electronic_address', length: 255, nullable: false }) + @IsNotEmpty({ message: 'Blank electronic addresses are not allowed' }) + electronicAddress!: string + + @ManyToOne(() => PartyEntity, (party: PartyEntity) => party.electronicAddresses, { + onDelete: 'CASCADE', + }) + party!: PartyEntity + + @CreateDateColumn({ name: 'created_at', nullable: false }) + createdAt!: Date + + @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) + lastUpdatedAt!: Date + + // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. + @BeforeInsert() + @BeforeUpdate() + updateUpdatedDate(): void { + this.lastUpdatedAt = new Date() + } + + @BeforeInsert() + @BeforeUpdate() + async validate(): Promise { + const validation: Array = await validate(this) + if (validation.length > 0) { + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } + } + } +} diff --git a/packages/data-store/src/entities/contact/IdentityEntity.ts b/packages/data-store/src/entities/contact/IdentityEntity.ts index 815d5c930..356f528f0 100644 --- a/packages/data-store/src/entities/contact/IdentityEntity.ts +++ b/packages/data-store/src/entities/contact/IdentityEntity.ts @@ -13,11 +13,12 @@ import { BeforeUpdate, } from 'typeorm' import { IsNotEmpty, validate, ValidationError } from 'class-validator' -import { correlationIdentifierEntityFrom, CorrelationIdentifierEntity } from './CorrelationIdentifierEntity' -import { ConnectionEntity, connectionEntityFrom } from './ConnectionEntity' -import { IdentityMetadataItemEntity, metadataItemEntityFrom } from './IdentityMetadataItemEntity' -import { BasicMetadataItem, IBasicIdentity, IdentityRoleEnum } from '../../types' -import { ContactEntity } from './ContactEntity' +import { CorrelationIdentifierEntity } from './CorrelationIdentifierEntity' +import { ConnectionEntity } from './ConnectionEntity' +import { IdentityMetadataItemEntity } from './IdentityMetadataItemEntity' +import { IdentityRoleEnum, ValidationConstraint } from '../../types' +import { PartyEntity } from './PartyEntity' +import { getConstraint } from '../../utils/ValidatorUtils' @Entity('Identity') export class IdentityEntity extends BaseEntity { @@ -57,7 +58,7 @@ export class IdentityEntity extends BaseEntity { eager: true, nullable: false, }) - @JoinColumn({ name: 'metadataId' }) + @JoinColumn({ name: 'metadata_id' }) // TODO check in db file metadata!: Array @CreateDateColumn({ name: 'created_at', nullable: false }) @@ -66,39 +67,31 @@ export class IdentityEntity extends BaseEntity { @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) lastUpdatedAt!: Date - @ManyToOne(() => ContactEntity, (contact: ContactEntity) => contact.identities, { + @ManyToOne(() => PartyEntity, (party: PartyEntity) => party.identities, { onDelete: 'CASCADE', }) - contact!: ContactEntity + party!: PartyEntity - @Column({ name: 'contactId', nullable: true }) - contactId!: string + @Column({ name: 'partyId', nullable: true }) + partyId!: string // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. @BeforeInsert() @BeforeUpdate() - updateUpdatedDate() { + updateUpdatedDate(): void { this.lastUpdatedAt = new Date() } @BeforeInsert() @BeforeUpdate() - async validate() { + async validate(): Promise { const validation: Array = await validate(this) if (validation.length > 0) { - return Promise.reject(Error(Object.values(validation[0].constraints!)[0])) + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } } - return } } - -export const identityEntityFrom = (args: IBasicIdentity): IdentityEntity => { - const identityEntity = new IdentityEntity() - identityEntity.alias = args.alias - identityEntity.roles = args.roles - identityEntity.identifier = correlationIdentifierEntityFrom(args.identifier) - identityEntity.connection = args.connection ? connectionEntityFrom(args.connection) : undefined - identityEntity.metadata = args.metadata ? args.metadata.map((item: BasicMetadataItem) => metadataItemEntityFrom(item)) : [] - - return identityEntity -} diff --git a/packages/data-store/src/entities/contact/IdentityMetadataItemEntity.ts b/packages/data-store/src/entities/contact/IdentityMetadataItemEntity.ts index 1c511a2d2..025c75399 100644 --- a/packages/data-store/src/entities/contact/IdentityMetadataItemEntity.ts +++ b/packages/data-store/src/entities/contact/IdentityMetadataItemEntity.ts @@ -1,7 +1,8 @@ import { Entity, Column, PrimaryGeneratedColumn, BaseEntity, ManyToOne, BeforeInsert, BeforeUpdate } from 'typeorm' -import { BasicMetadataItem } from '../../types' +import { ValidationConstraint } from '../../types' import { IdentityEntity } from './IdentityEntity' import { IsNotEmpty, validate, ValidationError } from 'class-validator' +import { getConstraint } from '../../utils/ValidatorUtils' @Entity('IdentityMetadata') export class IdentityMetadataItemEntity extends BaseEntity { @@ -21,19 +22,14 @@ export class IdentityMetadataItemEntity extends BaseEntity { @BeforeInsert() @BeforeUpdate() - async validate() { + async validate(): Promise { const validation: Array = await validate(this) if (validation.length > 0) { - return Promise.reject(Error(Object.values(validation[0].constraints!)[0])) + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } } - return } } - -export const metadataItemEntityFrom = (item: BasicMetadataItem): IdentityMetadataItemEntity => { - const metadataItem = new IdentityMetadataItemEntity() - metadataItem.label = item.label - metadataItem.value = item.value - - return metadataItem -} diff --git a/packages/data-store/src/entities/contact/NaturalPersonEntity.ts b/packages/data-store/src/entities/contact/NaturalPersonEntity.ts new file mode 100644 index 000000000..b04089bbb --- /dev/null +++ b/packages/data-store/src/entities/contact/NaturalPersonEntity.ts @@ -0,0 +1,38 @@ +import { Column, ChildEntity, BeforeInsert, BeforeUpdate } from 'typeorm' +import { BaseContactEntity } from './BaseContactEntity' +import { ValidationConstraint } from '../../types' +import { validate, IsNotEmpty, ValidationError, Validate } from 'class-validator' +import { IsNonEmptyStringConstraint } from '../validators' +import { getConstraint } from '../../utils/ValidatorUtils' + +@ChildEntity('NaturalPerson') +export class NaturalPersonEntity extends BaseContactEntity { + @Column({ name: 'first_name', length: 255, nullable: false, unique: false }) + @IsNotEmpty({ message: 'Blank first names are not allowed' }) + firstName!: string + + @Column({ name: 'middle_name', length: 255, nullable: true, unique: false }) + @Validate(IsNonEmptyStringConstraint, { message: 'Blank middle names are not allowed' }) + middleName?: string + + @Column({ name: 'last_name', length: 255, nullable: false, unique: false }) + @IsNotEmpty({ message: 'Blank last names are not allowed' }) + lastName!: string + + @Column({ name: 'display_name', length: 255, nullable: false, unique: false }) + @IsNotEmpty({ message: 'Blank display names are not allowed' }) + displayName!: string + + @BeforeInsert() + @BeforeUpdate() + async validate(): Promise { + const validation: Array = await validate(this) + if (validation.length > 0) { + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } + } + } +} diff --git a/packages/data-store/src/entities/contact/OpenIdConfigEntity.ts b/packages/data-store/src/entities/contact/OpenIdConfigEntity.ts index dcb18a368..07871ed4c 100644 --- a/packages/data-store/src/entities/contact/OpenIdConfigEntity.ts +++ b/packages/data-store/src/entities/contact/OpenIdConfigEntity.ts @@ -1,7 +1,5 @@ -import { ChildEntity, Column, JoinColumn, OneToOne } from 'typeorm' +import { ChildEntity, Column } from 'typeorm' import { BaseConfigEntity } from './BaseConfigEntity' -import { BasicOpenIdConfig } from '../../types' -import { ConnectionEntity } from './ConnectionEntity' @ChildEntity('OpenIdConfig') export class OpenIdConfigEntity extends BaseConfigEntity { @@ -25,24 +23,4 @@ export class OpenIdConfigEntity extends BaseConfigEntity { @Column('text', { name: 'client_auth_method', nullable: false }) clientAuthMethod!: 'basic' | 'post' | undefined - - @OneToOne(() => ConnectionEntity, (connection: ConnectionEntity) => connection.config, { - cascade: ['insert', 'update'], - onDelete: 'CASCADE', - }) - @JoinColumn({ name: 'connectionId' }) - connection?: ConnectionEntity -} - -export const openIdConfigEntityFrom = (config: BasicOpenIdConfig): OpenIdConfigEntity => { - const openIdConfig = new OpenIdConfigEntity() - openIdConfig.clientId = config.clientId - openIdConfig.clientSecret = config.clientSecret - openIdConfig.scopes = config.scopes - openIdConfig.issuer = config.issuer - openIdConfig.redirectUrl = config.redirectUrl - openIdConfig.dangerouslyAllowInsecureHttpRequests = config.dangerouslyAllowInsecureHttpRequests - openIdConfig.clientAuthMethod = config.clientAuthMethod - - return openIdConfig } diff --git a/packages/data-store/src/entities/contact/OrganizationEntity.ts b/packages/data-store/src/entities/contact/OrganizationEntity.ts new file mode 100644 index 000000000..a48e9d4b1 --- /dev/null +++ b/packages/data-store/src/entities/contact/OrganizationEntity.ts @@ -0,0 +1,34 @@ +import { JoinColumn, OneToOne, Column, ChildEntity, BeforeInsert, BeforeUpdate } from 'typeorm' +import { PartyEntity } from './PartyEntity' +import { BaseContactEntity } from './BaseContactEntity' +import { ValidationConstraint } from '../../types' +import { validate, IsNotEmpty, ValidationError } from 'class-validator' +import { getConstraint } from '../../utils/ValidatorUtils' + +@ChildEntity('Organization') +export class OrganizationEntity extends BaseContactEntity { + @Column({ name: 'legal_name', length: 255, nullable: false, unique: true }) + @IsNotEmpty({ message: 'Blank legal names are not allowed' }) + legalName!: string + + @Column({ name: 'display_name', length: 255, nullable: false, unique: false }) + @IsNotEmpty({ message: 'Blank display names are not allowed' }) + displayName!: string + + @OneToOne(() => PartyEntity) + @JoinColumn({ name: 'party_id' }) + party!: PartyEntity + + @BeforeInsert() + @BeforeUpdate() + async validate(): Promise { + const validation: Array = await validate(this) + if (validation.length > 0) { + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } + } + } +} diff --git a/packages/data-store/src/entities/contact/PartyEntity.ts b/packages/data-store/src/entities/contact/PartyEntity.ts new file mode 100644 index 000000000..43bb09279 --- /dev/null +++ b/packages/data-store/src/entities/contact/PartyEntity.ts @@ -0,0 +1,118 @@ +import { + BaseEntity, + BeforeInsert, + BeforeUpdate, + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + OneToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm' +import { ValidationConstraint } from '../../types' +import { IdentityEntity } from './IdentityEntity' +import { validate, ValidationError } from 'class-validator' +import { PartyTypeEntity } from './PartyTypeEntity' +import { BaseContactEntity } from './BaseContactEntity' +import { PartyRelationshipEntity } from './PartyRelationshipEntity' +import { getConstraint } from '../../utils/ValidatorUtils' +import { ElectronicAddressEntity } from './ElectronicAddressEntity' + +@Entity('Party') +export class PartyEntity extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @Column({ name: 'uri', length: 255 }) + uri?: string + + @OneToMany(() => IdentityEntity, (identity: IdentityEntity) => identity.party, { + cascade: true, + onDelete: 'CASCADE', + eager: true, + nullable: false, + }) + @JoinColumn({ name: 'identity_id' }) + identities!: Array + + @OneToMany(() => ElectronicAddressEntity, (electronicAddress: ElectronicAddressEntity) => electronicAddress.party, { + cascade: true, + onDelete: 'CASCADE', + eager: true, + nullable: false, + }) + @JoinColumn({ name: 'electronic_address_id' }) + electronicAddresses!: Array + + @ManyToOne(() => PartyTypeEntity, (contactType: PartyTypeEntity) => contactType.parties, { + cascade: true, + nullable: false, + eager: true, + }) + @JoinColumn({ name: 'party_type_id' }) + partyType!: PartyTypeEntity + + @OneToOne(() => BaseContactEntity, (contact: BaseContactEntity) => contact.party, { + cascade: true, + onDelete: 'CASCADE', + eager: true, + nullable: false, + }) + contact!: BaseContactEntity + + @OneToMany(() => PartyRelationshipEntity, (relationship: PartyRelationshipEntity) => relationship.left, { + cascade: true, + onDelete: 'CASCADE', + eager: true, + nullable: false, + }) + @JoinColumn({ name: 'relationship_id' }) + relationships!: Array + + @CreateDateColumn({ name: 'created_at', nullable: false }) + createdAt!: Date + + @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) + lastUpdatedAt!: Date + + // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. + @BeforeInsert() + @BeforeUpdate() + updateUpdatedDate(): void { + this.lastUpdatedAt = new Date() + } + + @BeforeInsert() + @BeforeUpdate() + async checkUniqueTenantId(): Promise { + const result: Array = await PartyEntity.find({ + where: { + partyType: { + tenantId: this.partyType.tenantId, + }, + }, + }) + + if (result?.length > 0) { + return Promise.reject(Error('Tenant id already in use')) + } + + return + } + + @BeforeInsert() + @BeforeUpdate() + async validate(): Promise { + const validation: Array = await validate(this) + if (validation.length > 0) { + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } + } + } +} diff --git a/packages/data-store/src/entities/contact/PartyRelationshipEntity.ts b/packages/data-store/src/entities/contact/PartyRelationshipEntity.ts new file mode 100644 index 000000000..2cc482262 --- /dev/null +++ b/packages/data-store/src/entities/contact/PartyRelationshipEntity.ts @@ -0,0 +1,61 @@ +import { + Entity, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + Column, + Index, + BeforeInsert, + BeforeUpdate, + JoinColumn, +} from 'typeorm' +import { PartyEntity } from './PartyEntity' + +@Entity('PartyRelationship') +@Index('IDX_PartyRelationship_left_right', ['left', 'right'], { unique: true }) +export class PartyRelationshipEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @ManyToOne(() => PartyEntity, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'left_id' }) + left!: PartyEntity + + @Column({ name: 'left_id', nullable: false }) + leftId!: string + + @ManyToOne(() => PartyEntity, { + nullable: false, + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'right_id' }) + right!: PartyEntity + + @Column({ name: 'right_id', nullable: false }) + rightId!: string + + @CreateDateColumn({ name: 'created_at', nullable: false }) + createdAt!: Date + + @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) + lastUpdatedAt!: Date + + // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. + @BeforeInsert() + @BeforeUpdate() + updateUpdatedDate(): void { + this.lastUpdatedAt = new Date() + } + + @BeforeInsert() + @BeforeUpdate() + async checkRelationshipSides(): Promise { + if ((this.left?.id ?? this.leftId) === (this.right?.id ?? this.rightId)) { + return Promise.reject(Error('Cannot use the same id for both sides of the relationship')) + } + } +} diff --git a/packages/data-store/src/entities/contact/PartyTypeEntity.ts b/packages/data-store/src/entities/contact/PartyTypeEntity.ts new file mode 100644 index 000000000..8436b615d --- /dev/null +++ b/packages/data-store/src/entities/contact/PartyTypeEntity.ts @@ -0,0 +1,59 @@ +import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn, UpdateDateColumn, OneToMany, BeforeInsert, BeforeUpdate } from 'typeorm' +import { PartyEntity } from './PartyEntity' +import { PartyTypeEnum, ValidationConstraint } from '../../types' +import { IsNotEmpty, Validate, validate, ValidationError } from 'class-validator' +import { IsNonEmptyStringConstraint } from '../validators' +import { getConstraint } from '../../utils/ValidatorUtils' + +@Entity('PartyType') +@Index('IDX_PartyType_type_tenant_id', ['type', 'tenantId'], { unique: true }) +export class PartyTypeEntity { + @PrimaryGeneratedColumn('uuid') + id!: string + + @Column('simple-enum', { name: 'type', enum: PartyTypeEnum, nullable: false, unique: false }) + type!: PartyTypeEnum + + @Column({ name: 'name', length: 255, nullable: false, unique: true }) + @IsNotEmpty({ message: 'Blank names are not allowed' }) + name!: string + + @Column({ name: 'description', length: 255, nullable: true, unique: false }) + @Validate(IsNonEmptyStringConstraint, { message: 'Blank descriptions are not allowed' }) + description?: string + + @Column({ name: 'tenant_id', length: 255, nullable: false, unique: false }) + @IsNotEmpty({ message: "Blank tenant id's are not allowed" }) + tenantId!: string + + @OneToMany(() => PartyEntity, (party: PartyEntity) => party.partyType, { + nullable: false, + }) + parties!: Array + + @CreateDateColumn({ name: 'created_at', nullable: false }) + createdAt!: Date + + @UpdateDateColumn({ name: 'last_updated_at', nullable: false }) + lastUpdatedAt!: Date + + // By default, @UpdateDateColumn in TypeORM updates the timestamp only when the entity's top-level properties change. + @BeforeInsert() + @BeforeUpdate() + updateUpdatedDate(): void { + this.lastUpdatedAt = new Date() + } + + @BeforeInsert() + @BeforeUpdate() + async validate(): Promise { + const validation: Array = await validate(this) + if (validation.length > 0) { + const constraint: ValidationConstraint | undefined = getConstraint(validation[0]) + if (constraint) { + const message: string = Object.values(constraint!)[0] + return Promise.reject(Error(message)) + } + } + } +} diff --git a/packages/data-store/src/index.ts b/packages/data-store/src/index.ts index 5999097ac..8c03dad94 100644 --- a/packages/data-store/src/index.ts +++ b/packages/data-store/src/index.ts @@ -1,12 +1,13 @@ import { BaseConfigEntity } from './entities/contact/BaseConfigEntity' import { BaseLocaleBrandingEntity } from './entities/issuanceBranding/BaseLocaleBrandingEntity' -import { ConnectionEntity, connectionEntityFrom } from './entities/contact/ConnectionEntity' -import { ContactEntity, contactEntityFrom } from './entities/contact/ContactEntity' -import { CorrelationIdentifierEntity, correlationIdentifierEntityFrom } from './entities/contact/CorrelationIdentifierEntity' -import { DidAuthConfigEntity, didAuthConfigEntityFrom } from './entities/contact/DidAuthConfigEntity' -import { IdentityEntity, identityEntityFrom } from './entities/contact/IdentityEntity' -import { IdentityMetadataItemEntity, metadataItemEntityFrom } from './entities/contact/IdentityMetadataItemEntity' -import { OpenIdConfigEntity, openIdConfigEntityFrom } from './entities/contact/OpenIdConfigEntity' +import { BaseContactEntity } from './entities/contact/BaseContactEntity' +import { ConnectionEntity } from './entities/contact/ConnectionEntity' +import { PartyEntity } from './entities/contact/PartyEntity' +import { CorrelationIdentifierEntity } from './entities/contact/CorrelationIdentifierEntity' +import { DidAuthConfigEntity } from './entities/contact/DidAuthConfigEntity' +import { IdentityEntity } from './entities/contact/IdentityEntity' +import { IdentityMetadataItemEntity } from './entities/contact/IdentityMetadataItemEntity' +import { OpenIdConfigEntity } from './entities/contact/OpenIdConfigEntity' import { BackgroundAttributesEntity, backgroundAttributesEntityFrom } from './entities/issuanceBranding/BackgroundAttributesEntity' import { CredentialBrandingEntity, credentialBrandingEntityFrom } from './entities/issuanceBranding/CredentialBrandingEntity' import { CredentialLocaleBrandingEntity, credentialLocaleBrandingEntityFrom } from './entities/issuanceBranding/CredentialLocaleBrandingEntity' @@ -18,7 +19,11 @@ import { TextAttributesEntity, textAttributesEntityFrom } from './entities/issua import { StatusListEntity } from './entities/statusList2021/StatusList2021Entity' import { StatusListEntryEntity } from './entities/statusList2021/StatusList2021EntryEntity' import { IStatusListEntity, IStatusListEntryEntity } from './types' - +import { PartyRelationshipEntity } from './entities/contact/PartyRelationshipEntity' +import { PartyTypeEntity } from './entities/contact/PartyTypeEntity' +import { OrganizationEntity } from './entities/contact/OrganizationEntity' +import { NaturalPersonEntity } from './entities/contact/NaturalPersonEntity' +import { ElectronicAddressEntity } from './entities/contact/ElectronicAddressEntity' export { ContactStore } from './contact/ContactStore' export { AbstractContactStore } from './contact/AbstractContactStore' export { AbstractIssuanceBrandingStore } from './issuanceBranding/AbstractIssuanceBrandingStore' @@ -26,16 +31,23 @@ export { IssuanceBrandingStore } from './issuanceBranding/IssuanceBrandingStore' export { StatusListStore } from './statusList/StatusListStore' export { DataStoreMigrations } from './migrations' export * from './types' +export * from './utils/contact/MappingUtils' export const DataStoreContactEntities = [ BaseConfigEntity, ConnectionEntity, - ContactEntity, + PartyEntity, IdentityEntity, IdentityMetadataItemEntity, CorrelationIdentifierEntity, DidAuthConfigEntity, OpenIdConfigEntity, + PartyRelationshipEntity, + PartyTypeEntity, + BaseContactEntity, + OrganizationEntity, + NaturalPersonEntity, + ElectronicAddressEntity, ] export const DataStoreIssuanceBrandingEntities = [ @@ -58,7 +70,7 @@ export const DataStoreEntities = [...DataStoreContactEntities, ...DataStoreIssua export { BaseConfigEntity, ConnectionEntity, - ContactEntity, + PartyEntity, CorrelationIdentifierEntity, DidAuthConfigEntity, IdentityEntity, @@ -73,13 +85,7 @@ export { TextAttributesEntity, CredentialLocaleBrandingEntity, IssuerLocaleBrandingEntity, - metadataItemEntityFrom, - connectionEntityFrom, - contactEntityFrom, - correlationIdentifierEntityFrom, - identityEntityFrom, - didAuthConfigEntityFrom, - openIdConfigEntityFrom, + ElectronicAddressEntity, backgroundAttributesEntityFrom, credentialBrandingEntityFrom, imageAttributesEntityFrom, diff --git a/packages/data-store/src/migrations/generic/1-CreateContacts.ts b/packages/data-store/src/migrations/generic/1-CreateContacts.ts index 4cd98b960..0191e1808 100644 --- a/packages/data-store/src/migrations/generic/1-CreateContacts.ts +++ b/packages/data-store/src/migrations/generic/1-CreateContacts.ts @@ -1,54 +1,64 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' +import { DatabaseType, MigrationInterface, QueryRunner } from 'typeorm' import Debug from 'debug' import { CreateContacts1659463079428 } from '../postgres/1659463079428-CreateContacts' import { CreateContacts1659463069549 } from '../sqlite/1659463069549-CreateContacts' -const debug = Debug('sphereon:ssi-sdk:migrations') +const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:migrations') export class CreateContacts1659463079429 implements MigrationInterface { name = 'CreateContacts1659463079429' public async up(queryRunner: QueryRunner): Promise { debug('migration: creating contacts tables') - const dbType = queryRunner.connection.driver.options.type - if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateContacts1659463079428() - const up = await mig.up(queryRunner) - debug('Migration statements executed') - return up - } else if (dbType === 'sqlite' || 'react-native') { - debug('using sqlite/react-native migration file') - const mig = new CreateContacts1659463069549() - const up = await mig.up(queryRunner) - debug('Migration statements executed') - return up - } else { - return Promise.reject( - "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" - ) + const dbType: DatabaseType = queryRunner.connection.driver.options.type + + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateContacts1659463079428 = new CreateContacts1659463079428() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateContacts1659463069549 = new CreateContacts1659463069549() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) } } public async down(queryRunner: QueryRunner): Promise { debug('migration: reverting contacts tables') - const dbType = queryRunner.connection.driver.options.type - if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateContacts1659463079428() - const down = await mig.down(queryRunner) - debug('Migration statements executed') - return down - } else if (dbType === 'sqlite' || 'react-native') { - debug('using sqlite/react-native migration file') - const mig = new CreateContacts1659463069549() - const down = await mig.down(queryRunner) - debug('Migration statements executed') - return down - } else { - return Promise.reject( - "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" - ) + const dbType: DatabaseType = queryRunner.connection.driver.options.type + + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateContacts1659463079428 = new CreateContacts1659463079428() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateContacts1659463069549 = new CreateContacts1659463069549() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) } } } diff --git a/packages/data-store/src/migrations/generic/2-CreateContacts.ts b/packages/data-store/src/migrations/generic/2-CreateContacts.ts new file mode 100644 index 000000000..90c913404 --- /dev/null +++ b/packages/data-store/src/migrations/generic/2-CreateContacts.ts @@ -0,0 +1,64 @@ +import { DatabaseType, MigrationInterface, QueryRunner } from 'typeorm' +import Debug from 'debug' +import { CreateContacts1690925872693 } from '../sqlite/1690925872693-CreateContacts' +import { CreateContacts1690925872592 } from '../postgres/1690925872592-CreateContacts' + +const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:migrations') + +export class CreateContacts1690925872318 implements MigrationInterface { + name = 'CreateContacts1690925872318' + + public async up(queryRunner: QueryRunner): Promise { + debug('migration: creating contacts tables') + const dbType: DatabaseType = queryRunner.connection.driver.options.type + + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateContacts1690925872592 = new CreateContacts1690925872592() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateContacts1690925872693 = new CreateContacts1690925872693() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) + } + } + + public async down(queryRunner: QueryRunner): Promise { + debug('migration: reverting contacts tables') + const dbType: DatabaseType = queryRunner.connection.driver.options.type + + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateContacts1690925872592 = new CreateContacts1690925872592() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateContacts1690925872693 = new CreateContacts1690925872693() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) + } + } +} diff --git a/packages/data-store/src/migrations/generic/2-CreateIssuanceBranding.ts b/packages/data-store/src/migrations/generic/2-CreateIssuanceBranding.ts index 9453ddf1b..b730d083c 100644 --- a/packages/data-store/src/migrations/generic/2-CreateIssuanceBranding.ts +++ b/packages/data-store/src/migrations/generic/2-CreateIssuanceBranding.ts @@ -1,54 +1,62 @@ -import { MigrationInterface, QueryRunner } from 'typeorm' +import { DatabaseType, MigrationInterface, QueryRunner } from 'typeorm' import Debug from 'debug' import { CreateIssuanceBranding1685628974232 } from '../postgres/1685628974232-CreateIssuanceBranding' import { CreateIssuanceBranding1685628973231 } from '../sqlite/1685628973231-CreateIssuanceBranding' -const debug = Debug('sphereon:ssi-sdk:migrations') +const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:migrations') export class CreateIssuanceBranding1659463079429 implements MigrationInterface { name = 'CreateIssuanceBranding1659463079429' public async up(queryRunner: QueryRunner): Promise { debug('migration: creating issuance branding tables') - const dbType = queryRunner.connection.driver.options.type - if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateIssuanceBranding1685628974232() - const up = await mig.up(queryRunner) - debug('Migration statements executed') - return up - } else if (dbType === 'sqlite' || 'react-native') { - debug('using sqlite/react-native migration file') - const mig = new CreateIssuanceBranding1685628973231() - const up = await mig.up(queryRunner) - debug('Migration statements executed') - return up - } else { - return Promise.reject( - "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" - ) + const dbType: DatabaseType = queryRunner.connection.driver.options.type + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateIssuanceBranding1685628974232 = new CreateIssuanceBranding1685628974232() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateIssuanceBranding1685628973231 = new CreateIssuanceBranding1685628973231() + await mig.up(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) } } public async down(queryRunner: QueryRunner): Promise { debug('migration: reverting issuance branding tables') - const dbType = queryRunner.connection.driver.options.type - if (dbType === 'postgres') { - debug('using postgres migration file') - const mig = new CreateIssuanceBranding1685628974232() - const down = await mig.down(queryRunner) - debug('Migration statements executed') - return down - } else if (dbType === 'sqlite' || 'react-native') { - debug('using sqlite/react-native migration file') - const mig = new CreateIssuanceBranding1685628973231() - const down = await mig.down(queryRunner) - debug('Migration statements executed') - return down - } else { - return Promise.reject( - "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" - ) + const dbType: DatabaseType = queryRunner.connection.driver.options.type + switch (dbType) { + case 'postgres': { + debug('using postgres migration file') + const mig: CreateIssuanceBranding1685628974232 = new CreateIssuanceBranding1685628974232() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + case 'sqlite': + case 'react-native': { + debug('using sqlite/react-native migration file') + const mig: CreateIssuanceBranding1685628973231 = new CreateIssuanceBranding1685628973231() + await mig.down(queryRunner) + debug('Migration statements executed') + return + } + default: + return Promise.reject( + "Migrations are currently only supported for sqlite, react-native and postgres. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now" + ) } } } diff --git a/packages/data-store/src/migrations/generic/index.ts b/packages/data-store/src/migrations/generic/index.ts index 8ed548f52..dbfce8eac 100644 --- a/packages/data-store/src/migrations/generic/index.ts +++ b/packages/data-store/src/migrations/generic/index.ts @@ -1,4 +1,5 @@ import { CreateContacts1659463079429 } from './1-CreateContacts' +import { CreateContacts1690925872318 } from './2-CreateContacts' import { CreateIssuanceBranding1659463079429 } from './2-CreateIssuanceBranding' import { CreateStatusList1693866470000 } from './3-CreateStatusList' @@ -11,9 +12,9 @@ import { CreateStatusList1693866470000 } from './3-CreateStatusList' */ // Individual migrations per purpose. Allows parties to not run migrations and thus create/update tables if they are not using a particular feature (yet) -export const DataStoreContactMigrations = [CreateContacts1659463079429] +export const DataStoreContactMigrations = [CreateContacts1659463079429, CreateContacts1690925872318] export const DataStoreIssuanceBrandingMigrations = [CreateIssuanceBranding1659463079429] export const DataStoreStatusListMigrations = [CreateStatusList1693866470000] // All migrations together -export const DataStoreMigrations = [CreateContacts1659463079429, CreateIssuanceBranding1659463079429, CreateStatusList1693866470000] +export const DataStoreMigrations = [...DataStoreContactMigrations, ...DataStoreIssuanceBrandingMigrations, ...DataStoreStatusListMigrations] diff --git a/packages/data-store/src/migrations/internal-migrations-ormconfig.ts b/packages/data-store/src/migrations/internal-migrations-ormconfig.ts index 69f15b773..27de527de 100644 --- a/packages/data-store/src/migrations/internal-migrations-ormconfig.ts +++ b/packages/data-store/src/migrations/internal-migrations-ormconfig.ts @@ -1,4 +1,4 @@ -import { ConnectionOptions } from 'typeorm' +import { DataSourceOptions } from 'typeorm' import { DataStoreContactEntities, DataStoreMigrations } from '../index' /** @@ -7,7 +7,6 @@ import { DataStoreContactEntities, DataStoreMigrations } from '../index' export default [ { type: 'sqlite', - name: 'migration-sqlite', database: 'migration.sqlite', migrationsRun: false, synchronize: false, @@ -17,7 +16,6 @@ export default [ }, { type: 'postgres', - name: 'migration-postgres', database: 'migration-postgres', migrationsRun: false, synchronize: false, @@ -25,4 +23,4 @@ export default [ entities: [...DataStoreContactEntities], migrations: [...DataStoreMigrations], }, -] as ConnectionOptions[] +] as DataSourceOptions[] diff --git a/packages/data-store/src/migrations/postgres/1690925872592-CreateContacts.ts b/packages/data-store/src/migrations/postgres/1690925872592-CreateContacts.ts new file mode 100644 index 000000000..b928cb5cc --- /dev/null +++ b/packages/data-store/src/migrations/postgres/1690925872592-CreateContacts.ts @@ -0,0 +1,94 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' +import { enableUuidv4 } from './uuid' + +export class CreateContacts1690925872592 implements MigrationInterface { + name = 'CreateContacts1690925872592' + + public async up(queryRunner: QueryRunner): Promise { + await enableUuidv4(queryRunner) + await queryRunner.query(`ALTER TABLE "CorrelationIdentifier" DROP CONSTRAINT "FK_CorrelationIdentifier_identityId"`) + await queryRunner.query(`ALTER TABLE "IdentityMetadata" DROP CONSTRAINT "FK_IdentityMetadata_identityId"`) + await queryRunner.query(`ALTER TABLE "Identity" DROP CONSTRAINT "FK_Identity_contactId"`) + await queryRunner.query(`ALTER TABLE "Connection" DROP CONSTRAINT "FK_Connection_identityId"`) + await queryRunner.query(`ALTER TABLE "CorrelationIdentifier" RENAME COLUMN "identityId" TO "identity_id"`) + await queryRunner.query(`ALTER TABLE "Connection" RENAME COLUMN "identityId" TO "identity_id"`) + await queryRunner.query(`CREATE TYPE "public"."PartyType_type_enum" AS ENUM('naturalPerson', 'organization')`) + await queryRunner.query( + `CREATE TABLE "PartyType" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" "public"."PartyType_type_enum" NOT NULL, "name" character varying(255) NOT NULL, "description" character varying(255), "tenant_id" character varying(255) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_PartyType_name" UNIQUE ("name"), CONSTRAINT "PK_PartyType_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_PartyType_type_tenant_id" ON "PartyType" ("type", "tenant_id")`) + await queryRunner.query( + `CREATE TABLE "BaseContact" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), "legal_name" character varying(255), "display_name" character varying(255), "first_name" character varying(255), "middle_name" character varying(255), "last_name" character varying(255), "type" character varying NOT NULL, "party_id" uuid, CONSTRAINT "UQ_BaseContact_legal_name" UNIQUE ("legal_name"), CONSTRAINT "REL_BaseContact_party_id" UNIQUE ("party_id"), CONSTRAINT "PK_BaseContact_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`CREATE INDEX "IDX_BaseContact_type" ON "BaseContact" ("type")`) + await queryRunner.query( + `CREATE TABLE "PartyRelationship" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "left_id" uuid NOT NULL, "right_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_PartyRelationship_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_PartyRelationship_left_right" ON "PartyRelationship" ("left_id", "right_id")`) + await queryRunner.query( + `CREATE TABLE "ElectronicAddress" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying(255) NOT NULL, "electronic_address" character varying(255) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), "partyId" uuid, CONSTRAINT "PK_ElectronicAddress_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `CREATE TABLE "Party" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "uri" character varying(255) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), "party_type_id" uuid NOT NULL, CONSTRAINT "PK_Party_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query( + `CREATE TABLE "BaseConfig" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "identifier" character varying(255), "redirect_url" character varying(255), "session_id" character varying(255), "client_id" character varying(255), "client_secret" character varying(255), "scopes" text, "issuer" character varying(255), "dangerously_allow_insecure_http_requests" boolean, "client_auth_method" text, "type" character varying NOT NULL, "connection_id" uuid, CONSTRAINT "REL_BaseConfig_connection_id" UNIQUE ("connection_id"), CONSTRAINT "PK_BaseConfig_id" PRIMARY KEY ("id"))` + ) + await queryRunner.query(`CREATE INDEX "IDX_BaseConfig_type" ON "BaseConfig" ("type")`) + await queryRunner.query(`ALTER TABLE "Identity" RENAME COLUMN "contactId" TO "partyId"`) + await queryRunner.query(`ALTER TABLE "Identity" ALTER COLUMN "roles" SET NOT NULL`) + await queryRunner.query( + `ALTER TABLE "CorrelationIdentifier" ADD CONSTRAINT "FK_CorrelationIdentifier_identity_id" FOREIGN KEY ("identity_id") REFERENCES "Identity"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "IdentityMetadata" ADD CONSTRAINT "FK_IdentityMetadata_identityId" FOREIGN KEY ("identityId") REFERENCES "Identity"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "BaseContact" ADD CONSTRAINT "FK_BaseContact_party_id" FOREIGN KEY ("party_id") REFERENCES "Party"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "PartyRelationship" ADD CONSTRAINT "FK_PartyRelationship_left_id" FOREIGN KEY ("left_id") REFERENCES "Party"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "PartyRelationship" ADD CONSTRAINT "FK_PartyRelationship_right_id" FOREIGN KEY ("right_id") REFERENCES "Party"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "ElectronicAddress" ADD CONSTRAINT "FK_ElectronicAddress_partyId" FOREIGN KEY ("partyId") REFERENCES "Party"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "Party" ADD CONSTRAINT "FK_Party_party_type_id" FOREIGN KEY ("party_type_id") REFERENCES "PartyType"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "Identity" ADD CONSTRAINT "FK_Identity_partyId" FOREIGN KEY ("partyId") REFERENCES "Party"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "Connection" ADD CONSTRAINT "FK_Connection_identity_id" FOREIGN KEY ("identity_id") REFERENCES "Identity"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "BaseConfig" ADD CONSTRAINT "FK_BaseConfig_connection_id" FOREIGN KEY ("connection_id") REFERENCES "Connection"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ) + + // migrate existing data + await queryRunner.query(`ALTER TABLE "CorrelationIdentifier" RENAME CONSTRAINT "UQ_Correlation_id" TO "UQ_CorrelationIdentifier_correlation_id"`) + await queryRunner.query(`ALTER TABLE "Identity" RENAME CONSTRAINT "UQ_Identity_Alias" TO "UQ_Identity_alias"`) + await queryRunner.query( + `INSERT INTO "BaseConfig"("id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connection_id") SELECT "id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connectionId" FROM "BaseConfigEntity"` + ) + await queryRunner.query(`DROP TABLE "BaseConfigEntity"`) + await queryRunner.query( + `INSERT INTO "PartyType"(id, type, name, description, tenant_id, created_at, last_updated_at) VALUES ('3875c12e-fdaa-4ef6-a340-c936e054b627', 'organization', 'Sphereon_default_type', 'sphereon_default_organization', '95e09cfc-c974-4174-86aa-7bf1d5251fb4', now(), now())` + ) + await queryRunner.query( + `INSERT INTO "Party"(id, uri, created_at, last_updated_at, party_type_id) SELECT id, uri, created_at, last_updated_at, '3875c12e-fdaa-4ef6-a340-c936e054b627' FROM "Contact"` + ) + await queryRunner.query( + `INSERT INTO "BaseContact"(id, legal_name, display_name, party_id, created_at, last_updated_at, type) SELECT id, name, alias, id, created_at, last_updated_at, 'Organization' FROM "Contact"` + ) + await queryRunner.query(`DROP TABLE "Contact"`) + } + + public async down(queryRunner: QueryRunner): Promise { + // TODO DPP-27 implement downgrade + return Promise.reject(Error(`Downgrade is not yet implemented for ${this.name}`)) + } +} diff --git a/packages/data-store/src/migrations/sqlite/1690925872693-CreateContacts.ts b/packages/data-store/src/migrations/sqlite/1690925872693-CreateContacts.ts new file mode 100644 index 000000000..77df025f7 --- /dev/null +++ b/packages/data-store/src/migrations/sqlite/1690925872693-CreateContacts.ts @@ -0,0 +1,163 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class CreateContacts1690925872693 implements MigrationInterface { + name = 'CreateContacts1690925872693' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_CorrelationIdentifier" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('did','url') ) NOT NULL, "correlation_id" text NOT NULL, "identityId" varchar, CONSTRAINT "REL_CorrelationIdentifier_identityId" UNIQUE ("identityId"), CONSTRAINT "UQ_CorrelationIdentifier_correlation_id" UNIQUE ("correlation_id"))` + ) + await queryRunner.query( + `INSERT INTO "temporary_CorrelationIdentifier"("id", "type", "correlation_id", "identityId") SELECT "id", "type", "correlation_id", "identityId" FROM "CorrelationIdentifier"` + ) + await queryRunner.query(`DROP TABLE "CorrelationIdentifier"`) + await queryRunner.query(`ALTER TABLE "temporary_CorrelationIdentifier" RENAME TO "CorrelationIdentifier"`) + await queryRunner.query( + `CREATE TABLE "temporary_Identity" ("id" varchar PRIMARY KEY NOT NULL, "alias" varchar(255) NOT NULL, "roles" text NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "contactId" varchar, CONSTRAINT "UQ_Identity_alias" UNIQUE ("alias"))` + ) + await queryRunner.query( + `INSERT INTO "temporary_Identity"("id", "alias", "roles", "created_at", "last_updated_at", "contactId") SELECT "id", "alias", "roles", "created_at", "last_updated_at", "contactId" FROM "Identity"` + ) + await queryRunner.query(`DROP TABLE "Identity"`) + await queryRunner.query(`ALTER TABLE "temporary_Identity" RENAME TO "Identity"`) + await queryRunner.query( + `CREATE TABLE "temporary_Connection" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('OIDC','SIOPv2','SIOPv2+OpenID4VP') ) NOT NULL, "identityId" varchar, CONSTRAINT "REL_Connection_identityId" UNIQUE ("identityId"))` + ) + await queryRunner.query(`INSERT INTO "temporary_Connection"("id", "type", "identityId") SELECT "id", "type", "identityId" FROM "Connection"`) + await queryRunner.query(`DROP TABLE "Connection"`) + await queryRunner.query(`ALTER TABLE "temporary_Connection" RENAME TO "Connection"`) + await queryRunner.query( + `CREATE TABLE "temporary_CorrelationIdentifier" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('did','url') ) NOT NULL, "correlation_id" text NOT NULL, "identity_id" varchar, CONSTRAINT "REL_CorrelationIdentifier_identityId" UNIQUE ("identity_id"), CONSTRAINT "UQ_CorrelationIdentifier_correlation_id" UNIQUE ("correlation_id"))` + ) + await queryRunner.query( + `INSERT INTO "temporary_CorrelationIdentifier"("id", "type", "correlation_id", "identity_id") SELECT "id", "type", "correlation_id", "identityId" FROM "CorrelationIdentifier"` + ) + await queryRunner.query(`DROP TABLE "CorrelationIdentifier"`) + await queryRunner.query(`ALTER TABLE "temporary_CorrelationIdentifier" RENAME TO "CorrelationIdentifier"`) + await queryRunner.query( + `CREATE TABLE "temporary_Identity" ("id" varchar PRIMARY KEY NOT NULL, "alias" varchar(255) NOT NULL, "roles" text NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "partyId" varchar, CONSTRAINT "UQ_Identity_alias" UNIQUE ("alias"))` + ) + await queryRunner.query( + `INSERT INTO "temporary_Identity"("id", "alias", "roles", "created_at", "last_updated_at", "partyId") SELECT "id", "alias", "roles", "created_at", "last_updated_at", "contactId" FROM "Identity"` + ) + await queryRunner.query(`DROP TABLE "Identity"`) + await queryRunner.query(`ALTER TABLE "temporary_Identity" RENAME TO "Identity"`) + await queryRunner.query( + `CREATE TABLE "temporary_Connection" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('OIDC','SIOPv2','SIOPv2+OpenID4VP') ) NOT NULL, "identity_id" varchar, CONSTRAINT "REL_Connection_identityId" UNIQUE ("identity_id"))` + ) + await queryRunner.query(`INSERT INTO "temporary_Connection"("id", "type", "identity_id") SELECT "id", "type", "identityId" FROM "Connection"`) + await queryRunner.query(`DROP TABLE "Connection"`) + await queryRunner.query(`ALTER TABLE "temporary_Connection" RENAME TO "Connection"`) + await queryRunner.query( + `CREATE TABLE "PartyType" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('naturalPerson','organization') ) NOT NULL, "name" varchar(255) NOT NULL, "description" varchar(255), "tenant_id" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_PartyType_name" UNIQUE ("name"))` + ) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_PartyType_type_tenant_id" ON "PartyType" ("type", "tenant_id")`) + await queryRunner.query( + `CREATE TABLE "BaseContact" ("id" varchar PRIMARY KEY NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "legal_name" varchar(255), "display_name" varchar(255), "first_name" varchar(255), "middle_name" varchar(255), "last_name" varchar(255), "type" varchar NOT NULL, "party_id" varchar, CONSTRAINT "UQ_BaseContact_legal_name" UNIQUE ("legal_name"), CONSTRAINT "REL_BaseContact_party_id" UNIQUE ("party_id"))` + ) + await queryRunner.query(`CREATE INDEX "IDX_BaseContact_type" ON "BaseContact" ("type")`) + await queryRunner.query( + `CREATE TABLE "PartyRelationship" ("id" varchar PRIMARY KEY NOT NULL, "left_id" varchar NOT NULL, "right_id" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')))` + ) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_PartyRelationship_left_right" ON "PartyRelationship" ("left_id", "right_id")`) + await queryRunner.query( + `CREATE TABLE "ElectronicAddress" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar(255) NOT NULL, "electronic_address" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "partyId" varchar)` + ) + await queryRunner.query( + `CREATE TABLE "Party" ("id" varchar PRIMARY KEY NOT NULL, "uri" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "party_type_id" varchar NOT NULL)` + ) + await queryRunner.query( + `CREATE TABLE "BaseConfig" ("id" varchar PRIMARY KEY NOT NULL, "identifier" varchar(255), "redirect_url" varchar(255), "session_id" varchar(255), "client_id" varchar(255), "client_secret" varchar(255), "scopes" text, "issuer" varchar(255), "dangerously_allow_insecure_http_requests" boolean, "client_auth_method" text, "type" varchar NOT NULL, "connection_id" varchar, CONSTRAINT "REL_BaseConfig_connection_id" UNIQUE ("connection_id"))` + ) + await queryRunner.query(`CREATE INDEX "IDX_BaseConfig_type" ON "BaseConfig" ("type")`) + await queryRunner.query( + `CREATE TABLE "temporary_CorrelationIdentifier" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('did','url') ) NOT NULL, "correlation_id" text NOT NULL, "identity_id" varchar, CONSTRAINT "REL_CorrelationIdentifier_identity_id" UNIQUE ("identity_id"), CONSTRAINT "UQ_CorrelationIdentifier_correlation_id" UNIQUE ("correlation_id"), CONSTRAINT "FK_CorrelationIdentifier_identity_id" FOREIGN KEY ("identity_id") REFERENCES "Identity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_CorrelationIdentifier"("id", "type", "correlation_id", "identity_id") SELECT "id", "type", "correlation_id", "identity_id" FROM "CorrelationIdentifier"` + ) + await queryRunner.query(`DROP TABLE "CorrelationIdentifier"`) + await queryRunner.query(`ALTER TABLE "temporary_CorrelationIdentifier" RENAME TO "CorrelationIdentifier"`) + await queryRunner.query(`DROP INDEX "IDX_BaseContact_type"`) + await queryRunner.query( + `CREATE TABLE "temporary_BaseContact" ("id" varchar PRIMARY KEY NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "legal_name" varchar(255), "display_name" varchar(255), "first_name" varchar(255), "middle_name" varchar(255), "last_name" varchar(255), "type" varchar NOT NULL, "party_id" varchar, CONSTRAINT "UQ_BaseContact_legal_name" UNIQUE ("legal_name"), CONSTRAINT "REL_BaseContact_party_id" UNIQUE ("party_id"), CONSTRAINT "FK_BaseContact_party_id" FOREIGN KEY ("party_id") REFERENCES "Party" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_BaseContact"("id", "created_at", "last_updated_at", "legal_name", "display_name", "first_name", "middle_name", "last_name", "type", "party_id") SELECT "id", "created_at", "last_updated_at", "legal_name", "display_name", "first_name", "middle_name", "last_name", "type", "party_id" FROM "BaseContact"` + ) + await queryRunner.query(`DROP TABLE "BaseContact"`) + await queryRunner.query(`ALTER TABLE "temporary_BaseContact" RENAME TO "BaseContact"`) + await queryRunner.query(`CREATE INDEX "IDX_BaseContact_type" ON "BaseContact" ("type")`) + await queryRunner.query(`DROP INDEX "IDX_PartyRelationship_left_right"`) + await queryRunner.query( + `CREATE TABLE "temporary_PartyRelationship" ("id" varchar PRIMARY KEY NOT NULL, "left_id" varchar NOT NULL, "right_id" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "FK_PartyRelationship_left_id" FOREIGN KEY ("left_id") REFERENCES "Party" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_PartyRelationship_right_id" FOREIGN KEY ("right_id") REFERENCES "Party" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_PartyRelationship"("id", "left_id", "right_id", "created_at", "last_updated_at") SELECT "id", "left_id", "right_id", "created_at", "last_updated_at" FROM "PartyRelationship"` + ) + await queryRunner.query(`DROP TABLE "PartyRelationship"`) + await queryRunner.query(`ALTER TABLE "temporary_PartyRelationship" RENAME TO "PartyRelationship"`) + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_PartyRelationship_left_right" ON "PartyRelationship" ("left_id", "right_id")`) + await queryRunner.query( + `CREATE TABLE "temporary_ElectronicAddress" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar(255) NOT NULL, "electronic_address" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "partyId" varchar, CONSTRAINT "FK_ElectronicAddress_partyId" FOREIGN KEY ("partyId") REFERENCES "Party" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_ElectronicAddress"("id", "type", "electronic_address", "created_at", "last_updated_at", "partyId") SELECT "id", "type", "electronic_address", "created_at", "last_updated_at", "partyId" FROM "ElectronicAddress"` + ) + await queryRunner.query(`DROP TABLE "ElectronicAddress"`) + await queryRunner.query(`ALTER TABLE "temporary_ElectronicAddress" RENAME TO "ElectronicAddress"`) + await queryRunner.query( + `CREATE TABLE "temporary_Party" ("id" varchar PRIMARY KEY NOT NULL, "uri" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "party_type_id" varchar NOT NULL, CONSTRAINT "FK_Party_party_type_id" FOREIGN KEY ("party_type_id") REFERENCES "PartyType" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_Party"("id", "uri", "created_at", "last_updated_at", "party_type_id") SELECT "id", "uri", "created_at", "last_updated_at", "party_type_id" FROM "Party"` + ) + await queryRunner.query(`DROP TABLE "Party"`) + await queryRunner.query(`ALTER TABLE "temporary_Party" RENAME TO "Party"`) + await queryRunner.query( + `CREATE TABLE "temporary_Identity" ("id" varchar PRIMARY KEY NOT NULL, "alias" varchar(255) NOT NULL, "roles" text NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), "partyId" varchar, CONSTRAINT "UQ_Identity_alias" UNIQUE ("alias"), CONSTRAINT "FK_Identity_partyId" FOREIGN KEY ("partyId") REFERENCES "Party" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_Identity"("id", "alias", "roles", "created_at", "last_updated_at", "partyId") SELECT "id", "alias", "roles", "created_at", "last_updated_at", "partyId" FROM "Identity"` + ) + await queryRunner.query(`DROP TABLE "Identity"`) + await queryRunner.query(`ALTER TABLE "temporary_Identity" RENAME TO "Identity"`) + await queryRunner.query( + `CREATE TABLE "temporary_Connection" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar CHECK( "type" IN ('OIDC','SIOPv2','SIOPv2+OpenID4VP') ) NOT NULL, "identity_id" varchar, CONSTRAINT "REL_Connection_identity_id" UNIQUE ("identity_id"), CONSTRAINT "FK_Connection_identity_id" FOREIGN KEY ("identity_id") REFERENCES "Identity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query(`INSERT INTO "temporary_Connection"("id", "type", "identity_id") SELECT "id", "type", "identity_id" FROM "Connection"`) + await queryRunner.query(`DROP TABLE "Connection"`) + await queryRunner.query(`ALTER TABLE "temporary_Connection" RENAME TO "Connection"`) + await queryRunner.query(`DROP INDEX "IDX_BaseConfig_type"`) + await queryRunner.query( + `CREATE TABLE "temporary_BaseConfig" ("id" varchar PRIMARY KEY NOT NULL, "identifier" varchar(255), "redirect_url" varchar(255), "session_id" varchar(255), "client_id" varchar(255), "client_secret" varchar(255), "scopes" text, "issuer" varchar(255), "dangerously_allow_insecure_http_requests" boolean, "client_auth_method" text, "type" varchar NOT NULL, "connection_id" varchar, CONSTRAINT "REL_BaseConfig_connection_id" UNIQUE ("connection_id"), CONSTRAINT "FK_BaseConfig_connection_id" FOREIGN KEY ("connection_id") REFERENCES "Connection" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ) + await queryRunner.query( + `INSERT INTO "temporary_BaseConfig"("id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connection_id") SELECT "id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connection_id" FROM "BaseConfig"` + ) + await queryRunner.query(`DROP TABLE "BaseConfig"`) + await queryRunner.query(`ALTER TABLE "temporary_BaseConfig" RENAME TO "BaseConfig"`) + await queryRunner.query(`CREATE INDEX "IDX_BaseConfig_type" ON "BaseConfig" ("type")`) + + // migrate existing data + await queryRunner.query( + `INSERT INTO "BaseConfig"("id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connection_id") SELECT "id", "identifier", "redirect_url", "session_id", "client_id", "client_secret", "scopes", "issuer", "dangerously_allow_insecure_http_requests", "client_auth_method", "type", "connection_id" FROM "BaseConfigEntity"` + ) + await queryRunner.query(`DROP TABLE "BaseConfigEntity"`) + await queryRunner.query( + `INSERT INTO "PartyType"(id, type, name, description, tenant_id, created_at, last_updated_at) VALUES ('3875c12e-fdaa-4ef6-a340-c936e054b627', 'organization', 'Sphereon_default_type', 'sphereon_default_organization', '95e09cfc-c974-4174-86aa-7bf1d5251fb4', datetime('now'), datetime('now'))` + ) + await queryRunner.query( + `INSERT INTO "Party"(id, uri, created_at, last_updated_at, party_type_id) SELECT id, uri, created_at, last_updated_at, '3875c12e-fdaa-4ef6-a340-c936e054b627' FROM "Contact"` + ) + await queryRunner.query( + `INSERT INTO "BaseContact"(id, legal_name, display_name, party_id, created_at, last_updated_at, type) SELECT id, name, alias, id, created_at, last_updated_at, 'Organization' FROM "Contact"` + ) + await queryRunner.query(`DROP TABLE "Contact"`) + } + + public async down(queryRunner: QueryRunner): Promise { + // TODO DPP-27 implement downgrade + return Promise.reject(Error(`Downgrade is not yet implemented for ${this.name}`)) + } +} diff --git a/packages/data-store/src/types/contact/IAbstractContactStore.ts b/packages/data-store/src/types/contact/IAbstractContactStore.ts index b9b26435d..dc5e413cc 100644 --- a/packages/data-store/src/types/contact/IAbstractContactStore.ts +++ b/packages/data-store/src/types/contact/IAbstractContactStore.ts @@ -1,52 +1,109 @@ -import { FindOptionsWhere } from 'typeorm' -import { ContactEntity } from '../../entities/contact/ContactEntity' -import { IdentityEntity } from '../../entities/contact/IdentityEntity' -import { IBasicIdentity, IContact, IIdentity } from './contact' +import { + NonPersistedPartyType, + NonPersistedContact, + NonPersistedIdentity, + Party, + Identity, + PartialParty, + PartialIdentity, + PartyTypeEnum, + PartyType, + PartyRelationship, + PartialPartyRelationship, + PartialPartyType, + NonPersistedElectronicAddress, +} from './contact' -// TODO WAL-625 refactor types to use interfaces and not the entities as the store should be replaceable -export type FindContactArgs = FindOptionsWhere[] -export type FindIdentityArgs = FindOptionsWhere[] +export type FindPartyArgs = Array +export type FindIdentityArgs = Array +export type FindPartyTypeArgs = Array +export type FindRelationshipArgs = Array -export interface IGetContactArgs { - contactId: string +export type GetPartyArgs = { + partyId: string } -export interface IGetContactsArgs { - filter?: FindContactArgs +export type GetPartiesArgs = { + filter?: FindPartyArgs } -export interface IAddContactArgs { - name: string - alias: string +export type AddPartyArgs = { uri?: string - identities?: Array + partyType: NonPersistedPartyType + contact: NonPersistedContact + identities?: Array + electronicAddresses?: Array } -export interface IUpdateContactArgs { - contact: Omit +export type UpdatePartyArgs = { + party: Omit } -export interface IRemoveContactArgs { - contactId: string +export type RemovePartyArgs = { + partyId: string } -export interface IGetIdentityArgs { +export type GetIdentityArgs = { identityId: string } -export interface IGetIdentitiesArgs { +export type GetIdentitiesArgs = { filter?: FindIdentityArgs } -export interface IAddIdentityArgs { - contactId: string - identity: IBasicIdentity +export type AddIdentityArgs = { + partyId: string + identity: NonPersistedIdentity } -export interface IUpdateIdentityArgs { - identity: IIdentity +export type UpdateIdentityArgs = { + identity: Identity } -export interface IRemoveIdentityArgs { +export type RemoveIdentityArgs = { identityId: string } + +export type RemoveRelationshipArgs = { + relationshipId: string +} + +export type AddRelationshipArgs = { + leftId: string + rightId: string +} + +export type GetRelationshipArgs = { + relationshipId: string +} + +export type GetRelationshipsArgs = { + filter: FindRelationshipArgs +} + +export type UpdateRelationshipArgs = { + relationship: Omit +} + +export type AddPartyTypeArgs = { + type: PartyTypeEnum + name: string + tenantId: string + description?: string +} + +export type GetPartyTypeArgs = { + partyTypeId: string +} + +export type GetPartyTypesArgs = { + filter?: FindPartyTypeArgs +} + +export type UpdatePartyTypeArgs = { + partyType: Omit +} + +export type RemovePartyTypeArgs = { + partyTypeId: string +} diff --git a/packages/data-store/src/types/contact/contact.ts b/packages/data-store/src/types/contact/contact.ts index 33430947c..1024ddf9d 100644 --- a/packages/data-store/src/types/contact/contact.ts +++ b/packages/data-store/src/types/contact/contact.ts @@ -1,82 +1,87 @@ import { IIdentifier } from '@veramo/core' -export enum IdentityRoleEnum { - ISSUER = 'issuer', - VERIFIER = 'verifier', - HOLDER = 'holder', -} - -export enum ConnectionTypeEnum { - OPENID_CONNECT = 'OIDC', - SIOPv2 = 'SIOPv2', - SIOPv2_OpenID4VP = 'SIOPv2+OpenID4VP', -} - -export enum CorrelationIdentifierEnum { - DID = 'did', - URL = 'url', -} - -export interface IContact { +export type Party = { id: string - name: string - alias: string uri?: string roles: Array - identities: Array + identities: Array + electronicAddresses: Array + contact: Contact + partyType: PartyType + relationships: Array createdAt: Date lastUpdatedAt: Date } -export interface IBasicContact { - name: string - alias: string - uri?: string - identities?: Array +export type NonPersistedParty = Omit< + Party, + 'id' | 'identities' | 'electronicAddresses' | 'contact' | 'roles' | 'partyType' | 'relationships' | 'createdAt' | 'lastUpdatedAt' +> & { + identities?: Array + electronicAddresses?: Array + contact: NonPersistedContact + partyType: NonPersistedPartyType + relationships?: Array +} +export type PartialParty = Partial> & { + identities?: PartialIdentity + electronicAddresses?: PartialElectronicAddress + contact?: PartialContact + partyType?: PartialPartyType + relationships?: PartialPartyRelationship } -export interface IIdentity { +export type Identity = { id: string alias: string roles: Array - identifier: ICorrelationIdentifier - connection?: IConnection - metadata?: Array + identifier: CorrelationIdentifier + connection?: Connection + metadata?: Array createdAt: Date lastUpdatedAt: Date } -export interface IBasicIdentity { - alias: string - roles: Array - identifier: BasicCorrelationIdentifier - connection?: IBasicConnection - metadata?: Array +export type NonPersistedIdentity = Omit & { + identifier: NonPersistedCorrelationIdentifier + connection?: NonPersistedConnection + metadata?: Array +} +export type PartialIdentity = Partial> & { + identifier?: PartialCorrelationIdentifier + connection?: PartialConnection + metadata?: PartialMetadataItem + roles?: IdentityRoleEnum + partyId?: string } -export interface IMetadataItem { +export type MetadataItem = { id: string label: string value: string } -export declare type BasicMetadataItem = Omit +export type NonPersistedMetadataItem = Omit +export type PartialMetadataItem = Partial -export interface ICorrelationIdentifier { +export type CorrelationIdentifier = { id: string type: CorrelationIdentifierEnum correlationId: string } -export declare type BasicCorrelationIdentifier = Omit +export type NonPersistedCorrelationIdentifier = Omit +export type PartialCorrelationIdentifier = Partial -export interface IConnection { +export type Connection = { id: string type: ConnectionTypeEnum config: ConnectionConfig } -export interface IBasicConnection { - type: ConnectionTypeEnum - config: BasicConnectionConfig +export type NonPersistedConnection = Omit & { + config: NonPersistedConnectionConfig +} +export type PartialConnection = Partial> & { + config: PartialConnectionConfig } -export interface IOpenIdConfig { +export type OpenIdConfig = { id: string clientId: string clientSecret: string @@ -86,16 +91,105 @@ export interface IOpenIdConfig { dangerouslyAllowInsecureHttpRequests: boolean clientAuthMethod: 'basic' | 'post' | undefined } -export declare type BasicOpenIdConfig = Omit +export type NonPersistedOpenIdConfig = Omit +export type PartialOpenIdConfig = Partial -export interface IDidAuthConfig { +export type DidAuthConfig = { id: string identifier: IIdentifier stateId: string redirectUrl: string sessionId: string } -export declare type BasicDidAuthConfig = Omit +export type NonPersistedDidAuthConfig = Omit +export type PartialDidAuthConfig = Partial> & { + identifier: Partial // TODO, we need to create partials for sub types in IIdentifier +} + +export type ConnectionConfig = OpenIdConfig | DidAuthConfig +export type NonPersistedConnectionConfig = NonPersistedDidAuthConfig | NonPersistedOpenIdConfig +export type PartialConnectionConfig = PartialOpenIdConfig | PartialDidAuthConfig -export declare type ConnectionConfig = IOpenIdConfig | IDidAuthConfig -export declare type BasicConnectionConfig = BasicDidAuthConfig | BasicOpenIdConfig +export type NaturalPerson = { + id: string + firstName: string + lastName: string + middleName?: string + displayName: string + createdAt: Date + lastUpdatedAt: Date +} +export type NonPersistedNaturalPerson = Omit +export type PartialNaturalPerson = Partial + +export type Organization = { + id: string + legalName: string + displayName: string + createdAt: Date + lastUpdatedAt: Date +} +export type NonPersistedOrganization = Omit +export type PartialOrganization = Partial + +export type Contact = NaturalPerson | Organization +export type NonPersistedContact = NonPersistedNaturalPerson | NonPersistedOrganization +export type PartialContact = PartialNaturalPerson | PartialOrganization + +export type PartyType = { + id: string + type: PartyTypeEnum + name: string + tenantId: string + description?: string + createdAt: Date + lastUpdatedAt: Date +} +export type NonPersistedPartyType = Omit & { + id?: string +} +export type PartialPartyType = Partial + +export type PartyRelationship = { + id: string + leftId: string + rightId: string + createdAt: Date + lastUpdatedAt: Date +} +export type NonPersistedPartyRelationship = Omit +export type PartialPartyRelationship = Partial + +export type ElectronicAddress = { + id: string + type: ElectronicAddressType + electronicAddress: string + createdAt: Date + lastUpdatedAt: Date +} +export type NonPersistedElectronicAddress = Omit +export type PartialElectronicAddress = Partial + +export type ElectronicAddressType = 'email' + +export enum IdentityRoleEnum { + ISSUER = 'issuer', + VERIFIER = 'verifier', + HOLDER = 'holder', +} + +export enum ConnectionTypeEnum { + OPENID_CONNECT = 'OIDC', + SIOPv2 = 'SIOPv2', + SIOPv2_OpenID4VP = 'SIOPv2+OpenID4VP', +} + +export enum CorrelationIdentifierEnum { + DID = 'did', + URL = 'url', +} + +export enum PartyTypeEnum { + NATURAL_PERSON = 'naturalPerson', + ORGANIZATION = 'organization', +} diff --git a/packages/data-store/src/types/index.ts b/packages/data-store/src/types/index.ts index 373085113..f74232faf 100644 --- a/packages/data-store/src/types/index.ts +++ b/packages/data-store/src/types/index.ts @@ -2,5 +2,7 @@ export * from './issuanceBranding/issuanceBranding' export * from './issuanceBranding/IAbstractIssuanceBrandingStore' export * from './contact/contact' export * from './contact/IAbstractContactStore' + +export * from './validation/validation' export * from './statusList/statusList' export * from './statusList/IAbstractStatusListStore' diff --git a/packages/data-store/src/types/validation/validation.ts b/packages/data-store/src/types/validation/validation.ts new file mode 100644 index 000000000..e41d161f7 --- /dev/null +++ b/packages/data-store/src/types/validation/validation.ts @@ -0,0 +1,3 @@ +export type ValidationConstraint = { + [x: string]: string +} diff --git a/packages/data-store/src/utils/ValidatorUtils.ts b/packages/data-store/src/utils/ValidatorUtils.ts new file mode 100644 index 000000000..ab7bcf659 --- /dev/null +++ b/packages/data-store/src/utils/ValidatorUtils.ts @@ -0,0 +1,10 @@ +import { ValidationError } from 'class-validator' +import { ValidationConstraint } from '../types' + +export const getConstraint = (validation: ValidationError): ValidationConstraint | undefined => { + if (validation.children && validation.children.length > 0) { + return getConstraint(validation.children[0]) + } else { + return validation.constraints + } +} diff --git a/packages/data-store/src/utils/contact/MappingUtils.ts b/packages/data-store/src/utils/contact/MappingUtils.ts new file mode 100644 index 000000000..1a4baa101 --- /dev/null +++ b/packages/data-store/src/utils/contact/MappingUtils.ts @@ -0,0 +1,344 @@ +import { + Connection, + ConnectionConfig, + Contact, + CorrelationIdentifier, + DidAuthConfig, + ElectronicAddress, + Identity, + MetadataItem, + NaturalPerson, + NonPersistedConnection, + NonPersistedConnectionConfig, + NonPersistedContact, + NonPersistedCorrelationIdentifier, + NonPersistedDidAuthConfig, + NonPersistedElectronicAddress, + NonPersistedIdentity, + NonPersistedMetadataItem, + NonPersistedNaturalPerson, + NonPersistedOpenIdConfig, + NonPersistedOrganization, + NonPersistedParty, + NonPersistedPartyRelationship, + NonPersistedPartyType, + OpenIdConfig, + Organization, + Party, + PartyRelationship, + PartyType, +} from '../../types' +import { PartyEntity } from '../../entities/contact/PartyEntity' +import { IdentityEntity } from '../../entities/contact/IdentityEntity' +import { ElectronicAddressEntity } from '../../entities/contact/ElectronicAddressEntity' +import { PartyRelationshipEntity } from '../../entities/contact/PartyRelationshipEntity' +import { BaseContactEntity } from '../../entities/contact/BaseContactEntity' +import { NaturalPersonEntity } from '../../entities/contact/NaturalPersonEntity' +import { OrganizationEntity } from '../../entities/contact/OrganizationEntity' +import { ConnectionEntity } from '../../entities/contact/ConnectionEntity' +import { BaseConfigEntity } from '../../entities/contact/BaseConfigEntity' +import { CorrelationIdentifierEntity } from '../../entities/contact/CorrelationIdentifierEntity' +import { DidAuthConfigEntity } from '../../entities/contact/DidAuthConfigEntity' +import { IdentityMetadataItemEntity } from '../../entities/contact/IdentityMetadataItemEntity' +import { OpenIdConfigEntity } from '../../entities/contact/OpenIdConfigEntity' +import { PartyTypeEntity } from '../../entities/contact/PartyTypeEntity' + +export const partyEntityFrom = (party: NonPersistedParty): PartyEntity => { + const partyEntity: PartyEntity = new PartyEntity() + partyEntity.uri = party.uri + partyEntity.identities = party.identities ? party.identities.map((identity: NonPersistedIdentity) => identityEntityFrom(identity)) : [] + partyEntity.electronicAddresses = party.electronicAddresses + ? party.electronicAddresses.map((electronicAddress: NonPersistedElectronicAddress) => electronicAddressEntityFrom(electronicAddress)) + : [] + partyEntity.partyType = partyTypeEntityFrom(party.partyType) + partyEntity.contact = contactEntityFrom(party.contact) + + return partyEntity +} + +export const partyFrom = (party: PartyEntity): Party => { + return { + id: party.id, + uri: party.uri, + roles: [...new Set(party.identities?.flatMap((identity: IdentityEntity) => identity.roles))] ?? [], + identities: party.identities ? party.identities.map((identity: IdentityEntity) => identityFrom(identity)) : [], + electronicAddresses: party.electronicAddresses + ? party.electronicAddresses.map((electronicAddress: ElectronicAddressEntity) => electronicAddressFrom(electronicAddress)) + : [], + relationships: party.relationships ? party.relationships.map((relationship: PartyRelationshipEntity) => partyRelationshipFrom(relationship)) : [], + partyType: partyTypeFrom(party.partyType), + contact: contactFrom(party.contact), + createdAt: party.createdAt, + lastUpdatedAt: party.lastUpdatedAt, + } +} + +export const contactEntityFrom = (contact: NonPersistedContact): BaseContactEntity => { + if (isNaturalPerson(contact)) { + return naturalPersonEntityFrom(contact) + } else if (isOrganization(contact)) { + return organizationEntityFrom(contact) + } + + throw new Error('Contact not supported') +} + +export const contactFrom = (contact: BaseContactEntity): Contact => { + if (isNaturalPerson(contact)) { + return naturalPersonFrom(contact) + } else if (isOrganization(contact)) { + return organizationFrom(contact) + } + + throw new Error('Contact not supported') +} + +export const isNaturalPerson = (contact: NonPersistedContact | BaseContactEntity): contact is NonPersistedNaturalPerson | NaturalPersonEntity => + 'firstName' in contact && 'lastName' in contact + +export const isOrganization = (contact: NonPersistedContact | BaseContactEntity): contact is NonPersistedOrganization | OrganizationEntity => + 'legalName' in contact + +export const connectionEntityFrom = (connection: NonPersistedConnection): ConnectionEntity => { + const connectionEntity: ConnectionEntity = new ConnectionEntity() + connectionEntity.type = connection.type + connectionEntity.config = configEntityFrom(connection.config) + + return connectionEntity +} + +export const connectionFrom = (connection: ConnectionEntity): Connection => { + return { + id: connection.id, + type: connection.type, + config: configFrom(connection.config), + } +} + +const configEntityFrom = (config: NonPersistedConnectionConfig): BaseConfigEntity => { + if (isOpenIdConfig(config)) { + return openIdConfigEntityFrom(config) + } else if (isDidAuthConfig(config)) { + return didAuthConfigEntityFrom(config) + } + + throw new Error('config type not supported') +} + +export const correlationIdentifierEntityFrom = (identifier: NonPersistedCorrelationIdentifier): CorrelationIdentifierEntity => { + const identifierEntity: CorrelationIdentifierEntity = new CorrelationIdentifierEntity() + identifierEntity.type = identifier.type + identifierEntity.correlationId = identifier.correlationId + + return identifierEntity +} + +export const correlationIdentifierFrom = (identifier: CorrelationIdentifierEntity): CorrelationIdentifier => { + return { + id: identifier.id, + type: identifier.type, + correlationId: identifier.correlationId, + } +} + +export const didAuthConfigEntityFrom = (config: NonPersistedDidAuthConfig): DidAuthConfigEntity => { + const didAuthConfig: DidAuthConfigEntity = new DidAuthConfigEntity() + didAuthConfig.identifier = config.identifier.did + didAuthConfig.redirectUrl = config.redirectUrl + didAuthConfig.sessionId = config.sessionId + + return didAuthConfig +} + +export const electronicAddressEntityFrom = (electronicAddress: NonPersistedElectronicAddress): ElectronicAddressEntity => { + const electronicAddressEntity: ElectronicAddressEntity = new ElectronicAddressEntity() + electronicAddressEntity.type = electronicAddress.type + electronicAddressEntity.electronicAddress = electronicAddress.electronicAddress + + return electronicAddressEntity +} + +export const electronicAddressFrom = (electronicAddress: ElectronicAddressEntity): ElectronicAddress => { + return { + id: electronicAddress.id, + type: electronicAddress.type, + electronicAddress: electronicAddress.electronicAddress, + createdAt: electronicAddress.createdAt, + lastUpdatedAt: electronicAddress.lastUpdatedAt, + } +} + +export const identityEntityFrom = (args: NonPersistedIdentity): IdentityEntity => { + const identityEntity: IdentityEntity = new IdentityEntity() + identityEntity.alias = args.alias + identityEntity.roles = args.roles + identityEntity.identifier = correlationIdentifierEntityFrom(args.identifier) + identityEntity.connection = args.connection ? connectionEntityFrom(args.connection) : undefined + identityEntity.metadata = args.metadata ? args.metadata.map((item: NonPersistedMetadataItem) => metadataItemEntityFrom(item)) : [] + + return identityEntity +} + +export const identityFrom = (identity: IdentityEntity): Identity => { + return { + id: identity.id, + alias: identity.alias, + roles: identity.roles, + identifier: correlationIdentifierFrom(identity.identifier), + ...(identity.connection && { connection: connectionFrom(identity.connection) }), + metadata: identity.metadata ? identity.metadata.map((item: IdentityMetadataItemEntity) => metadataItemFrom(item)) : [], + createdAt: identity.createdAt, + lastUpdatedAt: identity.createdAt, + } +} + +export const metadataItemEntityFrom = (item: NonPersistedMetadataItem): IdentityMetadataItemEntity => { + const metadataItemEntity: IdentityMetadataItemEntity = new IdentityMetadataItemEntity() + metadataItemEntity.label = item.label + metadataItemEntity.value = item.value + + return metadataItemEntity +} + +export const metadataItemFrom = (item: IdentityMetadataItemEntity): MetadataItem => { + return { + id: item.id, + label: item.label, + value: item.value, + } +} + +export const naturalPersonEntityFrom = (naturalPerson: NonPersistedNaturalPerson): NaturalPersonEntity => { + const naturalPersonEntity: NaturalPersonEntity = new NaturalPersonEntity() + naturalPersonEntity.firstName = naturalPerson.firstName + naturalPersonEntity.middleName = naturalPerson.middleName + naturalPersonEntity.lastName = naturalPerson.lastName + naturalPersonEntity.displayName = naturalPerson.displayName + + return naturalPersonEntity +} + +export const naturalPersonFrom = (naturalPerson: NaturalPersonEntity): NaturalPerson => { + return { + id: naturalPerson.id, + firstName: naturalPerson.firstName, + middleName: naturalPerson.middleName, + lastName: naturalPerson.lastName, + displayName: naturalPerson.displayName, + createdAt: naturalPerson.createdAt, + lastUpdatedAt: naturalPerson.lastUpdatedAt, + } +} + +export const openIdConfigEntityFrom = (config: NonPersistedOpenIdConfig): OpenIdConfigEntity => { + const openIdConfig: OpenIdConfigEntity = new OpenIdConfigEntity() + openIdConfig.clientId = config.clientId + openIdConfig.clientSecret = config.clientSecret + openIdConfig.scopes = config.scopes + openIdConfig.issuer = config.issuer + openIdConfig.redirectUrl = config.redirectUrl + openIdConfig.dangerouslyAllowInsecureHttpRequests = config.dangerouslyAllowInsecureHttpRequests + openIdConfig.clientAuthMethod = config.clientAuthMethod + + return openIdConfig +} + +export const organizationEntityFrom = (organization: NonPersistedOrganization): OrganizationEntity => { + const organizationEntity: OrganizationEntity = new OrganizationEntity() + organizationEntity.legalName = organization.legalName + organizationEntity.displayName = organization.displayName + + return organizationEntity +} + +export const organizationFrom = (organization: OrganizationEntity): Organization => { + return { + id: organization.id, + legalName: organization.legalName, + displayName: organization.displayName, + createdAt: organization.createdAt, + lastUpdatedAt: organization.lastUpdatedAt, + } +} + +export const partyRelationshipEntityFrom = (relationship: NonPersistedPartyRelationship): PartyRelationshipEntity => { + const partyRelationshipEntity: PartyRelationshipEntity = new PartyRelationshipEntity() + partyRelationshipEntity.leftId = relationship.leftId + partyRelationshipEntity.rightId = relationship.rightId + + return partyRelationshipEntity +} + +export const partyRelationshipFrom = (relationship: PartyRelationshipEntity): PartyRelationship => { + return { + id: relationship.id, + leftId: relationship.leftId, + rightId: relationship.rightId, + createdAt: relationship.createdAt, + lastUpdatedAt: relationship.lastUpdatedAt, + } +} + +export const partyTypeEntityFrom = (args: NonPersistedPartyType): PartyTypeEntity => { + const partyTypeEntity: PartyTypeEntity = new PartyTypeEntity() + if (args.id) { + partyTypeEntity.id = args.id + } + partyTypeEntity.type = args.type + partyTypeEntity.name = args.name + partyTypeEntity.description = args.description + partyTypeEntity.tenantId = args.tenantId + + return partyTypeEntity +} + +export const partyTypeFrom = (partyType: PartyTypeEntity): PartyType => { + return { + id: partyType.id, + type: partyType.type, + name: partyType.name, + tenantId: partyType.tenantId, + description: partyType.description, + createdAt: partyType.createdAt, + lastUpdatedAt: partyType.lastUpdatedAt, + } +} + +export const configFrom = (config: BaseConfigEntity): ConnectionConfig => { + if (isOpenIdConfig(config)) { + return openIdConfigFrom(config) + } else if (isDidAuthConfig(config)) { + return didAuthConfigFrom(config) + } + + throw new Error('config type not supported') +} + +export const openIdConfigFrom = (config: OpenIdConfigEntity): OpenIdConfig => { + return { + id: config.id, + clientId: config.clientId, + clientSecret: config.clientSecret, + scopes: config.scopes, + issuer: config.issuer, + redirectUrl: config.redirectUrl, + dangerouslyAllowInsecureHttpRequests: config.dangerouslyAllowInsecureHttpRequests, + clientAuthMethod: config.clientAuthMethod, + } +} + +export const didAuthConfigFrom = (config: DidAuthConfigEntity): DidAuthConfig => { + return { + id: config.id, + identifier: { did: config.identifier, provider: '', keys: [], services: [] }, + stateId: '', // FIXME + redirectUrl: config.redirectUrl, + sessionId: config.sessionId, + } +} + +export const isOpenIdConfig = (config: NonPersistedConnectionConfig | BaseConfigEntity): config is OpenIdConfig | OpenIdConfigEntity => + 'clientSecret' in config && 'issuer' in config && 'redirectUrl' in config + +export const isDidAuthConfig = (config: NonPersistedConnectionConfig | BaseConfigEntity): config is DidAuthConfig | DidAuthConfigEntity => + 'identifier' in config && 'redirectUrl' in config && 'sessionId' in config diff --git a/packages/issuance-branding/__tests__/localAgent.test.ts b/packages/issuance-branding/__tests__/localAgent.test.ts index deac4b496..3345ff5ac 100644 --- a/packages/issuance-branding/__tests__/localAgent.test.ts +++ b/packages/issuance-branding/__tests__/localAgent.test.ts @@ -29,6 +29,6 @@ const testContext = { tearDown, } -describe('Local integration tests', () => { +describe('Local integration tests', (): void => { issuanceBrandingAgentLogic(testContext) }) diff --git a/packages/issuance-branding/__tests__/restAgent.test.ts b/packages/issuance-branding/__tests__/restAgent.test.ts index e5a30fc72..5957779bc 100644 --- a/packages/issuance-branding/__tests__/restAgent.test.ts +++ b/packages/issuance-branding/__tests__/restAgent.test.ts @@ -66,6 +66,6 @@ const testContext = { tearDown, } -describe('REST integration tests', () => { +describe('REST integration tests', (): void => { issuanceBrandingAgentLogic(testContext) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f8d91928..9738d930e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -177,7 +177,7 @@ importers: version: 5.0.1 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) url-parse: specifier: ^1.5.10 version: 1.5.10 @@ -205,7 +205,7 @@ importers: version: 3.1.8 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) devDependencies: '@sphereon/ssi-sdk.agent-config': specifier: workspace:* @@ -233,8 +233,11 @@ importers: version: 4.3.4 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) devDependencies: + pg: + specifier: ^8.11.3 + version: 8.11.3 sqlite3: specifier: ^5.1.6 version: 5.1.6 @@ -310,7 +313,7 @@ importers: version: 4.3.4 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) devDependencies: '@sphereon/ssi-sdk.agent-config': specifier: workspace:* @@ -338,7 +341,7 @@ importers: version: 3.0.1 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) uint8arrays: specifier: ^3.1.1 version: 3.1.1 @@ -470,7 +473,7 @@ importers: version: 27.1.5(@babel/core@7.22.15)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) typescript: specifier: 4.9.5 version: 4.9.5 @@ -691,7 +694,7 @@ importers: version: 10.9.1(@types/node@18.16.3)(typescript@4.9.5) typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) web-did-resolver: specifier: ^2.0.24 version: 2.0.27 @@ -1200,7 +1203,7 @@ importers: version: 10.9.1(@types/node@18.16.3)(typescript@4.9.5) typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) packages/siopv2-oid4vp-rp-rest-client: dependencies: @@ -1535,7 +1538,7 @@ importers: version: 10.9.1(@types/node@18.16.3)(typescript@4.9.5) typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) web-did-resolver: specifier: ^2.0.27 version: 2.0.27 @@ -1825,7 +1828,7 @@ importers: version: 4.3.4 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) uint8arrays: specifier: ^3.1.1 version: 3.1.1 @@ -1883,7 +1886,7 @@ importers: version: 0.1.13 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) uint8arrays: specifier: ^3.1.1 version: 3.1.1 @@ -2134,7 +2137,7 @@ importers: version: 10.9.1(@types/node@18.16.3)(typescript@4.9.5) typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) packages/web3-provider-headless: dependencies: @@ -2294,7 +2297,7 @@ importers: version: 3.1.3 typeorm: specifier: 0.3.17 - version: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + version: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) uuid: specifier: ^8.3.2 version: 8.3.2 @@ -9338,7 +9341,7 @@ packages: '@veramo/key-manager': 4.2.0 '@veramo/utils': 4.2.0 debug: 4.3.4 - typeorm: 0.3.17(sqlite3@5.1.6)(ts-node@10.9.1) + typeorm: 0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1) transitivePeerDependencies: - '@google-cloud/spanner' - '@sap/hana-client' @@ -10749,6 +10752,10 @@ packages: /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + /buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} dev: true @@ -19212,6 +19219,9 @@ packages: p-reduce: 2.1.0 dev: true + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + /pacote@13.6.2: resolution: {integrity: sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -19479,6 +19489,62 @@ packages: /pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + optional: true + + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + /pg-pool@3.6.1(pg@8.11.3): + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.3 + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + /pg@8.11.3: + resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.11.3) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -19570,6 +19636,24 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -21041,6 +21125,10 @@ packages: readable-stream: 3.6.2 dev: true + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + /split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} dependencies: @@ -22053,7 +22141,7 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: true - /typeorm@0.3.17(sqlite3@5.1.6)(ts-node@10.9.1): + /typeorm@0.3.17(pg@8.11.3)(sqlite3@5.1.6)(ts-node@10.9.1): resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} engines: {node: '>= 12.9.0'} hasBin: true @@ -22121,6 +22209,7 @@ packages: dotenv: 16.3.1 glob: 8.1.0 mkdirp: 2.1.6 + pg: 8.11.3 reflect-metadata: 0.1.13 sha.js: 2.4.11 sqlite3: 5.1.6