Skip to content

Commit

Permalink
Merge pull request #277 from Sphereon-Opensource/feature/SPRIND-73
Browse files Browse the repository at this point in the history
feature/SPRIND-73
  • Loading branch information
nklomp authored Nov 19, 2024
2 parents 76d2414 + d90e79c commit 25c2028
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 28 deletions.
129 changes: 125 additions & 4 deletions packages/data-store/src/__tests__/issuanceBranding.entities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ describe('Database entities tests', (): void => {
text: {
color: '#000000',
},
claims: [
{
key: 'given_name',
name: 'Given Name'
},
{
key: 'family_name',
name: 'Surname'
}
]
},
],
}
Expand Down Expand Up @@ -112,6 +122,8 @@ describe('Database entities tests', (): void => {
)
expect(fromDb?.localeBranding[0].text).toBeDefined()
expect(fromDb?.localeBranding[0].text!.color).toEqual(credentialBranding.localeBranding[0].text!.color)
expect(fromDb?.localeBranding[0].claims).toBeDefined()
expect(fromDb?.localeBranding[0].claims.length).toEqual(credentialBranding.localeBranding[0].claims!.length)
expect(fromDb?.createdAt).toBeDefined()
expect(fromDb?.lastUpdatedAt).toBeDefined()
})
Expand Down Expand Up @@ -302,8 +314,8 @@ describe('Database entities tests', (): void => {

const credentialLocaleBrandingEntity: CredentialLocaleBrandingEntity = credentialLocaleBrandingEntityFrom(localeBranding)
const fromDb: CredentialLocaleBrandingEntity = await dbConnection
.getRepository(CredentialLocaleBrandingEntity)
.save(credentialLocaleBrandingEntity)
.getRepository(CredentialLocaleBrandingEntity)
.save(credentialLocaleBrandingEntity)

expect(fromDb).toBeDefined()
expect(fromDb?.alias).toEqual(localeBranding.alias)
Expand Down Expand Up @@ -453,13 +465,122 @@ describe('Database entities tests', (): void => {
}

const result: CredentialLocaleBrandingEntity = await dbConnection
.getRepository(CredentialLocaleBrandingEntity)
.save(updatedCredentialLocaleBranding)
.getRepository(CredentialLocaleBrandingEntity)
.save(updatedCredentialLocaleBranding)

expect(result).toBeDefined()
expect(result?.lastUpdatedAt).not.toEqual(fromDb?.localeBranding[0].lastUpdatedAt)
})

it('Should save only claims branding to database', async (): Promise<void> => {
const credentialBranding: IBasicCredentialBranding = {
issuerCorrelationId: 'issuerCorrelationId',
vcHash: 'vcHash',
localeBranding: [
{
claims: [
{
key: 'given_name',
name: 'Given Name'
},
{
key: 'family_name',
name: 'Surname'
}
]
},
],
}

const credentialBrandingEntity: CredentialBrandingEntity = credentialBrandingEntityFrom(credentialBranding)
const fromDb: CredentialBrandingEntity = await dbConnection.getRepository(CredentialBrandingEntity).save(credentialBrandingEntity)

expect(fromDb).toBeDefined()
expect(fromDb?.id).toBeDefined()
expect(fromDb?.issuerCorrelationId).toEqual(credentialBranding.issuerCorrelationId)
expect(fromDb?.vcHash).toEqual(credentialBranding.vcHash)
expect(fromDb?.localeBranding).toBeDefined()
expect(fromDb?.localeBranding.length).toEqual(1)
expect(fromDb?.localeBranding[0].claims).toBeDefined()
expect(fromDb?.localeBranding[0].claims.length).toEqual(credentialBranding.localeBranding[0].claims!.length)
expect(fromDb?.createdAt).toBeDefined()
expect(fromDb?.lastUpdatedAt).toBeDefined()
})

it('Should enforce unique locale for a credential claim key', async (): Promise<void> => {
const credentialBranding: IBasicCredentialBranding = {
issuerCorrelationId: 'issuerCorrelationId',
vcHash: 'vcHash',
localeBranding: [
{
locale: 'en-US',
claims: [
{
key: 'given_name',
name: 'Given Name'
},
{
key: 'given_name',
name: 'Given Name'
},
],
}
],
}

const credentialBrandingEntity: CredentialBrandingEntity = credentialBrandingEntityFrom(credentialBranding)

await expect(dbConnection.getRepository(CredentialBrandingEntity).save(credentialBrandingEntity)).rejects.toThrowError(
'SQLITE_CONSTRAINT: UNIQUE constraint failed: CredentialClaims.credentialLocaleBrandingId, CredentialClaims.key',
)
})

it('should throw error when saving credential branding with blank claim key', async (): Promise<void> => {
const credentialBranding: IBasicCredentialBranding = {
issuerCorrelationId: 'issuerCorrelationId',
vcHash: 'vcHash',
localeBranding: [
{
claims: [
{
key: '',
name: 'Given Name'
}
]
},
],
}

const credentialBrandingEntity: CredentialBrandingEntity = credentialBrandingEntityFrom(credentialBranding)

await expect(dbConnection.getRepository(CredentialBrandingEntity).save(credentialBrandingEntity)).rejects.toThrowError(
'Blank claim keys are not allowed',
)
})

it('should throw error when saving credential branding with blank claim name', async (): Promise<void> => {
const credentialBranding: IBasicCredentialBranding = {
issuerCorrelationId: 'issuerCorrelationId',
vcHash: 'vcHash',
localeBranding: [
{
claims: [
{
key: 'given_name',
name: ''
}
]
},
],
}

const credentialBrandingEntity: CredentialBrandingEntity = credentialBrandingEntityFrom(credentialBranding)

await expect(dbConnection.getRepository(CredentialBrandingEntity).save(credentialBrandingEntity)).rejects.toThrowError(
'Blank claim names are not allowed',
)
})

// Issuer tests

it('Should save issuer branding to database', async (): Promise<void> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
BaseEntity,
BeforeInsert,
BeforeUpdate,
Column,
Entity,
Index,
ManyToOne,
PrimaryGeneratedColumn
} from 'typeorm'
import { CredentialLocaleBrandingEntity } from './CredentialLocaleBrandingEntity'
import { validate, Validate, ValidationError } from 'class-validator'
import { IsNonEmptyStringConstraint } from '../validators'

@Entity('CredentialClaims')
@Index('IDX_CredentialClaimsEntity_credentialLocaleBranding_locale', ['credentialLocaleBranding', 'key'], { unique: true })
export class CredentialClaimsEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string

@Column('varchar', { name: 'key', length: 255, nullable: false, unique: false })
@Validate(IsNonEmptyStringConstraint, { message: 'Blank claim keys are not allowed' })
key!: string

@Column('varchar', { name: 'name', length: 255, nullable: false, unique: false })
@Validate(IsNonEmptyStringConstraint, { message: 'Blank claim names are not allowed' })
name!: string

@ManyToOne(() => CredentialLocaleBrandingEntity, (credentialLocaleBranding: CredentialLocaleBrandingEntity) => credentialLocaleBranding.claims, {
cascade: ['insert', 'update'],
onDelete: 'CASCADE'
})
credentialLocaleBranding!: CredentialLocaleBrandingEntity

@BeforeInsert()
@BeforeUpdate()
async validate(): Promise<undefined> {
const validation: Array<ValidationError> = await validate(this)
if (validation.length > 0) {
return Promise.reject(Error(Object.values(validation[0].constraints!)[0]))
}
return
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChildEntity, Column, JoinColumn, ManyToOne, Index } from 'typeorm'
import { ChildEntity, Column, JoinColumn, ManyToOne, Index, OneToMany } from 'typeorm'
import { CredentialBrandingEntity } from './CredentialBrandingEntity'
import { BaseLocaleBrandingEntity } from './BaseLocaleBrandingEntity'
import { CredentialClaimsEntity } from './CredentialClaimsEntity'

@ChildEntity('CredentialLocaleBranding')
@Index('IDX_CredentialLocaleBrandingEntity_credentialBranding_locale', ['credentialBranding', 'locale'], { unique: true })
Expand All @@ -11,6 +12,15 @@ export class CredentialLocaleBrandingEntity extends BaseLocaleBrandingEntity {
@JoinColumn({ name: 'credentialBrandingId' })
credentialBranding!: CredentialBrandingEntity

@OneToMany(() => CredentialClaimsEntity, (claims: CredentialClaimsEntity) => claims.credentialLocaleBranding, {
cascade: true,
onDelete: 'CASCADE',
eager: true,
nullable: false,
})
@JoinColumn({ name: 'claim_id' })
claims!: Array<CredentialClaimsEntity>

@Column('text', { name: 'credentialBrandingId', nullable: false })
credentialBrandingId!: string
}
13 changes: 8 additions & 5 deletions packages/data-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,25 @@ import { OrganizationEntity } from './entities/contact/OrganizationEntity'
import { NaturalPersonEntity } from './entities/contact/NaturalPersonEntity'
import { ElectronicAddressEntity } from './entities/contact/ElectronicAddressEntity'
import { PhysicalAddressEntity } from './entities/contact/PhysicalAddressEntity'
import { AuditEventEntity } from './entities/eventLogger/AuditEventEntity'
import { DigitalCredentialEntity } from './entities/digitalCredential/DigitalCredentialEntity'
import { PresentationDefinitionItemEntity } from './entities/presentationDefinition/PresentationDefinitionItemEntity'
import { ContactMetadataItemEntity } from './entities/contact/ContactMetadataItemEntity'
import { CredentialClaimsEntity } from './entities/issuanceBranding/CredentialClaimsEntity'

export { ContactStore } from './contact/ContactStore'
export { AbstractContactStore } from './contact/AbstractContactStore'
export { AbstractDigitalCredentialStore } from './digitalCredential/AbstractDigitalCredentialStore'
export { DigitalCredentialStore } from './digitalCredential/DigitalCredentialStore'
export { AbstractIssuanceBrandingStore } from './issuanceBranding/AbstractIssuanceBrandingStore'
export { IssuanceBrandingStore } from './issuanceBranding/IssuanceBrandingStore'
export { StatusListStore } from './statusList/StatusListStore'
import { AuditEventEntity } from './entities/eventLogger/AuditEventEntity'
import { DigitalCredentialEntity } from './entities/digitalCredential/DigitalCredentialEntity'
import { PresentationDefinitionItemEntity } from './entities/presentationDefinition/PresentationDefinitionItemEntity'
import { ContactMetadataItemEntity } from './entities/contact/ContactMetadataItemEntity'
export { AbstractEventLoggerStore } from './eventLogger/AbstractEventLoggerStore'
export { EventLoggerStore } from './eventLogger/EventLoggerStore'
export { IAbstractMachineStateStore } from './machineState/IAbstractMachineStateStore'
export { MachineStateStore } from './machineState/MachineStateStore'
export { AbstractPDStore } from './presentationDefinition/AbstractPDStore'
export { PDStore } from './presentationDefinition/PDStore'

export {
DataStoreMigrations,
DataStoreEventLoggerMigrations,
Expand Down Expand Up @@ -91,6 +92,7 @@ export const DataStoreIssuanceBrandingEntities = [
TextAttributesEntity,
CredentialLocaleBrandingEntity,
IssuerLocaleBrandingEntity,
CredentialClaimsEntity
]

export const DataStorePresentationDefinitionEntities = [PresentationDefinitionItemEntity]
Expand Down Expand Up @@ -144,4 +146,5 @@ export {
MachineStateInfoEntity,
PresentationDefinitionItemEntity,
ContactMetadataItemEntity,
CredentialClaimsEntity
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export class CreateIssuanceBranding1685628974232 implements MigrationInterface {
`CREATE UNIQUE INDEX "IDX_IssuerLocaleBrandingEntity_issuerBranding_locale" ON "BaseLocaleBranding" ("issuerBrandingId", "locale")`,
)
await queryRunner.query(`CREATE INDEX "IDX_BaseLocaleBranding_type" ON "BaseLocaleBranding" ("type")`)

await queryRunner.query(`CREATE TABLE "CredentialClaims" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "key" character varying(255) NOT NULL, "name" character varying(255) NOT NULL, "credentialLocaleBrandingId" character varying, CONSTRAINT "PK_CredentialClaims_id" PRIMARY KEY ("id"))`)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_CredentialClaimsEntity_credentialLocaleBranding_locale" ON "CredentialClaims" ("credentialLocaleBrandingId", "key")`
)

await queryRunner.query(
`CREATE TABLE "CredentialBranding" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "vcHash" character varying(255) NOT NULL, "issuerCorrelationId" character varying(255) NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "last_updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_vcHash" UNIQUE ("vcHash"), CONSTRAINT "PK_CredentialBranding_id" PRIMARY KEY ("id"))`,
)
Expand Down Expand Up @@ -76,6 +82,8 @@ export class CreateIssuanceBranding1685628974232 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" DROP INDEX "IDX_BaseLocaleBranding_type"`)
await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" DROP INDEX "IDX_IssuerLocaleBrandingEntity_issuerBranding_locale"`)
await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" DROP INDEX "IDX_CredentialLocaleBrandingEntity_credentialBranding_locale"`)
await queryRunner.query(`ALTER TABLE "CredentialClaims" DROP INDEX "IDX_CredentialClaimsEntity_credentialLocaleBranding_locale"`)
await queryRunner.query(`DROP TABLE "CredentialClaims"`)
await queryRunner.query(`DROP TABLE "BaseLocaleBranding"`)
await queryRunner.query(`DROP TABLE "TextAttributes"`)
await queryRunner.query(`DROP TABLE "BackgroundAttributes"`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export class CreateIssuanceBranding1685628973231 implements MigrationInterface {
`CREATE UNIQUE INDEX "IDX_IssuerLocaleBrandingEntity_issuerBranding_locale" ON "BaseLocaleBranding" ("issuerBrandingId", "locale")`,
)
await queryRunner.query(`CREATE INDEX "IDX_BaseLocaleBranding_type" ON "BaseLocaleBranding" ("type")`)
await queryRunner.query(`CREATE TABLE "CredentialClaims" ("id" varchar PRIMARY KEY NOT NULL, "key" varchar(255) NOT NULL, "name" varchar(255) NOT NULL, "credentialLocaleBrandingId" varchar)`)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_CredentialClaimsEntity_credentialLocaleBranding_locale" ON "CredentialClaims" ("credentialLocaleBrandingId", "key")`
)
await queryRunner.query(
`CREATE TABLE "CredentialBranding" ("id" varchar PRIMARY KEY NOT NULL, "vcHash" varchar(255) NOT NULL, "issuerCorrelationId" varchar(255) NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_vcHash" UNIQUE ("vcHash"))`,
)
Expand Down Expand Up @@ -108,6 +112,8 @@ export class CreateIssuanceBranding1685628973231 implements MigrationInterface {
await queryRunner.query(`DROP INDEX "IDX_CredentialBrandingEntity_issuerCorrelationId"`)
await queryRunner.query(`DROP TABLE "CredentialBranding"`)
await queryRunner.query(`DROP INDEX "IDX_BaseLocaleBranding_type"`)
await queryRunner.query(`DROP INDEX "IDX_CredentialClaimsEntity_credentialLocaleBranding_locale"`)
await queryRunner.query(`DROP TABLE "CredentialClaims"`)
await queryRunner.query(`DROP INDEX "IDX_IssuerLocaleBrandingEntity_issuerBranding_locale"`)
await queryRunner.query(`DROP INDEX "IDX_CredentialLocaleBrandingEntity_credentialBranding_locale"`)
await queryRunner.query(`DROP TABLE "BaseLocaleBranding"`)
Expand Down
18 changes: 15 additions & 3 deletions packages/data-store/src/types/issuanceBranding/issuanceBranding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,29 @@ export interface IImageDimensions {
export interface IBasicImageDimensions extends Omit<IImageDimensions, 'id'> {}
export interface IPartialImageDimensions extends Partial<IImageDimensions> {}

export interface ICredentialLocaleBranding extends ILocaleBranding {}
export interface ICredentialClaim {
id: string
key: string
name: string
}
export interface IBasicCredentialClaim extends Omit<ICredentialClaim, 'id'> {}
export interface IPartialCredentialClaim extends Partial<ICredentialClaim> {}

export interface ICredentialLocaleBranding extends ILocaleBranding {
claims?: Array<ICredentialClaim>
}
export interface IBasicCredentialLocaleBranding
extends Omit<ICredentialLocaleBranding, 'id' | 'createdAt' | 'lastUpdatedAt' | 'logo' | 'background' | 'text'> {
extends Omit<ICredentialLocaleBranding, 'id' | 'createdAt' | 'lastUpdatedAt' | 'logo' | 'background' | 'text' | 'claims'> {
logo?: IBasicImageAttributes
background?: IBasicBackgroundAttributes
text?: IBasicTextAttributes
claims?: Array<IBasicCredentialClaim>
}
export interface IPartialCredentialLocaleBranding extends Partial<Omit<ICredentialLocaleBranding, 'logo' | 'background' | 'text'>> {
export interface IPartialCredentialLocaleBranding extends Partial<Omit<ICredentialLocaleBranding, 'logo' | 'background' | 'text' | 'claims'>> {
logo?: IPartialImageAttributes
background?: IPartialBackgroundAttributes
text?: IPartialTextAttributes
claims?: IPartialCredentialClaim
}

export interface ICredentialBranding {
Expand Down
15 changes: 14 additions & 1 deletion packages/data-store/src/utils/issuanceBranding/MappingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { TextAttributesEntity } from '../../entities/issuanceBranding/TextAttrib
import { IssuerLocaleBrandingEntity } from '../../entities/issuanceBranding/IssuerLocaleBrandingEntity'
import { CredentialLocaleBrandingEntity } from '../../entities/issuanceBranding/CredentialLocaleBrandingEntity'
import { ImageDimensionsEntity } from '../../entities/issuanceBranding/ImageDimensionsEntity'
import { CredentialClaimsEntity } from '../../entities/issuanceBranding/CredentialClaimsEntity'
import {
IBasicBackgroundAttributes,
IBasicCredentialBranding,
IBasicCredentialBranding, IBasicCredentialClaim,
IBasicCredentialLocaleBranding,
IBasicImageAttributes,
IBasicImageDimensions,
Expand Down Expand Up @@ -94,6 +95,9 @@ export const credentialLocaleBrandingEntityFrom = (args: IBasicCredentialLocaleB
credentialLocaleBrandingEntity.description = isEmptyString(args.description) ? undefined : args.description
credentialLocaleBrandingEntity.background = args.background ? backgroundAttributesEntityFrom(args.background) : undefined
credentialLocaleBrandingEntity.text = args.text ? textAttributesEntityFrom(args.text) : undefined
credentialLocaleBrandingEntity.claims = args.claims
? args.claims.map((claim) => credentialClaimsEntityFrom(claim))
: []

return credentialLocaleBrandingEntity
}
Expand Down Expand Up @@ -133,3 +137,12 @@ export const textAttributesEntityFrom = (args: IBasicTextAttributes): TextAttrib

return textAttributesEntity
}

export const credentialClaimsEntityFrom = (args: IBasicCredentialClaim): CredentialClaimsEntity => {
const credentialClaimsEntity: CredentialClaimsEntity = new CredentialClaimsEntity()
credentialClaimsEntity.key = args.key
credentialClaimsEntity.name = args.name

return credentialClaimsEntity
}

Loading

0 comments on commit 25c2028

Please sign in to comment.