diff --git a/apps/credential-showcase-api-server/src/database/migrations/0006_credential-showcase-api.sql b/apps/credential-showcase-api-server/src/database/migrations/0006_credential-showcase-api.sql new file mode 100644 index 0000000..d677b48 --- /dev/null +++ b/apps/credential-showcase-api-server/src/database/migrations/0006_credential-showcase-api.sql @@ -0,0 +1,3 @@ +ALTER TABLE "credentialDefinition" ALTER COLUMN "icon" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "step" ADD COLUMN "credential_definition_id" uuid;--> statement-breakpoint +ALTER TABLE "step" ADD CONSTRAINT "step_credential_definition_id_credentialDefinition_id_fk" FOREIGN KEY ("credential_definition_id") REFERENCES "public"."credentialDefinition"("id") ON DELETE no action ON UPDATE no action; diff --git a/apps/credential-showcase-api-server/src/database/migrations/meta/_journal.json b/apps/credential-showcase-api-server/src/database/migrations/meta/_journal.json index fe484ad..6f312be 100644 --- a/apps/credential-showcase-api-server/src/database/migrations/meta/_journal.json +++ b/apps/credential-showcase-api-server/src/database/migrations/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1742469395188, "tag": "0005_credential-showcase-api", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1742829236121, + "tag": "0006_credential-showcase-api", + "breakpoints": true } ] } diff --git a/apps/credential-showcase-api-server/src/database/repositories/CredentialDefinitionRepository.ts b/apps/credential-showcase-api-server/src/database/repositories/CredentialDefinitionRepository.ts index 41c2e9b..a50a199 100644 --- a/apps/credential-showcase-api-server/src/database/repositories/CredentialDefinitionRepository.ts +++ b/apps/credential-showcase-api-server/src/database/repositories/CredentialDefinitionRepository.ts @@ -1,11 +1,11 @@ -import { eq } from 'drizzle-orm' +import { and, eq } from 'drizzle-orm' import { Service } from 'typedi' import DatabaseService from '../../services/DatabaseService' import AssetRepository from './AssetRepository' import { NotFoundError } from '../../errors' import { credentialDefinitions, credentialRepresentations, revocationInfo } from '../schema' import CredentialSchemaRepository from './CredentialSchemaRepository' -import { CredentialDefinition, NewCredentialDefinition, RepositoryDefinition } from '../../types' +import { CredentialDefinition, IdentifierType, NewCredentialDefinition, RepositoryDefinition } from '../../types' @Service() class CredentialDefinitionRepository implements RepositoryDefinition { @@ -149,6 +149,20 @@ class CredentialDefinitionRepository implements RepositoryDefinition { + const result = await ( + await this.databaseService.getConnection() + ).query.credentialDefinitions.findFirst({ + where: and(eq(credentialDefinitions.identifier, identifier), eq(credentialDefinitions.identifierType, identifierType)), + }) + + if (!result) { + return Promise.reject(new NotFoundError(`No credential definition found for identifier: ${identifier}`)) + } + + return result.id + } } export default CredentialDefinitionRepository diff --git a/apps/credential-showcase-api-server/src/database/repositories/ScenarioRepository.ts b/apps/credential-showcase-api-server/src/database/repositories/ScenarioRepository.ts index 6cec914..45ce5a7 100644 --- a/apps/credential-showcase-api-server/src/database/repositories/ScenarioRepository.ts +++ b/apps/credential-showcase-api-server/src/database/repositories/ScenarioRepository.ts @@ -26,6 +26,7 @@ import { Step, ScenarioType, } from '../../types' +import CredentialDefinitionRepository from './CredentialDefinitionRepository' @Service() class ScenarioRepository implements RepositoryDefinition { @@ -35,6 +36,7 @@ class ScenarioRepository implements RepositoryDefinition private readonly issuerRepository: IssuerRepository, private readonly relyingPartyRepository: RelyingPartyRepository, private readonly assetRepository: AssetRepository, + private readonly credentialDefinitionRepository: CredentialDefinitionRepository, ) {} async create(scenario: NewScenario): Promise { @@ -50,6 +52,19 @@ class ScenarioRepository implements RepositoryDefinition const personaPromises = scenario.personas.map(async (persona) => await this.personaRepository.findById(persona)) await Promise.all(personaPromises) + const updatedSteps = await Promise.all( + scenario.steps.map(async (step: NewStep) => ({ + ...step, + credentialDefinition: + step?.credentialDefinitionIdentifier && step?.credentialDefinitionIdentifierType + ? await this.credentialDefinitionRepository.findIdByIdentifier( + step.credentialDefinitionIdentifier, + step.credentialDefinitionIdentifierType, + ) + : null, + })), + ) + const scenarioType = isIssuanceScenario(scenario) ? ScenarioType.ISSUANCE : ScenarioType.PRESENTATION const scenarioPartyResult: Issuer | RelyingParty = isIssuanceScenario(scenario) @@ -103,7 +118,7 @@ class ScenarioRepository implements RepositoryDefinition const stepsResult = await tx .insert(steps) .values( - scenario.steps.map((step: NewStep) => ({ + updatedSteps.map((step: NewStep) => ({ ...step, scenario: scenarioResult.id, })), @@ -203,6 +218,19 @@ class ScenarioRepository implements RepositoryDefinition const personaPromises = scenario.personas.map(async (persona) => await this.personaRepository.findById(persona)) await Promise.all(personaPromises) + const updatedSteps = await Promise.all( + scenario.steps.map(async (step: NewStep) => ({ + ...step, + credentialDefinition: + step.credentialDefinitionIdentifier && step.credentialDefinitionIdentifierType + ? await this.credentialDefinitionRepository.findIdByIdentifier( + step.credentialDefinitionIdentifier, + step.credentialDefinitionIdentifierType, + ) + : null, + })), + ) + const scenarioType = isIssuanceScenario(scenario) ? ScenarioType.ISSUANCE : ScenarioType.PRESENTATION const scenarioPartyResult: Issuer | RelyingParty = isIssuanceScenario(scenario) @@ -262,7 +290,7 @@ class ScenarioRepository implements RepositoryDefinition const stepsResult = await tx .insert(steps) .values( - scenario.steps.map((step: NewStep) => ({ + updatedSteps.map((step: NewStep) => ({ ...step, scenario: scenarioResult.id, })), @@ -358,6 +386,7 @@ class ScenarioRepository implements RepositoryDefinition }, }, asset: true, + credentialDefinition: true, }, }, relyingParty: { @@ -464,6 +493,7 @@ class ScenarioRepository implements RepositoryDefinition }, }, asset: true, + credentialDefinition: true, }, }, relyingParty: { @@ -554,12 +584,17 @@ class ScenarioRepository implements RepositoryDefinition async createStep(scenarioId: string, step: NewStep): Promise { await this.findById(scenarioId) + const credentialDefinitionIdResult = + step.credentialDefinitionIdentifier && step.credentialDefinitionIdentifierType + ? await this.credentialDefinitionRepository.findIdByIdentifier(step.credentialDefinitionIdentifier, step.credentialDefinitionIdentifierType) + : null const assetResult = step.asset ? await this.assetRepository.findById(step.asset) : null return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { const [stepResult] = await tx .insert(steps) .values({ ...step, + credentialDefinition: credentialDefinitionIdResult, scenario: scenarioId, }) .returning() @@ -612,12 +647,17 @@ class ScenarioRepository implements RepositoryDefinition async updateStep(scenarioId: string, stepId: string, step: NewStep): Promise { await this.findById(scenarioId) + const credentialDefinitionIdResult = + step?.credentialDefinitionIdentifier && step?.credentialDefinitionIdentifierType + ? await this.credentialDefinitionRepository.findIdByIdentifier(step.credentialDefinitionIdentifier, step.credentialDefinitionIdentifierType) + : null const assetResult = step.asset ? await this.assetRepository.findById(step.asset) : null return (await this.databaseService.getConnection()).transaction(async (tx): Promise => { const [stepResult] = await tx .update(steps) .set({ ...step, + credentialDefinition: credentialDefinitionIdResult, scenario: scenarioId, }) .where(eq(steps.id, stepId)) @@ -677,6 +717,7 @@ class ScenarioRepository implements RepositoryDefinition }, }, asset: true, + credentialDefinition: true, }, }) @@ -695,6 +736,7 @@ class ScenarioRepository implements RepositoryDefinition where: eq(steps.scenario, scenarioId), with: { asset: true, + credentialDefinition: true, actions: { with: { proofRequest: true, diff --git a/apps/credential-showcase-api-server/src/database/repositories/__tests__/scenario.repository.test.ts b/apps/credential-showcase-api-server/src/database/repositories/__tests__/scenario.repository.test.ts index f8111e5..63060fb 100644 --- a/apps/credential-showcase-api-server/src/database/repositories/__tests__/scenario.repository.test.ts +++ b/apps/credential-showcase-api-server/src/database/repositories/__tests__/scenario.repository.test.ts @@ -16,6 +16,7 @@ import * as schema from '../../../database/schema' import { Asset, CredentialAttributeType, + CredentialDefinition, CredentialType, IdentifierType, IssuanceScenario, @@ -48,6 +49,7 @@ describe('Database scenario repository tests', (): void => { let asset: Asset let persona1: Persona let persona2: Persona + let credentialDefinition: CredentialDefinition beforeEach(async (): Promise => { client = new PGlite() @@ -113,7 +115,7 @@ describe('Database scenario repository tests', (): void => { description: 'example_revocation_description', }, } - const credentialDefinition = await credentialDefinitionRepository.create(newCredentialDefinition) + credentialDefinition = await credentialDefinitionRepository.create(newCredentialDefinition) const newIssuer: NewIssuer = { name: 'example_name', type: IssuerType.ARIES, @@ -164,6 +166,8 @@ describe('Database scenario repository tests', (): void => { order: 1, type: StepType.HUMAN_TASK, asset: asset.id, + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title', @@ -256,6 +260,8 @@ describe('Database scenario repository tests', (): void => { expect(savedIssuanceScenario.steps[0].title).toEqual(issuanceScenario.steps[0].title) expect(savedIssuanceScenario.steps[0].order).toEqual(issuanceScenario.steps[0].order) expect(savedIssuanceScenario.steps[0].type).toEqual(issuanceScenario.steps[0].type) + expect(savedIssuanceScenario.steps[0].credentialDefinition).toBeDefined() + expect(savedIssuanceScenario.steps[0].credentialDefinition).toEqual(credentialDefinition.id) expect(savedIssuanceScenario.steps[0].actions!.length).toEqual(1) expect(savedIssuanceScenario.steps[0].actions![0].id).toBeDefined() expect(savedIssuanceScenario.steps[0].actions![0].title).toEqual(issuanceScenario.steps[0].actions![0].title) @@ -283,6 +289,7 @@ describe('Database scenario repository tests', (): void => { expect(savedIssuanceScenario.steps[0].asset!.fileName).toEqual(asset.fileName) expect(savedIssuanceScenario.steps[0].asset!.description).toEqual(asset.description) expect(savedIssuanceScenario.steps[0].asset!.content).toStrictEqual(asset.content) + expect(savedIssuanceScenario.steps[1].credentialDefinition).toBeNull() expect((savedIssuanceScenario).issuer).not.toBeNull() expect((savedIssuanceScenario).issuer!.name).toEqual(issuer.name) expect((savedIssuanceScenario).issuer!.credentialDefinitions.length).toEqual(1) @@ -325,6 +332,8 @@ describe('Database scenario repository tests', (): void => { order: 1, type: StepType.HUMAN_TASK, asset: asset.id, + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title', @@ -416,6 +425,8 @@ describe('Database scenario repository tests', (): void => { expect(savedPresentationScenario.steps[0].title).toEqual(presentationScenario.steps[0].title) expect(savedPresentationScenario.steps[0].order).toEqual(presentationScenario.steps[0].order) expect(savedPresentationScenario.steps[0].type).toEqual(presentationScenario.steps[0].type) + expect(savedPresentationScenario.steps[0].credentialDefinition).toBeDefined() + expect(savedPresentationScenario.steps[0].credentialDefinition).toEqual(credentialDefinition.id) expect(savedPresentationScenario.steps[0].actions!.length).toEqual(1) expect(savedPresentationScenario.steps[0].actions![0].id).toBeDefined() expect(savedPresentationScenario.steps[0].actions![0].title).toEqual(presentationScenario.steps[0].actions![0].title) @@ -426,6 +437,7 @@ describe('Database scenario repository tests', (): void => { expect(savedPresentationScenario.steps[0].asset!.fileName).toEqual(asset.fileName) expect(savedPresentationScenario.steps[0].asset!.description).toEqual(asset.description) expect(savedPresentationScenario.steps[0].asset!.content).toStrictEqual(asset.content) + expect(savedPresentationScenario.steps[1].credentialDefinition).toBeNull() expect((savedPresentationScenario).relyingParty).not.toBeNull() expect((savedPresentationScenario).relyingParty!.name).toEqual(relyingParty.name) expect((savedPresentationScenario).relyingParty!.credentialDefinitions.length).toEqual(1) @@ -1308,6 +1320,8 @@ describe('Database scenario repository tests', (): void => { order: 1, type: StepType.HUMAN_TASK, asset: asset.id, + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title1', @@ -1390,6 +1404,8 @@ describe('Database scenario repository tests', (): void => { expect(updatedIssuanceScenarioResult.steps[0].title).toEqual(updatedIssuanceScenario.steps[0].title) expect(updatedIssuanceScenarioResult.steps[0].order).toEqual(updatedIssuanceScenario.steps[0].order) expect(updatedIssuanceScenarioResult.steps[0].type).toEqual(updatedIssuanceScenario.steps[0].type) + expect(savedIssuanceScenario.steps[0].credentialDefinition).toBeDefined() + expect(savedIssuanceScenario.steps[0].credentialDefinition).toBeNull() expect(updatedIssuanceScenarioResult.steps[0].actions!.length).toEqual(2) expect(updatedIssuanceScenarioResult.steps[0].actions![0].id).toBeDefined() expect(updatedIssuanceScenarioResult.steps[0].actions![0].title).toEqual(updatedIssuanceScenario.steps[0].actions![0].title) @@ -1417,6 +1433,7 @@ describe('Database scenario repository tests', (): void => { issuanceScenario.steps[0].actions![0].proofRequest!.predicates.predicate1.value, ) expect(updatedIssuanceScenarioResult.steps[0].actions![0].proofRequest!.predicates!.predicate1.restrictions!.length).toEqual(2) + expect(savedIssuanceScenario.steps[1].credentialDefinition).toBeNull() expect(updatedIssuanceScenarioResult.personas).toBeDefined() expect(updatedIssuanceScenarioResult.personas.length).toEqual(1) expect(updatedIssuanceScenarioResult.personas[0].name).toEqual(persona1.name) @@ -2114,6 +2131,8 @@ describe('Database scenario repository tests', (): void => { order: 2, type: StepType.HUMAN_TASK, asset: asset.id, + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title1', @@ -2190,6 +2209,7 @@ describe('Database scenario repository tests', (): void => { expect(fromDb.steps[1].title).toEqual(step.title) expect(fromDb.steps[1].order).toEqual(step.order) expect(fromDb.steps[1].type).toEqual(step.type) + expect(fromDb.steps[1].credentialDefinition).not.toBeNull() expect(fromDb.steps[1].actions!.length).toEqual(2) expect(fromDb.steps[1].actions![0].id).toBeDefined() expect(fromDb.steps[1].actions![0].title).toEqual(step.actions![0].title) @@ -2293,6 +2313,7 @@ describe('Database scenario repository tests', (): void => { updatedAt: expect.any(Date), }, createdAt: expect.any(Date), + credentialDefinition: null, description: 'example_description', id: expect.any(String), order: 2, @@ -2655,6 +2676,8 @@ describe('Database scenario repository tests', (): void => { order: 1, type: StepType.HUMAN_TASK, asset: asset.id, + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title', @@ -2700,6 +2723,8 @@ describe('Database scenario repository tests', (): void => { const updatedStep: NewStep = { ...savedIssuanceScenario.steps[0], title: 'new_title', + credentialDefinitionIdentifierType: credentialDefinition.identifierType, + credentialDefinitionIdentifier: credentialDefinition.identifier, actions: [ { title: 'example_title1', @@ -2772,6 +2797,8 @@ describe('Database scenario repository tests', (): void => { expect(updatedStepResult.title).toEqual(updatedStep.title) expect(updatedStepResult.order).toEqual(updatedStep.order) expect(updatedStepResult.type).toEqual(updatedStep.type) + expect(updatedStepResult.credentialDefinition).toBeDefined() + expect(updatedStepResult.credentialDefinition).toEqual(credentialDefinition.id) expect(updatedStepResult.actions!.length).toEqual(2) expect(updatedStepResult.actions![0].id).toBeDefined() expect(updatedStepResult.actions![0].title).toEqual(updatedStep.actions![0].title) @@ -2872,6 +2899,7 @@ describe('Database scenario repository tests', (): void => { updatedAt: expect.any(Date), }, createdAt: expect.any(Date), + credentialDefinition: null, description: 'example_description', id: expect.any(String), order: 1, diff --git a/apps/credential-showcase-api-server/src/database/schema/step.ts b/apps/credential-showcase-api-server/src/database/schema/step.ts index e698381..b589e95 100644 --- a/apps/credential-showcase-api-server/src/database/schema/step.ts +++ b/apps/credential-showcase-api-server/src/database/schema/step.ts @@ -5,6 +5,7 @@ import { scenarios } from './scenario' import { stepActions } from './stepAction' import { assets } from './asset' import { StepType } from '../../types' +import { credentialDefinitions } from './credentialDefinition' export const steps = pgTable( 'step', @@ -15,6 +16,7 @@ export const steps = pgTable( screenId: text(), order: integer().notNull(), type: StepTypePg().notNull().$type(), + credentialDefinition: uuid('credential_definition_id').references(() => credentialDefinitions.id), subScenario: uuid('sub_scenario').references(() => scenarios.id), scenario: uuid() .references(() => scenarios.id, { onDelete: 'cascade' }) @@ -50,4 +52,8 @@ export const stepRelations = relations(steps, ({ one, many }) => ({ fields: [steps.asset], references: [assets.id], }), + credentialDefinition: one(credentialDefinitions, { + fields: [steps.credentialDefinition], + references: [credentialDefinitions.id], + }), })) diff --git a/apps/credential-showcase-api-server/src/types/schema/index.ts b/apps/credential-showcase-api-server/src/types/schema/index.ts index 161401a..7669fc8 100644 --- a/apps/credential-showcase-api-server/src/types/schema/index.ts +++ b/apps/credential-showcase-api-server/src/types/schema/index.ts @@ -181,6 +181,8 @@ export type NewStep = Omit & { actions?: NewAriesOOBAction[] subScenario?: string | null screenId?: string | null + credentialDefinitionIdentifierType?: IdentifierType | null + credentialDefinitionIdentifier?: string | null } export type AriesOOBAction = Omit & { diff --git a/packages/credential-showcase-openapi/openapi/openapi.yaml b/packages/credential-showcase-openapi/openapi/openapi.yaml index 73f6b12..4224900 100644 --- a/packages/credential-showcase-openapi/openapi/openapi.yaml +++ b/packages/credential-showcase-openapi/openapi/openapi.yaml @@ -1827,6 +1827,12 @@ components: type: string description: Optional sub-scenario for this step example: 123e4567-e89b-12d3-a456-434314174000 + credentialDefinitionIdentifierType: + $ref: '#/components/schemas/IdentifierType' + credentialDefinitionIdentifier: + type: string + description: External identifier of this credential definition + example: did:sov:XUeUZauFLeBNofY3NhaZCB actions: type: array items: @@ -1875,6 +1881,12 @@ components: type: string description: Optional sub-scenario for this step example: 123e4567-e89b-12d3-a456-434314174000 + credentialDefinitionIdentifierType: + $ref: '#/components/schemas/IdentifierType' + credentialDefinitionIdentifier: + type: string + description: External identifier of this credential definition + example: did:sov:XUeUZauFLeBNofY3NhaZCB actions: type: array description: List of actions associated with this step