@@ -11,26 +11,92 @@ import NotificationService from "../../services/NotificationService";
1111import type { KeyService } from "./key" ;
1212import type { UserController } from "./user" ;
1313
14- const STORE_META_ENVELOPE = `
15- mutation StoreMetaEnvelope($input: MetaEnvelopeInput!) {
16- storeMetaEnvelope(input: $input) {
14+ const USER_PROFILE_ONTOLOGY = "550e8400-e29b-41d4-a716-446655440000" ;
15+
16+ const FIND_USER_PROFILE = `
17+ query FindUserProfile($ontologyId: String!) {
18+ metaEnvelopes(filter: { ontologyId: $ontologyId }, first: 1) {
19+ edges {
20+ node {
21+ id
22+ ontology
23+ parsed
24+ }
25+ }
26+ }
27+ }
28+ ` ;
29+
30+ const CREATE_META_ENVELOPE = `
31+ mutation CreateMetaEnvelope($input: MetaEnvelopeInput!) {
32+ createMetaEnvelope(input: $input) {
33+ metaEnvelope {
34+ id
35+ ontology
36+ parsed
37+ }
38+ errors {
39+ message
40+ code
41+ }
42+ }
43+ }
44+ ` ;
45+
46+ const UPDATE_META_ENVELOPE = `
47+ mutation UpdateMetaEnvelope($id: ID!, $input: MetaEnvelopeInput!) {
48+ updateMetaEnvelope(id: $id, input: $input) {
1749 metaEnvelope {
1850 id
1951 ontology
2052 parsed
2153 }
54+ errors {
55+ message
56+ code
57+ }
2258 }
2359 }
2460` ;
2561
26- interface MetaEnvelopeResponse {
27- storeMetaEnvelope : {
62+ interface GraphQLErrorItem {
63+ message : string ;
64+ code ?: string ;
65+ }
66+
67+ interface ExistingUserProfileEnvelope {
68+ id : string ;
69+ ontology : string ;
70+ parsed ?: Partial < UserProfile > ;
71+ }
72+
73+ interface FindUserProfileResponse {
74+ metaEnvelopes : {
75+ edges : Array < {
76+ node : ExistingUserProfileEnvelope ;
77+ } > ;
78+ } ;
79+ }
80+
81+ interface CreateMetaEnvelopeResponse {
82+ createMetaEnvelope : {
2883 metaEnvelope : {
2984 id : string ;
3085 ontology : string ;
31- // biome-ignore lint/suspicious/noExplicitAny: <explanation>
32- parsed : any ;
33- } ;
86+ parsed : UserProfile ;
87+ } | null ;
88+ errors : GraphQLErrorItem [ ] | null ;
89+ } ;
90+ }
91+
92+ interface UpdateMetaEnvelopeResponse {
93+ updateMetaEnvelope : {
94+ metaEnvelope : {
95+ id : string ;
96+ ontology : string ;
97+ parsed : UserProfile ;
98+ } | null ;
99+ errors : GraphQLErrorItem [ ] | null ;
34100 } ;
35101}
36102
@@ -158,7 +224,7 @@ export class VaultController {
158224 const userData = await this . #userController. user ;
159225 const displayName = userData ?. name || vault . ename ;
160226
161- await this . createUserProfileInEVault (
227+ await this . upsertUserProfileInEVault (
162228 vault . ename ,
163229 displayName ,
164230 vault . ename ,
@@ -167,7 +233,7 @@ export class VaultController {
167233 this . profileCreationStatus = "success" ;
168234 } catch ( error ) {
169235 console . error (
170- "Failed to create UserProfile in eVault (retry):" ,
236+ "Failed to upsert UserProfile in eVault (retry):" ,
171237 error ,
172238 ) ;
173239 this . profileCreationStatus = "failed" ;
@@ -278,71 +344,133 @@ export class VaultController {
278344 return this . #client;
279345 }
280346
281- /**
282- * Create UserProfile in eVault with retry mechanism
283- */
284- private async createUserProfileInEVault (
347+ private buildUserProfilePayload (
285348 ename : string ,
286349 displayName : string ,
287- w3id : string ,
288- maxRetries = 10 ,
289- ) : Promise < void > {
290- console . log ( "attempting" ) ;
350+ now : string ,
351+ existingProfile ?: Partial < UserProfile > ,
352+ ) : UserProfile {
291353 const username = ename . replace ( "@" , "" ) ;
292- const now = new Date ( ) . toISOString ( ) ;
293-
294- const userProfile : UserProfile = {
295- username,
354+ return {
355+ username : existingProfile ?. username ?? username ,
296356 displayName,
297- bio : null ,
298- avatarUrl : null ,
299- bannerUrl : null ,
300- ename,
301- isVerified : false ,
302- isPrivate : false ,
303- createdAt : now ,
357+ bio : existingProfile ?. bio ?? null ,
358+ avatarUrl : existingProfile ?. avatarUrl ?? null ,
359+ bannerUrl : existingProfile ?. bannerUrl ?? null ,
360+ ename : existingProfile ?. ename ?? ename ,
361+ isVerified : existingProfile ?. isVerified ?? false ,
362+ isPrivate : existingProfile ?. isPrivate ?? false ,
363+ createdAt : existingProfile ?. createdAt ?? now ,
304364 updatedAt : now ,
305- isArchived : false ,
365+ isArchived : existingProfile ?. isArchived ?? false ,
306366 } ;
367+ }
368+
369+ private async findExistingUserProfile (
370+ client : GraphQLClient ,
371+ ) : Promise < ExistingUserProfileEnvelope | undefined > {
372+ const response = await client . request < FindUserProfileResponse > (
373+ FIND_USER_PROFILE ,
374+ {
375+ ontologyId : USER_PROFILE_ONTOLOGY ,
376+ } ,
377+ ) ;
378+ return response . metaEnvelopes . edges [ 0 ] ?. node ;
379+ }
380+
381+ private throwIfGraphQLErrors (
382+ errors : GraphQLErrorItem [ ] | null | undefined ,
383+ operation : "create" | "update" ,
384+ ) {
385+ if ( ! errors ?. length ) return ;
386+ const message = errors [ 0 ] ?. message ?? "Unknown GraphQL error" ;
387+ throw new Error ( `[UserProfile ${ operation } ] ${ message } ` ) ;
388+ }
307389
390+ /**
391+ * Upsert UserProfile in eVault with retry mechanism.
392+ * Lookup is performed on each attempt to avoid creating duplicates.
393+ */
394+ private async upsertUserProfileInEVault (
395+ ename : string ,
396+ displayName : string ,
397+ w3id : string ,
398+ maxRetries = 10 ,
399+ ) : Promise < void > {
308400 for ( let attempt = 1 ; attempt <= maxRetries ; attempt ++ ) {
309401 try {
402+ const now = new Date ( ) . toISOString ( ) ;
310403 const client = await this . ensureClient ( w3id , ename ) ;
404+ const existingProfile = await this . findExistingUserProfile ( client ) ;
405+ const payload = this . buildUserProfilePayload (
406+ ename ,
407+ displayName ,
408+ now ,
409+ existingProfile ?. parsed ,
410+ ) ;
411+
412+ if ( existingProfile ?. id ) {
413+ console . log (
414+ `Attempting to update existing UserProfile in eVault (attempt ${ attempt } /${ maxRetries } )` ,
415+ ) ;
416+ const response = await client . request < UpdateMetaEnvelopeResponse > (
417+ UPDATE_META_ENVELOPE ,
418+ {
419+ id : existingProfile . id ,
420+ input : {
421+ ontology : USER_PROFILE_ONTOLOGY ,
422+ payload,
423+ acl : [ "*" ] ,
424+ } ,
425+ } ,
426+ ) ;
427+ this . throwIfGraphQLErrors (
428+ response . updateMetaEnvelope . errors ,
429+ "update" ,
430+ ) ;
431+ console . log (
432+ "UserProfile updated successfully in eVault:" ,
433+ response . updateMetaEnvelope . metaEnvelope ?. id ??
434+ existingProfile . id ,
435+ ) ;
436+ return ;
437+ }
311438
312439 console . log (
313440 `Attempting to create UserProfile in eVault (attempt ${ attempt } /${ maxRetries } )` ,
314441 ) ;
315-
316- const response = await client . request < MetaEnvelopeResponse > (
317- STORE_META_ENVELOPE ,
442+ const response = await client . request < CreateMetaEnvelopeResponse > (
443+ CREATE_META_ENVELOPE ,
318444 {
319445 input : {
320- ontology : "550e8400-e29b-41d4-a716-446655440000" ,
321- payload : userProfile ,
446+ ontology : USER_PROFILE_ONTOLOGY ,
447+ payload,
322448 acl : [ "*" ] ,
323449 } ,
324450 } ,
325451 ) ;
326-
452+ this . throwIfGraphQLErrors (
453+ response . createMetaEnvelope . errors ,
454+ "create" ,
455+ ) ;
327456 console . log (
328457 "UserProfile created successfully in eVault:" ,
329- response . storeMetaEnvelope . metaEnvelope . id ,
458+ response . createMetaEnvelope . metaEnvelope ? .id ?? "unknown-id" ,
330459 ) ;
331460 return ;
332461 } catch ( error ) {
333462 console . error (
334- `Failed to create UserProfile in eVault (attempt ${ attempt } /${ maxRetries } ):` ,
463+ `Failed to upsert UserProfile in eVault (attempt ${ attempt } /${ maxRetries } ):` ,
335464 error ,
336465 ) ;
337466
338467 if ( attempt === maxRetries ) {
339468 console . error (
340- "Max retries reached, giving up on UserProfile creation " ,
469+ "Max retries reached, giving up on UserProfile upsert " ,
341470 ) ;
342471 throw error ;
343472 }
344473
345- // Wait before retrying (exponential backoff)
346474 const delay = Math . min ( 1000 * 2 ** ( attempt - 1 ) , 10000 ) ;
347475 console . log ( `Waiting ${ delay } ms before retry...` ) ;
348476 await new Promise ( ( resolve ) => setTimeout ( resolve , delay ) ) ;
@@ -375,15 +503,15 @@ export class VaultController {
375503 if ( this . profileCreationStatus === "success" )
376504 return ;
377505 this . profileCreationStatus = "loading" ;
378- await this . createUserProfileInEVault (
506+ await this . upsertUserProfileInEVault (
379507 resolvedUser ?. ename as string ,
380508 displayName as string ,
381509 resolvedUser ?. ename as string ,
382510 ) ;
383511 this . profileCreationStatus = "success" ;
384512 } catch ( error ) {
385513 console . error (
386- "Failed to create UserProfile in eVault:" ,
514+ "Failed to upsert UserProfile in eVault:" ,
387515 error ,
388516 ) ;
389517 this . profileCreationStatus = "failed" ;
@@ -407,34 +535,34 @@ export class VaultController {
407535 // Set loading status
408536 this . profileCreationStatus = "loading" ;
409537
410- // Get user data for display name and create UserProfile
538+ // Get user data for display name and upsert UserProfile
411539 ( async ( ) => {
412540 try {
413541 const userData = await this . #userController. user ;
414542 const displayName = userData ?. name || vault . ename ;
415543
416- await this . createUserProfileInEVault (
544+ await this . upsertUserProfileInEVault (
417545 vault . ename ,
418546 displayName ,
419547 vault . ename ,
420548 ) ;
421549 this . profileCreationStatus = "success" ;
422550 } catch ( error ) {
423551 console . error (
424- "Failed to get user data or create UserProfile:" ,
552+ "Failed to get user data or upsert UserProfile:" ,
425553 error ,
426554 ) ;
427555 // Fallback to using ename as display name
428556 try {
429- await this . createUserProfileInEVault (
557+ await this . upsertUserProfileInEVault (
430558 vault . ename ,
431559 vault . ename ,
432560 vault . ename ,
433561 ) ;
434562 this . profileCreationStatus = "success" ;
435563 } catch ( fallbackError ) {
436564 console . error (
437- "Failed to create UserProfile in eVault (fallback):" ,
565+ "Failed to upsert UserProfile in eVault (fallback):" ,
438566 fallbackError ,
439567 ) ;
440568 this . profileCreationStatus = "failed" ;
0 commit comments