@sphereon/ssi-sdk.data-store 0.34.1-feature.SSISDK.78.306 → 0.34.1-feature.SSISDK.82.and.SSISDK.70.346

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sphereon/ssi-sdk.data-store",
3
- "version": "0.34.1-feature.SSISDK.78.306+9aff176a",
3
+ "version": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
4
4
  "source": "src/index.ts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,12 +28,12 @@
28
28
  "dependencies": {
29
29
  "@sphereon/kmp-mdoc-core": "0.2.0-SNAPSHOT.26",
30
30
  "@sphereon/pex": "5.0.0-unstable.28",
31
- "@sphereon/ssi-sdk-ext.did-utils": "0.34.1-feature.SSISDK.78.306+9aff176a",
32
- "@sphereon/ssi-sdk-ext.identifier-resolution": "0.34.1-feature.SSISDK.78.306+9aff176a",
33
- "@sphereon/ssi-sdk.agent-config": "0.34.1-feature.SSISDK.78.306+9aff176a",
34
- "@sphereon/ssi-sdk.core": "0.34.1-feature.SSISDK.78.306+9aff176a",
35
- "@sphereon/ssi-sdk.data-store-types": "0.34.1-feature.SSISDK.78.306+9aff176a",
36
- "@sphereon/ssi-types": "0.34.1-feature.SSISDK.78.306+9aff176a",
31
+ "@sphereon/ssi-sdk-ext.did-utils": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
32
+ "@sphereon/ssi-sdk-ext.identifier-resolution": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
33
+ "@sphereon/ssi-sdk.agent-config": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
34
+ "@sphereon/ssi-sdk.core": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
35
+ "@sphereon/ssi-sdk.data-store-types": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
36
+ "@sphereon/ssi-types": "0.34.1-feature.SSISDK.82.and.SSISDK.70.346+6bddc62b",
37
37
  "@veramo/core": "4.2.0",
38
38
  "@veramo/utils": "4.2.0",
39
39
  "blakejs": "^1.2.1",
@@ -66,5 +66,5 @@
66
66
  "PostgreSQL",
67
67
  "Contact Store"
68
68
  ],
69
- "gitHead": "9aff176afa613d6f69fb1ff33d4f6c8c7b811ffd"
69
+ "gitHead": "6bddc62b9e6c51c49ed95336ac485cde68ca8df0"
70
70
  }
@@ -334,7 +334,7 @@ describe('Database entities tests', (): void => {
334
334
  expect(result).toEqual(false)
335
335
  })
336
336
 
337
- it('should update stored digital credential', async (): Promise<void> => {
337
+ it('should update stored digital credential state', async (): Promise<void> => {
338
338
  const rawCredential: string =
339
339
  'eyJraWQiOiJkaWQ6a2V5Ono2TWtyaGt5M3B1c20yNk1laUZhWFUzbjJuZWtyYW13RlVtZ0dyZUdHa0RWNnpRaiN6Nk1rcmhreTNwdXNtMjZNZWlGYVhVM24ybmVrcmFtd0ZVbWdHcmVHR2tEVjZ6UWoiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJTIiwibGFzdE5hbWUiOiJLIiwiZW1haWxBZGRyZXNzIjoic0BrIn19LCJzdWIiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJqdGkiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJuYmYiOjE3MDg0NDA4MDgsImlzcyI6ImRpZDprZXk6ejZNa3Joa3kzcHVzbTI2TWVpRmFYVTNuMm5la3JhbXdGVW1nR3JlR0drRFY2elFqIn0.G0M84XVAxSmzGY-NQuB9NBofNrINSn6lvxW6761Vlq6ypvYgtc2xNdpiRmw8ryVNfnpzrr4Z5cB1RlrC05rJAw'
340
340
  const digitalCredential: AddCredentialArgs = {
@@ -359,6 +359,121 @@ describe('Database entities tests', (): void => {
359
359
  expect(result.verifiedState).toEqual(CredentialStateType.VERIFIED)
360
360
  })
361
361
 
362
+ // Add these test cases to digitalCredential.store.test.ts after the existing update tests
363
+
364
+ it('should update digital credential fields', async (): Promise<void> => {
365
+ const rawCredential: string =
366
+ 'eyJraWQiOiJkaWQ6a2V5Ono2TWtyaGt5M3B1c20yNk1laUZhWFUzbjJuZWtyYW13RlVtZ0dyZUdHa0RWNnpRaiN6Nk1rcmhreTNwdXNtMjZNZWlGYVhVM24ybmVrcmFtd0ZVbWdHcmVHR2tEVjZ6UWoiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJTIiwibGFzdE5hbWUiOiJLIiwiZW1haWxBZGRyZXNzIjoic0BrIn19LCJzdWIiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJqdGkiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJuYmYiOjE3MDg0NDA4MDgsImlzcyI6ImRpZDprZXk6ejZNa3Joa3kzcHVzbTI2TWVpRmFYVTNuMm5la3JhbXdGVW1nR3JlR0drRFY2elFqIn0.G0M84XVAxSmzGY-NQuB9NBofNrINSn6lvxW6761Vlq6ypvYgtc2xNdpiRmw8ryVNfnpzrr4Z5cB1RlrC05rJAw'
367
+ const digitalCredential: AddCredentialArgs = {
368
+ rawDocument: rawCredential,
369
+ kmsKeyRef: 'testRef',
370
+ identifierMethod: 'did',
371
+ issuerCorrelationType: CredentialCorrelationType.DID,
372
+ subjectCorrelationType: CredentialCorrelationType.DID,
373
+ issuerCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
374
+ subjectCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
375
+ credentialRole: CredentialRole.VERIFIER,
376
+ tenantId: 'urn:uuid:nnag4b43-1e7a-98f8-a32c-a48dbc5b10mj',
377
+ }
378
+
379
+ const savedDigitalCredential: DigitalCredential = await digitalCredentialStore.addCredential(digitalCredential)
380
+
381
+ const result = await digitalCredentialStore.updateCredential({
382
+ id: savedDigitalCredential.id,
383
+ kmsKeyRef: 'newTestRef',
384
+ tenantId: 'urn:uuid:new-tenant-id',
385
+ linkedVpId: 'vp-123',
386
+ })
387
+
388
+ expect(result.kmsKeyRef).toEqual('newTestRef')
389
+ expect(result.tenantId).toEqual('urn:uuid:new-tenant-id')
390
+ expect(result.linkedVpId).toEqual('vp-123')
391
+ expect(result.id).toEqual(savedDigitalCredential.id)
392
+ expect(result.hash).toEqual(savedDigitalCredential.hash)
393
+ expect(result.createdAt).toEqual(savedDigitalCredential.createdAt)
394
+ expect(result.lastUpdatedAt.getTime()).toBeGreaterThan(savedDigitalCredential.lastUpdatedAt.getTime())
395
+ })
396
+
397
+ it('should update digital credential by hash', async (): Promise<void> => {
398
+ const rawCredential: string =
399
+ 'eyJraWQiOiJkaWQ6a2V5Ono2TWtyaGt5M3B1c20yNk1laUZhWFUzbjJuZWtyYW13RlVtZ0dyZUdHa0RWNnpRaiN6Nk1rcmhreTNwdXNtMjZNZWlGYVhVM24ybmVrcmFtd0ZVbWdHcmVHR2tEVjZ6UWoiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJTIiwibGFzdE5hbWUiOiJLIiwiZW1haWxBZGRyZXNzIjoic0BrIn19LCJzdWIiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJqdGkiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJuYmYiOjE3MDg0NDA4MDgsImlzcyI6ImRpZDprZXk6ejZNa3Joa3kzcHVzbTI2TWVpRmFYVTNuMm5la3JhbXdGVW1nR3JlR0drRFY2elFqIn0.G0M84XVAxSmzGY-NQuB9NBofNrINSn6lvxW6761Vlq6ypvYgtc2xNdpiRmw8ryVNfnpzrr4Z5cB1RlrC05rJAw'
400
+ const digitalCredential: AddCredentialArgs = {
401
+ rawDocument: rawCredential,
402
+ kmsKeyRef: 'testRef',
403
+ identifierMethod: 'did',
404
+ issuerCorrelationType: CredentialCorrelationType.DID,
405
+ subjectCorrelationType: CredentialCorrelationType.DID,
406
+ issuerCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
407
+ subjectCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
408
+ credentialRole: CredentialRole.VERIFIER,
409
+ tenantId: 'urn:uuid:nnag4b43-1e7a-98f8-a32c-a48dbc5b10mj',
410
+ }
411
+
412
+ const savedDigitalCredential: DigitalCredential = await digitalCredentialStore.addCredential(digitalCredential)
413
+
414
+ const result = await digitalCredentialStore.updateCredential({
415
+ hash: savedDigitalCredential.hash,
416
+ rpCorrelationId: 'did:example:rp',
417
+ rpCorrelationType: CredentialCorrelationType.DID,
418
+ })
419
+
420
+ expect(result.rpCorrelationId).toEqual('did:example:rp')
421
+ expect(result.rpCorrelationType).toEqual(CredentialCorrelationType.DID)
422
+ expect(result.hash).toEqual(savedDigitalCredential.hash)
423
+ })
424
+
425
+ it('should throw error when updating credential without id or hash', async (): Promise<void> => {
426
+ await expect(
427
+ digitalCredentialStore.updateCredential({
428
+ kmsKeyRef: 'newRef',
429
+ } as any),
430
+ ).rejects.toThrowError('No id or hash param is provided.')
431
+ })
432
+
433
+ it('should throw error when updating non-existent credential', async (): Promise<void> => {
434
+ await expect(
435
+ digitalCredentialStore.updateCredential({
436
+ id: 'non-existent-id',
437
+ kmsKeyRef: 'newRef',
438
+ }),
439
+ ).rejects.toThrowError('No credential found for args: {"id":"non-existent-id"}')
440
+ })
441
+
442
+ it('should preserve immutable fields when updating credential', async (): Promise<void> => {
443
+ const rawCredential: string =
444
+ 'eyJraWQiOiJkaWQ6a2V5Ono2TWtyaGt5M3B1c20yNk1laUZhWFUzbjJuZWtyYW13RlVtZ0dyZUdHa0RWNnpRaiN6Nk1rcmhreTNwdXNtMjZNZWlGYVhVM24ybmVrcmFtd0ZVbWdHcmVHR2tEVjZ6UWoiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJTIiwibGFzdE5hbWUiOiJLIiwiZW1haWxBZGRyZXNzIjoic0BrIn19LCJzdWIiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJqdGkiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJuYmYiOjE3MDg0NDA4MDgsImlzcyI6ImRpZDprZXk6ejZNa3Joa3kzcHVzbTI2TWVpRmFYVTNuMm5la3JhbXdGVW1nR3JlR0drRFY2elFqIn0.G0M84XVAxSmzGY-NQuB9NBofNrINSn6lvxW6761Vlq6ypvYgtc2xNdpiRmw8ryVNfnpzrr4Z5cB1RlrC05rJAw'
445
+ const digitalCredential: AddCredentialArgs = {
446
+ rawDocument: rawCredential,
447
+ kmsKeyRef: 'testRef',
448
+ identifierMethod: 'did',
449
+ issuerCorrelationType: CredentialCorrelationType.DID,
450
+ subjectCorrelationType: CredentialCorrelationType.DID,
451
+ issuerCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
452
+ subjectCorrelationId: 'did:key:z6Mkrhky3pusm26MeiFaXU3n2nekramwFUmgGreGGkDV6zQj',
453
+ credentialRole: CredentialRole.VERIFIER,
454
+ tenantId: 'urn:uuid:nnag4b43-1e7a-98f8-a32c-a48dbc5b10mj',
455
+ }
456
+
457
+ const savedDigitalCredential: DigitalCredential = await digitalCredentialStore.addCredential(digitalCredential)
458
+ const originalId = savedDigitalCredential.id
459
+ const originalHash = savedDigitalCredential.hash
460
+ const originalCreatedAt = savedDigitalCredential.createdAt
461
+
462
+ // Try to update with different id, hash, and createdAt - these should be ignored
463
+ const result = await digitalCredentialStore.updateCredential({
464
+ id: savedDigitalCredential.id,
465
+ kmsKeyRef: 'updatedRef',
466
+ hash: 'should-be-ignored' as any,
467
+ })
468
+
469
+ // Verify immutable fields remain unchanged
470
+ expect(result.id).toEqual(originalId)
471
+ expect(result.hash).toEqual(originalHash)
472
+ expect(result.createdAt).toEqual(originalCreatedAt)
473
+ // Verify mutable field was updated
474
+ expect(result.kmsKeyRef).toEqual('updatedRef')
475
+ })
476
+
362
477
  it('should throw exception on updating stored digital credential to revoked', async (): Promise<void> => {
363
478
  const rawCredential: string =
364
479
  'eyJraWQiOiJkaWQ6a2V5Ono2TWtyaGt5M3B1c20yNk1laUZhWFUzbjJuZWtyYW13RlVtZ0dyZUdHa0RWNnpRaiN6Nk1rcmhreTNwdXNtMjZNZWlGYVhVM24ybmVrcmFtd0ZVbWdHcmVHR2tEVjZ6UWoiLCJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vc3BoZXJlb24tb3BlbnNvdXJjZS5naXRodWIuaW8vc3NpLW1vYmlsZS13YWxsZXQvY29udGV4dC9zcGhlcmVvbi13YWxsZXQtaWRlbnRpdHktdjEuanNvbmxkIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTcGhlcmVvbldhbGxldElkZW50aXR5Q3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJmaXJzdE5hbWUiOiJTIiwibGFzdE5hbWUiOiJLIiwiZW1haWxBZGRyZXNzIjoic0BrIn19LCJzdWIiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJqdGkiOiJ1cm46dXVpZDpkZGE3YmYyNC04ZTdhLTQxZjgtYjY2Yy1hNDhkYmM1YjEwZmEiLCJuYmYiOjE3MDg0NDA4MDgsImlzcyI6ImRpZDprZXk6ejZNa3Joa3kzcHVzbTI2TWVpRmFYVTNuMm5la3JhbXdGVW1nR3JlR0drRFY2elFqIn0.G0M84XVAxSmzGY-NQuB9NBofNrINSn6lvxW6761Vlq6ypvYgtc2xNdpiRmw8ryVNfnpzrr4Z5cB1RlrC05rJAw'
@@ -8,13 +8,20 @@ import {
8
8
  GetCredentialsResponse,
9
9
  NonPersistedDigitalCredential,
10
10
  RemoveCredentialArgs,
11
+ UpdateCredentialArgs,
11
12
  UpdateCredentialStateArgs,
12
13
  } from '@sphereon/ssi-sdk.data-store-types'
13
14
  import { CredentialRole, OrPromise } from '@sphereon/ssi-types'
14
15
  import Debug from 'debug'
15
16
  import { DataSource, type FindOptionsOrder, type FindOptionsWhere, Repository } from 'typeorm'
16
17
 
17
- import { digitalCredentialFrom, digitalCredentialsFrom, nonPersistedDigitalCredentialEntityFromAddArgs } from '../../src'
18
+ import {
19
+ digitalCredentialFrom,
20
+ digitalCredentialsFrom,
21
+ nonPersistedDigitalCredentialEntityFromAddArgs,
22
+ persistedDigitalCredentialEntityFromStateArgs,
23
+ persistedDigitalCredentialEntityFromUpdateArgs,
24
+ } from '../../src'
18
25
  import { DigitalCredentialEntity } from '../entities/digitalCredential/DigitalCredentialEntity'
19
26
  import { parseAndValidateOrderOptions } from '../utils/SortingUtils'
20
27
 
@@ -37,7 +44,7 @@ export class DigitalCredentialStore extends AbstractDigitalCredentialStore {
37
44
  return Promise.reject(validationError)
38
45
  }
39
46
  const dcRepo = await this.getRepository()
40
- const createdResult: DigitalCredentialEntity = await dcRepo.save(credentialEntity)
47
+ const createdResult: DigitalCredentialEntity = await dcRepo.save(credentialEntity as any)
41
48
  return Promise.resolve(digitalCredentialFrom(createdResult))
42
49
  }
43
50
 
@@ -72,6 +79,41 @@ export class DigitalCredentialStore extends AbstractDigitalCredentialStore {
72
79
  }
73
80
  }
74
81
 
82
+ updateCredential = async (args: UpdateCredentialArgs): Promise<DigitalCredential> => {
83
+ const dcRepo = await this.getRepository()
84
+ const whereClause: Record<string, any> = {}
85
+
86
+ if ('id' in args) {
87
+ whereClause.id = args.id
88
+ } else if ('hash' in args) {
89
+ whereClause.hash = args.hash
90
+ } else {
91
+ return Promise.reject(Error('No id or hash param is provided.'))
92
+ }
93
+
94
+ const credential: DigitalCredentialEntity | null = await dcRepo.findOne({
95
+ where: whereClause,
96
+ })
97
+
98
+ if (!credential) {
99
+ return Promise.reject(Error(`No credential found for args: ${JSON.stringify(whereClause)}`))
100
+ }
101
+
102
+ // Extract updates by removing the identifier fields
103
+ const updates = Object.fromEntries(Object.entries(args).filter(([key]) => key !== 'id' && key !== 'hash')) as Partial<DigitalCredential>
104
+
105
+ const entityToSave = persistedDigitalCredentialEntityFromUpdateArgs(credential, updates)
106
+
107
+ const validationError = this.assertValidDigitalCredential(entityToSave)
108
+ if (validationError) {
109
+ return Promise.reject(validationError)
110
+ }
111
+
112
+ debug('Updating credential', entityToSave)
113
+ const updatedResult = await dcRepo.save(entityToSave as any, { transaction: true })
114
+ return digitalCredentialFrom(updatedResult)
115
+ }
116
+
75
117
  removeCredential = async (args: RemoveCredentialArgs): Promise<boolean> => {
76
118
  if (!args) {
77
119
  return false
@@ -147,20 +189,16 @@ export class DigitalCredentialStore extends AbstractDigitalCredentialStore {
147
189
  if (!credential) {
148
190
  return Promise.reject(Error(`No credential found for args: ${JSON.stringify(whereClause)}`))
149
191
  }
150
- const updatedCredential: DigitalCredential = {
151
- ...credential,
152
- ...(args.verifiedState !== CredentialStateType.REVOKED && { verifiedAt: args.verifiedAt }),
153
- ...(args.verifiedState === CredentialStateType.REVOKED && { revokedAt: args.revokedAt }),
154
- identifierMethod: credential.identifierMethod,
155
- lastUpdatedAt: new Date(),
156
- verifiedState: args.verifiedState,
157
- }
158
- debug('Updating credential', credential)
159
- const updatedResult: DigitalCredentialEntity = await credentialRepository.save(updatedCredential, { transaction: true })
192
+
193
+ // Create entity with state updates applied
194
+ const entityToSave = persistedDigitalCredentialEntityFromStateArgs(credential, args)
195
+
196
+ debug('Updating credential state', entityToSave)
197
+ const updatedResult: DigitalCredentialEntity = await credentialRepository.save(entityToSave as any, { transaction: true })
160
198
  return digitalCredentialFrom(updatedResult)
161
199
  }
162
200
 
163
- private assertValidDigitalCredential(credentialEntity: NonPersistedDigitalCredential): Error | undefined {
201
+ private assertValidDigitalCredential(credentialEntity: NonPersistedDigitalCredential | DigitalCredentialEntity): Error | undefined {
164
202
  const { kmsKeyRef, identifierMethod, credentialRole, isIssuerSigned } = credentialEntity
165
203
 
166
204
  const isRoleInvalid = credentialRole === CredentialRole.ISSUER || (credentialRole === CredentialRole.HOLDER && !isIssuerSigned)
@@ -76,6 +76,12 @@ export class DigitalCredentialEntity extends BaseEntity implements DigitalCreden
76
76
  @Column('text', { name: 'tenant_id', nullable: true })
77
77
  tenantId?: string
78
78
 
79
+ @Column('text', { name: 'linked_vp_id', nullable: true })
80
+ linkedVpId?: string
81
+
82
+ @CreateDateColumn({ name: 'linked_vp_from', nullable: true, type: typeOrmDateTime() })
83
+ linkedVpFrom?: Date
84
+
79
85
  @CreateDateColumn({ name: 'created_at', nullable: false, type: typeOrmDateTime() })
80
86
  createdAt!: Date
81
87
 
@@ -0,0 +1,66 @@
1
+ import Debug, { Debugger } from 'debug'
2
+ import { DatabaseType, MigrationInterface, QueryRunner } from 'typeorm'
3
+ import { AddLinkedVpFields1763387280001 } from '../postgres/1763387280001-AddLinkedVpFields'
4
+ import { AddLinkedVpFields1763387280002 } from '../sqlite/1763387280002-AddLinkedVpFields'
5
+
6
+ const debug: Debugger = Debug('sphereon:ssi-sdk:migrations')
7
+
8
+ export class AddLinkedVpFields1763387280000 implements MigrationInterface {
9
+ name: string = 'AddLinkedVpFields1763387280000'
10
+
11
+ public async up(queryRunner: QueryRunner): Promise<void> {
12
+ debug('migration: adding linked VP fields to DigitalCredential table')
13
+ const dbType: DatabaseType = queryRunner.connection.driver.options.type
14
+
15
+ switch (dbType) {
16
+ case 'postgres': {
17
+ debug('using postgres migration file for AddLinkedVpFields')
18
+ const mig: AddLinkedVpFields1763387280001 = new AddLinkedVpFields1763387280001()
19
+ await mig.up(queryRunner)
20
+ debug('Postgres migration statements for AddLinkedVpFields executed')
21
+ return
22
+ }
23
+ case 'sqlite':
24
+ case 'expo':
25
+ case 'react-native': {
26
+ debug('using sqlite/react-native migration file for AddLinkedVpFields')
27
+ const mig: AddLinkedVpFields1763387280002 = new AddLinkedVpFields1763387280002()
28
+ await mig.up(queryRunner)
29
+ debug('SQLite migration statements for AddLinkedVpFields executed')
30
+ return
31
+ }
32
+ default:
33
+ return Promise.reject(
34
+ `Migrations are currently only supported for sqlite, react-native, expo, and postgres for AddLinkedVpFields. Was ${dbType}. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now`,
35
+ )
36
+ }
37
+ }
38
+
39
+ public async down(queryRunner: QueryRunner): Promise<void> {
40
+ debug('migration: reverting linked VP fields from DigitalCredential table')
41
+ const dbType: DatabaseType = queryRunner.connection.driver.options.type
42
+
43
+ switch (dbType) {
44
+ case 'postgres': {
45
+ debug('using postgres migration file for AddLinkedVpFields')
46
+ const mig: AddLinkedVpFields1763387280001 = new AddLinkedVpFields1763387280001()
47
+ await mig.down(queryRunner)
48
+ debug('Postgres migration statements for AddLinkedVpFields reverted')
49
+ return
50
+ }
51
+ case 'sqlite':
52
+ case 'expo':
53
+ case 'react-native': {
54
+ debug('using sqlite/react-native migration file for AddLinkedVpFields')
55
+ const mig: AddLinkedVpFields1763387280002 = new AddLinkedVpFields1763387280002()
56
+ await mig.down(queryRunner)
57
+ debug('SQLite migration statements for AddLinkedVpFields reverted')
58
+ return
59
+ }
60
+ default:
61
+ return Promise.reject(
62
+ `Migrations are currently only supported for sqlite, react-native, expo, and postgres for AddLinkedVpFields. Was ${dbType}. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now`,
63
+ )
64
+ }
65
+ }
66
+ }
@@ -3,6 +3,7 @@ import { CreatePresentationDefinitions1716533767523 } from './10-CreatePresentat
3
3
  import { FixCredentialClaimsReferencesUuid1741895822987 } from './11-FixCredentialClaimsReferenceUuid'
4
4
  import { AddBitstringStatusListEnum1741895823000, CreateBitstringStatusList1741895823000 } from './12-CreateBitstringStatusList'
5
5
  import { CreateDcqlQueryItem1726617600000 } from './13-CreateDcqlQueryItem'
6
+ import { AddLinkedVpFields1763387280000 } from './14-AddLinkedVpFields'
6
7
  import { CreateIssuanceBranding1659463079429 } from './2-CreateIssuanceBranding'
7
8
  import { CreateContacts1690925872318 } from './3-CreateContacts'
8
9
  import { CreateStatusList1693866470000 } from './4-CreateStatusList'
@@ -34,7 +35,7 @@ export const DataStoreStatusListMigrations = [
34
35
  CreateBitstringStatusList1741895823000,
35
36
  ]
36
37
  export const DataStoreEventLoggerMigrations = [CreateAuditEvents1701635835330]
37
- export const DataStoreDigitalCredentialMigrations = [CreateDigitalCredential1708525189000]
38
+ export const DataStoreDigitalCredentialMigrations = [CreateDigitalCredential1708525189000, AddLinkedVpFields1763387280000]
38
39
  export const DataStoreMachineStateMigrations = [CreateMachineStateStore1708098041262]
39
40
  export const DataStorePresentationDefinitionMigrations = [CreatePresentationDefinitions1716533767523, CreateDcqlQueryItem1726617600000]
40
41
 
@@ -0,0 +1,29 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ export class AddLinkedVpFields1763387280001 implements MigrationInterface {
4
+ name = 'AddLinkedVpFields1763387280001'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`
8
+ ALTER TABLE "DigitalCredential"
9
+ ADD COLUMN "linked_vp_id" text
10
+ `)
11
+
12
+ await queryRunner.query(`
13
+ ALTER TABLE "DigitalCredential"
14
+ ADD COLUMN "linked_vp_from" TIMESTAMP
15
+ `)
16
+ }
17
+
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ await queryRunner.query(`
20
+ ALTER TABLE "DigitalCredential"
21
+ DROP COLUMN "linked_vp_from"
22
+ `)
23
+
24
+ await queryRunner.query(`
25
+ ALTER TABLE "DigitalCredential"
26
+ DROP COLUMN "linked_vp_id"
27
+ `)
28
+ }
29
+ }
@@ -0,0 +1,32 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ export class AddLinkedVpFields1763387280002 implements MigrationInterface {
4
+ name = 'AddLinkedVpFields1763387280002'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`
8
+ ALTER TABLE "DigitalCredential"
9
+ ADD COLUMN "linked_vp_id" text
10
+ `)
11
+
12
+ await queryRunner.query(`
13
+ ALTER TABLE "DigitalCredential"
14
+ ADD COLUMN "linked_vp_from" datetime
15
+ `)
16
+ }
17
+
18
+ public async down(queryRunner: QueryRunner): Promise<void> {
19
+ // SQLite doesn't support DROP COLUMN in older versions
20
+ // For production, you may need to recreate the table
21
+ // For now, we'll try the direct approach which works in SQLite 3.35.0+
22
+ await queryRunner.query(`
23
+ ALTER TABLE "DigitalCredential"
24
+ DROP COLUMN "linked_vp_from"
25
+ `)
26
+
27
+ await queryRunner.query(`
28
+ ALTER TABLE "DigitalCredential"
29
+ DROP COLUMN "linked_vp_id"
30
+ `)
31
+ }
32
+ }
@@ -1,6 +1,11 @@
1
1
  import { defaultHasher } from '@sphereon/ssi-sdk.core'
2
- import type { AddCredentialArgs, DigitalCredential, NonPersistedDigitalCredential } from '@sphereon/ssi-sdk.data-store-types'
3
- import { CredentialDocumentFormat, DocumentType, RegulationType } from '@sphereon/ssi-sdk.data-store-types'
2
+ import type {
3
+ AddCredentialArgs,
4
+ DigitalCredential,
5
+ NonPersistedDigitalCredential,
6
+ UpdateCredentialStateArgs,
7
+ } from '@sphereon/ssi-sdk.data-store-types'
8
+ import { CredentialDocumentFormat, CredentialStateType, DocumentType, RegulationType } from '@sphereon/ssi-sdk.data-store-types'
4
9
  import {
5
10
  CredentialMapper,
6
11
  DocumentFormat,
@@ -66,6 +71,20 @@ function determineCredentialDocumentFormat(documentFormat: DocumentFormat): Cred
66
71
  }
67
72
  }
68
73
 
74
+ /**
75
+ * Normalizes nullable fields by converting undefined to null.
76
+ * This ensures TypeORM actually clears the database fields instead of ignoring them.
77
+ */
78
+ export function normalizeNullableFields<T extends Record<string, any>>(obj: T, nullableKeys: Array<keyof T>): T {
79
+ const normalized = { ...obj }
80
+ for (const key of nullableKeys) {
81
+ if (normalized[key] === undefined) {
82
+ normalized[key] = null as any
83
+ }
84
+ }
85
+ return normalized
86
+ }
87
+
69
88
  function getValidUntil(uniformDocument: IVerifiableCredential | IVerifiablePresentation | SdJwtDecodedVerifiableCredentialPayload): Date | undefined {
70
89
  if ('expirationDate' in uniformDocument && uniformDocument.expirationDate) {
71
90
  return new Date(uniformDocument.expirationDate)
@@ -127,6 +146,54 @@ export const nonPersistedDigitalCredentialEntityFromAddArgs = (addCredentialArgs
127
146
  }
128
147
  }
129
148
 
149
+ export const persistedDigitalCredentialEntityFromUpdateArgs = (
150
+ existingCredential: DigitalCredentialEntity,
151
+ updates: Partial<DigitalCredential>,
152
+ ): DigitalCredentialEntity => {
153
+ const entity = new DigitalCredentialEntity()
154
+
155
+ // Copy all fields from existing credential
156
+ Object.assign(entity, existingCredential)
157
+
158
+ // Normalize nullable fields before applying updates
159
+ const normalizedUpdates = normalizeNullableFields(updates, ['linkedVpId', 'linkedVpFrom'])
160
+
161
+ // Apply updates
162
+ Object.assign(entity, normalizedUpdates)
163
+
164
+ // Ensure these fields are never overwritten
165
+ entity.id = existingCredential.id
166
+ entity.hash = existingCredential.hash
167
+ entity.createdAt = existingCredential.createdAt
168
+ entity.lastUpdatedAt = new Date()
169
+
170
+ return entity
171
+ }
172
+
173
+ export const persistedDigitalCredentialEntityFromStateArgs = (
174
+ existingCredential: DigitalCredentialEntity,
175
+ args: UpdateCredentialStateArgs,
176
+ ): DigitalCredentialEntity => {
177
+ const entity = new DigitalCredentialEntity()
178
+
179
+ // Copy all fields from existing credential
180
+ Object.assign(entity, existingCredential)
181
+
182
+ // Apply state updates
183
+ entity.verifiedState = args.verifiedState
184
+ entity.lastUpdatedAt = new Date()
185
+
186
+ if (args.verifiedState === CredentialStateType.REVOKED && args.revokedAt) {
187
+ entity.revokedAt = args.revokedAt
188
+ }
189
+
190
+ if (args.verifiedState !== CredentialStateType.REVOKED && args.verifiedAt) {
191
+ entity.verifiedAt = args.verifiedAt
192
+ }
193
+
194
+ return entity
195
+ }
196
+
130
197
  export const digitalCredentialFrom = (credentialEntity: DigitalCredentialEntity): DigitalCredential => {
131
198
  const result: DigitalCredential = {
132
199
  ...credentialEntity,