@sphereon/ssi-sdk.data-store 0.36.1-next.115 → 0.36.1-next.129

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.
@@ -44,6 +44,7 @@ import {
44
44
  credentialBrandingEntityFrom,
45
45
  credentialBrandingFrom,
46
46
  credentialLocaleBrandingEntityFrom,
47
+ credentialLocaleBrandingFromEntity,
47
48
  issuerBrandingEntityFrom,
48
49
  issuerBrandingFrom,
49
50
  issuerLocaleBrandingEntityFrom,
@@ -83,7 +84,7 @@ export class IssuanceBrandingStore extends AbstractIssuanceBrandingStore {
83
84
  }
84
85
 
85
86
  public getCredentialBranding = async (args?: IGetCredentialBrandingArgs): Promise<Array<ICredentialBranding>> => {
86
- const { filter } = args ?? {}
87
+ const { filter, knownStates } = args ?? {}
87
88
  if (filter) {
88
89
  filter.forEach((filter: IPartialCredentialBranding): void => {
89
90
  if (filter.localeBranding && 'locale' in filter.localeBranding && filter.localeBranding.locale === undefined) {
@@ -93,11 +94,48 @@ export class IssuanceBrandingStore extends AbstractIssuanceBrandingStore {
93
94
  }
94
95
 
95
96
  debug('Getting credential branding', args)
96
- const result: Array<CredentialBrandingEntity> = await (await this.dbConnection).getRepository(CredentialBrandingEntity).find({
97
+ const repository: Repository<CredentialBrandingEntity> = (await this.dbConnection).getRepository(CredentialBrandingEntity)
98
+
99
+ if (knownStates && Object.keys(knownStates).length > 0) {
100
+ // First do a lightweight query selecting only id and state to determine which records are "dirty"
101
+ const stateQuery = repository
102
+ .createQueryBuilder('branding')
103
+ .select(['branding.id', 'branding.state'])
104
+ if (filter) {
105
+ stateQuery.where(filter)
106
+ }
107
+ const stateResults: Array<{ id: string; state: string }> = await stateQuery.getRawMany().then((rows) =>
108
+ rows.map((row) => ({
109
+ id: row.branding_id,
110
+ state: row.branding_state,
111
+ })),
112
+ )
113
+
114
+ // Filter to find dirty record ids (new or changed state)
115
+ const dirtyIds: Array<string> = stateResults
116
+ .filter((result) => {
117
+ const knownState: string | undefined = knownStates[result.id]
118
+ return !knownState || knownState !== result.state
119
+ })
120
+ .map((result) => result.id)
121
+
122
+ if (dirtyIds.length === 0) {
123
+ return []
124
+ }
125
+
126
+ // Only fetch full data for dirty records
127
+ const result: Array<CredentialBrandingEntity> = await repository.find({
128
+ where: { id: In(dirtyIds) },
129
+ })
130
+
131
+ return result.map((branding: CredentialBrandingEntity) => credentialBrandingFrom(branding))
132
+ }
133
+
134
+ const result: Array<CredentialBrandingEntity> = await repository.find({
97
135
  ...(filter && { where: filter }),
98
136
  })
99
137
 
100
- return result.map((credentialBranding: CredentialBrandingEntity) => credentialBrandingFrom(credentialBranding))
138
+ return result.map((branding: CredentialBrandingEntity) => credentialBrandingFrom(branding))
101
139
  }
102
140
 
103
141
  public removeCredentialBranding = async (args: IRemoveCredentialBrandingArgs): Promise<void> => {
@@ -133,7 +171,7 @@ export class IssuanceBrandingStore extends AbstractIssuanceBrandingStore {
133
171
  return Promise.reject(Error(`No credential branding found for id: ${credentialBranding.id}`))
134
172
  }
135
173
 
136
- const branding: Omit<ICredentialBranding, 'createdAt' | 'lastUpdatedAt'> = {
174
+ const branding: Omit<ICredentialBranding, 'createdAt' | 'lastUpdatedAt' | 'state'> = {
137
175
  ...credentialBranding,
138
176
  localeBranding: credentialBrandingEntity.localeBranding,
139
177
  }
@@ -218,7 +256,8 @@ export class IssuanceBrandingStore extends AbstractIssuanceBrandingStore {
218
256
 
219
257
  return credentialBrandingLocale
220
258
  ? credentialBrandingLocale.map(
221
- (credentialLocaleBranding: CredentialLocaleBrandingEntity) => localeBrandingFrom(credentialLocaleBranding) as ICredentialLocaleBranding,
259
+ (credentialLocaleBranding: CredentialLocaleBrandingEntity) =>
260
+ credentialLocaleBrandingFromEntity(credentialLocaleBranding) as ICredentialLocaleBranding,
222
261
  )
223
262
  : []
224
263
  }
@@ -342,7 +381,7 @@ export class IssuanceBrandingStore extends AbstractIssuanceBrandingStore {
342
381
  return Promise.reject(Error(`No issuer branding found for id: ${issuerBranding.id}`))
343
382
  }
344
383
 
345
- const branding: Omit<IIssuerBranding, 'createdAt' | 'lastUpdatedAt'> = {
384
+ const branding: Omit<IIssuerBranding, 'createdAt' | 'lastUpdatedAt' | 'state'> = {
346
385
  ...issuerBranding,
347
386
  localeBranding: issuerBrandingEntity.localeBranding,
348
387
  }
@@ -0,0 +1,64 @@
1
+ import Debug from 'debug'
2
+ import { DatabaseType, MigrationInterface, QueryRunner } from 'typeorm'
3
+ import { AddBrandingStatePostgres1766000000000 } from '../postgres/1766000000000-AddBrandingState'
4
+ import { AddBrandingStateSqlite1766000000000 } from '../sqlite/1766000000000-AddBrandingState'
5
+
6
+ const debug: Debug.Debugger = Debug('sphereon:ssi-sdk:migrations')
7
+
8
+ export class AddBrandingState1766000000000 implements MigrationInterface {
9
+ name = 'AddBrandingState1766000000000'
10
+
11
+ public async up(queryRunner: QueryRunner): Promise<void> {
12
+ debug('migration: adding branding state checksum columns')
13
+ const dbType: DatabaseType = queryRunner.connection.driver.options.type
14
+ switch (dbType) {
15
+ case 'postgres': {
16
+ debug('using postgres migration file')
17
+ const mig: AddBrandingStatePostgres1766000000000 = new AddBrandingStatePostgres1766000000000()
18
+ await mig.up(queryRunner)
19
+ debug('Migration statements executed')
20
+ return
21
+ }
22
+ case 'sqlite':
23
+ case 'expo':
24
+ case 'react-native': {
25
+ debug('using sqlite/react-native migration file')
26
+ const mig: AddBrandingStateSqlite1766000000000 = new AddBrandingStateSqlite1766000000000()
27
+ await mig.up(queryRunner)
28
+ debug('Migration statements executed')
29
+ return
30
+ }
31
+ default:
32
+ return Promise.reject(
33
+ `Migrations are currently only supported for sqlite, react-native, expo and postgres. Was ${dbType}. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now`,
34
+ )
35
+ }
36
+ }
37
+
38
+ public async down(queryRunner: QueryRunner): Promise<void> {
39
+ debug('migration: removing branding state checksum columns')
40
+ const dbType: DatabaseType = queryRunner.connection.driver.options.type
41
+ switch (dbType) {
42
+ case 'postgres': {
43
+ debug('using postgres migration file')
44
+ const mig: AddBrandingStatePostgres1766000000000 = new AddBrandingStatePostgres1766000000000()
45
+ await mig.down(queryRunner)
46
+ debug('Migration statements executed')
47
+ return
48
+ }
49
+ case 'sqlite':
50
+ case 'expo':
51
+ case 'react-native': {
52
+ debug('using sqlite/react-native migration file')
53
+ const mig: AddBrandingStateSqlite1766000000000 = new AddBrandingStateSqlite1766000000000()
54
+ await mig.down(queryRunner)
55
+ debug('Migration statements executed')
56
+ return
57
+ }
58
+ default:
59
+ return Promise.reject(
60
+ `Migrations are currently only supported for sqlite, react-native, expo and postgres. Was ${dbType}. Please run your database without migrations and with 'migrationsRun: false' and 'synchronize: true' for now`,
61
+ )
62
+ }
63
+ }
64
+ }
@@ -4,6 +4,7 @@ import { FixCredentialClaimsReferencesUuid1741895822987 } from './11-FixCredenti
4
4
  import { AddBitstringStatusListEnum1741895823000, CreateBitstringStatusList1741895823000 } from './12-CreateBitstringStatusList'
5
5
  import { CreateDcqlQueryItem1726617600000 } from './13-CreateDcqlQueryItem'
6
6
  import { AddLinkedVpFields1763387280000 } from './14-AddLinkedVpFields'
7
+ import { AddBrandingState1766000000000 } from './15-AddBrandingState'
7
8
  import { CreateIssuanceBranding1659463079429 } from './2-CreateIssuanceBranding'
8
9
  import { CreateContacts1690925872318 } from './3-CreateContacts'
9
10
  import { CreateStatusList1693866470000 } from './4-CreateStatusList'
@@ -28,7 +29,11 @@ export const DataStoreContactMigrations = [
28
29
  CreateContacts1708525189000,
29
30
  CreateContacts1715761125000,
30
31
  ]
31
- export const DataStoreIssuanceBrandingMigrations = [CreateIssuanceBranding1659463079429, FixCredentialClaimsReferencesUuid1741895822987]
32
+ export const DataStoreIssuanceBrandingMigrations = [
33
+ CreateIssuanceBranding1659463079429,
34
+ FixCredentialClaimsReferencesUuid1741895822987,
35
+ AddBrandingState1766000000000,
36
+ ]
32
37
  export const DataStoreStatusListMigrations = [
33
38
  CreateStatusList1693866470000,
34
39
  AddBitstringStatusListEnum1741895823000,
@@ -0,0 +1,15 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ export class AddBrandingStatePostgres1766000000000 implements MigrationInterface {
4
+ name = 'AddBrandingState1766000000000'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(`ALTER TABLE "CredentialBranding" ADD "state" character varying(255) NOT NULL DEFAULT ''`)
8
+ await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" ADD "state" character varying(255) NOT NULL DEFAULT ''`)
9
+ }
10
+
11
+ public async down(queryRunner: QueryRunner): Promise<void> {
12
+ await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" DROP COLUMN "state"`)
13
+ await queryRunner.query(`ALTER TABLE "CredentialBranding" DROP COLUMN "state"`)
14
+ }
15
+ }
@@ -0,0 +1,87 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ export class AddBrandingStateSqlite1766000000000 implements MigrationInterface {
4
+ name = 'AddBrandingState1766000000000'
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ // Add state column with empty string default for existing records
8
+ // Note: Existing records will have state='', which won't match computed hashes.
9
+ // This makes the knownStates optimization ineffective until records are naturally updated.
10
+ await queryRunner.query(`ALTER TABLE "CredentialBranding" ADD COLUMN "state" varchar(255) NOT NULL DEFAULT ''`)
11
+ await queryRunner.query(`ALTER TABLE "BaseLocaleBranding" ADD COLUMN "state" varchar(255) NOT NULL DEFAULT ''`)
12
+ }
13
+
14
+ public async down(queryRunner: QueryRunner): Promise<void> {
15
+ // Disable foreign key constraints during migration to avoid issues with DROP TABLE operations
16
+ await queryRunner.query(`PRAGMA foreign_keys = OFF`)
17
+
18
+ // Recreate CredentialBranding without the state column
19
+ await queryRunner.query(`
20
+ CREATE TABLE "CredentialBranding_old"
21
+ (
22
+ "id" varchar PRIMARY KEY NOT NULL,
23
+ "vcHash" varchar(255) NOT NULL,
24
+ "issuerCorrelationId" varchar(255) NOT NULL,
25
+ "created_at" datetime NOT NULL DEFAULT (datetime('now')),
26
+ "last_updated_at" datetime NOT NULL DEFAULT (datetime('now'))
27
+ )
28
+ `)
29
+ await queryRunner.query(`
30
+ INSERT INTO "CredentialBranding_old" ("id", "vcHash", "issuerCorrelationId", "created_at", "last_updated_at")
31
+ SELECT "id", "vcHash", "issuerCorrelationId", "created_at", "last_updated_at"
32
+ FROM "CredentialBranding"
33
+ `)
34
+ await queryRunner.query(`DROP TABLE "CredentialBranding"`)
35
+ await queryRunner.query(`ALTER TABLE "CredentialBranding_old" RENAME TO "CredentialBranding"`)
36
+ await queryRunner.query(`CREATE INDEX "IDX_CredentialBrandingEntity_issuerCorrelationId" ON "CredentialBranding" ("issuerCorrelationId")`)
37
+ await queryRunner.query(`CREATE INDEX "IDX_CredentialBrandingEntity_vcHash" ON "CredentialBranding" ("vcHash")`)
38
+
39
+ // Recreate BaseLocaleBranding without the state column
40
+ await queryRunner.query(`
41
+ CREATE TABLE "BaseLocaleBranding_old"
42
+ (
43
+ "id" varchar PRIMARY KEY NOT NULL,
44
+ "alias" varchar(255),
45
+ "locale" varchar(255) NOT NULL,
46
+ "description" varchar(255),
47
+ "created_at" datetime NOT NULL DEFAULT (datetime('now')),
48
+ "last_updated_at" datetime NOT NULL DEFAULT (datetime('now')),
49
+ "credentialBrandingId" varchar,
50
+ "issuerBrandingId" varchar,
51
+ "type" varchar NOT NULL,
52
+ "logoId" varchar,
53
+ "backgroundId" varchar,
54
+ "textId" varchar,
55
+ "client_uri" varchar,
56
+ "tos_uri" varchar,
57
+ "policy_uri" varchar,
58
+ "contacts" varchar,
59
+ CONSTRAINT "UQ_logoId" UNIQUE ("logoId"),
60
+ CONSTRAINT "UQ_backgroundId" UNIQUE ("backgroundId"),
61
+ CONSTRAINT "UQ_textId" UNIQUE ("textId"),
62
+ CONSTRAINT "FK_BaseLocaleBranding_logoId" FOREIGN KEY ("logoId") REFERENCES "ImageAttributes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
63
+ CONSTRAINT "FK_BaseLocaleBranding_backgroundId" FOREIGN KEY ("backgroundId") REFERENCES "BackgroundAttributes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
64
+ CONSTRAINT "FK_BaseLocaleBranding_textId" FOREIGN KEY ("textId") REFERENCES "TextAttributes" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
65
+ CONSTRAINT "FK_BaseLocaleBranding_credentialBrandingId" FOREIGN KEY ("credentialBrandingId") REFERENCES "CredentialBranding" ("id") ON DELETE CASCADE ON UPDATE NO ACTION,
66
+ CONSTRAINT "FK_BaseLocaleBranding_issuerBrandingId" FOREIGN KEY ("issuerBrandingId") REFERENCES "IssuerBranding" ("id") ON DELETE CASCADE ON UPDATE NO ACTION
67
+ )
68
+ `)
69
+ await queryRunner.query(`
70
+ INSERT INTO "BaseLocaleBranding_old" ("id", "alias", "locale", "description", "created_at", "last_updated_at", "credentialBrandingId", "issuerBrandingId", "type", "logoId", "backgroundId", "textId", "client_uri", "tos_uri", "policy_uri", "contacts")
71
+ SELECT "id", "alias", "locale", "description", "created_at", "last_updated_at", "credentialBrandingId", "issuerBrandingId", "type", "logoId", "backgroundId", "textId", "client_uri", "tos_uri", "policy_uri", "contacts"
72
+ FROM "BaseLocaleBranding"
73
+ `)
74
+ await queryRunner.query(`DROP TABLE "BaseLocaleBranding"`)
75
+ await queryRunner.query(`ALTER TABLE "BaseLocaleBranding_old" RENAME TO "BaseLocaleBranding"`)
76
+ await queryRunner.query(
77
+ `CREATE UNIQUE INDEX "IDX_CredentialLocaleBrandingEntity_credentialBranding_locale" ON "BaseLocaleBranding" ("credentialBrandingId", "locale")`,
78
+ )
79
+ await queryRunner.query(
80
+ `CREATE UNIQUE INDEX "IDX_IssuerLocaleBrandingEntity_issuerBranding_locale" ON "BaseLocaleBranding" ("issuerBrandingId", "locale")`,
81
+ )
82
+ await queryRunner.query(`CREATE INDEX "IDX_BaseLocaleBranding_type" ON "BaseLocaleBranding" ("type")`)
83
+
84
+ // Re-enable foreign key constraints
85
+ await queryRunner.query(`PRAGMA foreign_keys = ON`)
86
+ }
87
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Computes a compact hash suitable for change detection in branding data.
3
+ * Uses FNV-1a 64-bit hash algorithm with base36 encoding for a compact representation.
4
+ *
5
+ * Output format: ~11 characters (base36 encoded 64-bit hash)
6
+ * Example: "3qvf8n2kl9x"
7
+ *
8
+ * This is significantly smaller than SHA256+base64 (44 chars) while maintaining
9
+ * sufficient collision resistance for this use case.
10
+ *
11
+ * @param input - The string to hash
12
+ * @returns A compact hash string (~11 characters)
13
+ */
14
+
15
+ // FNV-1a 64-bit hash constants
16
+ const FNV_PRIME = 0x100000001b3n
17
+ const OFFSET_BASIS = 0xcbf29ce484222325n
18
+
19
+ export function computeCompactHash(input: string): string {
20
+ let hash = OFFSET_BASIS
21
+
22
+ for (let i = 0; i < input.length; i++) {
23
+ hash ^= BigInt(input.charCodeAt(i))
24
+ hash = (hash * FNV_PRIME) & 0xffffffffffffffffn // Keep it 64-bit
25
+ }
26
+
27
+ // Convert to base36 for compact representation
28
+ // Base36 uses 0-9 and a-z, URL-safe and case-insensitive friendly
29
+ return hash.toString(36)
30
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  IBasicIssuerLocaleBranding,
10
10
  IBasicTextAttributes,
11
11
  ICredentialBranding,
12
+ ICredentialLocaleBranding,
12
13
  IIssuerBranding,
13
14
  ILocaleBranding,
14
15
  } from '@sphereon/ssi-sdk.data-store-types'
@@ -28,7 +29,9 @@ import { replaceNullWithUndefined } from '../FormattingUtils'
28
29
  export const credentialBrandingFrom = (credentialBranding: CredentialBrandingEntity): ICredentialBranding => {
29
30
  const result: ICredentialBranding = {
30
31
  ...credentialBranding,
31
- localeBranding: credentialBranding.localeBranding.map((localeBranding: BaseLocaleBrandingEntity) => localeBrandingFrom(localeBranding)),
32
+ localeBranding: credentialBranding.localeBranding.map((localeBranding: CredentialLocaleBrandingEntity) =>
33
+ credentialLocaleBrandingFromEntity(localeBranding),
34
+ ),
32
35
  }
33
36
 
34
37
  return replaceNullWithUndefined(result)
@@ -52,6 +55,23 @@ export const localeBrandingFrom = (localeBranding: BaseLocaleBrandingEntity): IL
52
55
  return replaceNullWithUndefined(result)
53
56
  }
54
57
 
58
+ export const credentialLocaleBrandingFromEntity = (localeBranding: CredentialLocaleBrandingEntity): ICredentialLocaleBranding => {
59
+ const base: ILocaleBranding = localeBrandingFrom(localeBranding)
60
+ const result: ICredentialLocaleBranding = {
61
+ ...(base as ILocaleBranding),
62
+ state: localeBranding.state,
63
+ claims: localeBranding.claims
64
+ ? localeBranding.claims.map((claim: CredentialClaimsEntity) => ({
65
+ id: claim.id,
66
+ key: claim.key,
67
+ name: claim.name,
68
+ }))
69
+ : undefined,
70
+ } as ICredentialLocaleBranding
71
+
72
+ return replaceNullWithUndefined(result) as ICredentialLocaleBranding
73
+ }
74
+
55
75
  export const issuerLocaleBrandingEntityFrom = (args: IBasicIssuerLocaleBranding): IssuerLocaleBrandingEntity => {
56
76
  const issuerLocaleBrandingEntity: IssuerLocaleBrandingEntity = new IssuerLocaleBrandingEntity()
57
77
  issuerLocaleBrandingEntity.alias = isEmptyString(args.alias) ? undefined : args.alias