@cooperation/vc-storage 1.0.26 → 1.0.27
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.
@@ -208,7 +208,7 @@ export class GoogleDriveStorage {
|
|
208
208
|
throw new Error('Folder ID is required to save a file.');
|
209
209
|
}
|
210
210
|
const fileMetadata = {
|
211
|
-
name: data.fileName || '
|
211
|
+
name: data.fileName || data.name + '.json' || data.credentialSubject.person.name.formattedName + '.json' || 'Untitled file.json',
|
212
212
|
parents: [folderId],
|
213
213
|
mimeType: data.mimeType || 'application/json',
|
214
214
|
};
|
@@ -475,6 +475,61 @@ export class GoogleDriveStorage {
|
|
475
475
|
await this.updateFileIdsJson(relationsFile.id);
|
476
476
|
return relationsFile;
|
477
477
|
}
|
478
|
+
async updateResumeRelation({ authorFolderId, draftFileId, signedFileId, }) {
|
479
|
+
try {
|
480
|
+
const relationFileName = 'relations.json';
|
481
|
+
// Step 1: Check if `relations.json` exists in RESUMES_AUTHOR/
|
482
|
+
const existingRelationFiles = await this.searchFiles(`name='${relationFileName}' and '${authorFolderId}' in parents`);
|
483
|
+
let relationFileId = null;
|
484
|
+
let existingRelations = {};
|
485
|
+
if (existingRelationFiles.length > 0) {
|
486
|
+
relationFileId = existingRelationFiles[0].id;
|
487
|
+
existingRelations = await this.getFileContent(relationFileId);
|
488
|
+
}
|
489
|
+
else {
|
490
|
+
console.log('relations.json does not exist. Will create a new one.');
|
491
|
+
}
|
492
|
+
// Step 2: Update relations object
|
493
|
+
existingRelations[draftFileId] = signedFileId;
|
494
|
+
// Step 3: Create or update the file on Drive
|
495
|
+
const fileBlob = new Blob([JSON.stringify(existingRelations, null, 2)], {
|
496
|
+
type: 'application/json',
|
497
|
+
});
|
498
|
+
const formData = new FormData();
|
499
|
+
const metadata = {
|
500
|
+
name: relationFileName,
|
501
|
+
parents: [authorFolderId],
|
502
|
+
mimeType: 'application/json',
|
503
|
+
};
|
504
|
+
formData.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
505
|
+
formData.append('file', fileBlob);
|
506
|
+
let uploadedFile;
|
507
|
+
if (relationFileId) {
|
508
|
+
// Update existing file
|
509
|
+
uploadedFile = await this.fetcher({
|
510
|
+
method: 'PATCH',
|
511
|
+
headers: {},
|
512
|
+
body: formData,
|
513
|
+
url: `https://www.googleapis.com/upload/drive/v3/files/${relationFileId}?uploadType=multipart&fields=id,parents`,
|
514
|
+
});
|
515
|
+
}
|
516
|
+
else {
|
517
|
+
// Create new file
|
518
|
+
uploadedFile = await this.fetcher({
|
519
|
+
method: 'POST',
|
520
|
+
headers: {},
|
521
|
+
body: formData,
|
522
|
+
url: `https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,parents`,
|
523
|
+
});
|
524
|
+
}
|
525
|
+
console.log(`✅ Resume relation updated. File ID: ${uploadedFile.id}`);
|
526
|
+
return uploadedFile;
|
527
|
+
}
|
528
|
+
catch (error) {
|
529
|
+
console.error('❌ Failed to update resume relation:', error);
|
530
|
+
throw error;
|
531
|
+
}
|
532
|
+
}
|
478
533
|
/**
|
479
534
|
* Delete file by id
|
480
535
|
* @param id
|
package/dist/models/Resume.js
CHANGED
@@ -5,21 +5,55 @@ export const resumeFolderTypes = {
|
|
5
5
|
};
|
6
6
|
export class StorageHandler {
|
7
7
|
storage;
|
8
|
+
folderCreationPromises = {};
|
9
|
+
createdFolders = {};
|
8
10
|
constructor(storage) {
|
9
11
|
this.storage = storage;
|
10
12
|
}
|
11
13
|
async getOrCreateFolder(folderName, parentId) {
|
12
|
-
const
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
folder = await this.storage.createFolder({
|
18
|
-
folderName,
|
19
|
-
parentFolderId: parentId,
|
20
|
-
});
|
14
|
+
const cacheKey = `${parentId}_${folderName}`;
|
15
|
+
// Check our local cache first (this is separate from the storage's cache)
|
16
|
+
if (this.createdFolders[cacheKey]) {
|
17
|
+
console.log(`Using cached folder ${folderName} (${this.createdFolders[cacheKey].id}) under ${parentId}`);
|
18
|
+
return this.createdFolders[cacheKey];
|
21
19
|
}
|
22
|
-
|
20
|
+
// If there's an existing promise for this folder, wait for it
|
21
|
+
if (this.folderCreationPromises[cacheKey]) {
|
22
|
+
console.log(`Waiting for existing folder creation: ${folderName} under ${parentId}`);
|
23
|
+
return this.folderCreationPromises[cacheKey];
|
24
|
+
}
|
25
|
+
// Create a new promise for this folder operation
|
26
|
+
this.folderCreationPromises[cacheKey] = (async () => {
|
27
|
+
try {
|
28
|
+
// Double-check if folder exists
|
29
|
+
console.log(`Searching for folder ${folderName} under ${parentId}`);
|
30
|
+
const folders = await this.storage.findFolders(parentId);
|
31
|
+
let folder = folders.find((f) => f.name === folderName);
|
32
|
+
if (folder) {
|
33
|
+
console.log(`Found existing folder ${folderName} (${folder.id}) under ${parentId}`);
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
console.log(`Creating folder ${folderName} under ${parentId} (no existing folder found)`);
|
37
|
+
folder = await this.storage.createFolder({
|
38
|
+
folderName,
|
39
|
+
parentFolderId: parentId,
|
40
|
+
});
|
41
|
+
console.log(`Created folder ${folderName} (${folder.id}) under ${parentId}`);
|
42
|
+
}
|
43
|
+
// Store in our local cache
|
44
|
+
this.createdFolders[cacheKey] = folder;
|
45
|
+
return folder;
|
46
|
+
}
|
47
|
+
catch (error) {
|
48
|
+
console.error(`Error in getOrCreateFolder(${folderName}, ${parentId}):`, error);
|
49
|
+
throw error;
|
50
|
+
}
|
51
|
+
finally {
|
52
|
+
// Clean up the promise after completion
|
53
|
+
delete this.folderCreationPromises[cacheKey];
|
54
|
+
}
|
55
|
+
})();
|
56
|
+
return this.folderCreationPromises[cacheKey];
|
23
57
|
}
|
24
58
|
async findFilesInFolder(folderName) {
|
25
59
|
const folders = await this.storage.findFolders();
|
@@ -36,12 +70,8 @@ export class Resume extends StorageHandler {
|
|
36
70
|
}
|
37
71
|
async saveResume({ resume, type }) {
|
38
72
|
try {
|
39
|
-
|
40
|
-
|
41
|
-
let rootFolder = rootFolders.find((folder) => folder.name === resumeFolderTypes.root);
|
42
|
-
if (!rootFolder) {
|
43
|
-
rootFolder = await this.storage.createFolder({ folderName: resumeFolderTypes.root, parentFolderId: 'root' });
|
44
|
-
}
|
73
|
+
let rootFolder = await this.getOrCreateFolder(resumeFolderTypes.root, 'root');
|
74
|
+
console.log('🚀 ~ Resume ~ saveResume ~ rootFolder:', rootFolder);
|
45
75
|
// Get or create the subfolder
|
46
76
|
const subFolderName = type === 'sign' ? resumeFolderTypes.signed : resumeFolderTypes.nonSigned;
|
47
77
|
const subFolder = await this.getOrCreateFolder(subFolderName, rootFolder.id);
|
@@ -72,7 +102,7 @@ export class Resume extends StorageHandler {
|
|
72
102
|
async getSignedResumes() {
|
73
103
|
try {
|
74
104
|
// Find the root folder first
|
75
|
-
const rootFolder = await this.
|
105
|
+
const rootFolder = await this.getOrCreateFolder(resumeFolderTypes.root, 'root');
|
76
106
|
// Find or create the signed resumes folder
|
77
107
|
const signedFolder = await this.getOrCreateFolder(resumeFolderTypes.signed, rootFolder.id);
|
78
108
|
// Retrieve all files from the signed folder
|
@@ -86,7 +116,7 @@ export class Resume extends StorageHandler {
|
|
86
116
|
async getNonSignedResumes() {
|
87
117
|
try {
|
88
118
|
// Find the root folder first
|
89
|
-
const rootFolder = await this.
|
119
|
+
const rootFolder = await this.getOrCreateFolder(resumeFolderTypes.root, 'root');
|
90
120
|
// Find or create the non-signed resumes folder
|
91
121
|
const nonSignedFolder = await this.getOrCreateFolder(resumeFolderTypes.nonSigned, rootFolder.id);
|
92
122
|
// Retrieve all files from the non-signed folder
|
@@ -97,13 +127,28 @@ export class Resume extends StorageHandler {
|
|
97
127
|
throw new Error('Error while fetching non-signed resumes: ' + error.message);
|
98
128
|
}
|
99
129
|
}
|
100
|
-
async
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
130
|
+
async saveResumeDraft(data, signedResumeId) {
|
131
|
+
try {
|
132
|
+
const fileName = `FinalDraft_${signedResumeId}.json`;
|
133
|
+
// 1. Find or create root and NON_SIGNED_RESUMES folder
|
134
|
+
const rootFolder = await this.getOrCreateFolder(resumeFolderTypes.root, 'root');
|
135
|
+
const nonSignedFolder = await this.getOrCreateFolder(resumeFolderTypes.nonSigned, rootFolder.id);
|
136
|
+
const dataWithFileName = {
|
137
|
+
...data,
|
138
|
+
fileName: `FinalDraft_${signedResumeId}.json`,
|
139
|
+
};
|
140
|
+
// Save the file
|
141
|
+
const savedDraft = await this.storage.saveFile({
|
142
|
+
data: dataWithFileName,
|
143
|
+
folderId: nonSignedFolder.id,
|
144
|
+
});
|
145
|
+
console.log(`✅ Draft saved as ${fileName}`);
|
146
|
+
return savedDraft;
|
147
|
+
}
|
148
|
+
catch (error) {
|
149
|
+
console.error('❌ Error saving resume draft:', error);
|
150
|
+
throw new Error('Failed to save resume draft: ' + error.message);
|
105
151
|
}
|
106
|
-
return rootFolder;
|
107
152
|
}
|
108
153
|
isResumeFolderExist() { }
|
109
154
|
}
|
package/dist/models/ResumeVC.js
CHANGED
@@ -6,149 +6,150 @@ import { Ed25519VerificationKey2020 } from '@digitalbazaar/ed25519-verification-
|
|
6
6
|
import { generateDIDSchema } from '../utils/credential.js';
|
7
7
|
import { inlineResumeContext } from '../utils/context.js';
|
8
8
|
export class ResumeVC {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
9
|
+
async sign({ formData, issuerDid, keyPair }) {
|
10
|
+
const unsignedCredential = this.generateUnsignedCredential({ formData, issuerDid });
|
11
|
+
const suite = new Ed25519Signature2020({
|
12
|
+
key: new Ed25519VerificationKey2020(keyPair), // Ensure proper initialization
|
13
|
+
verificationMethod: keyPair.id,
|
14
|
+
});
|
15
|
+
try {
|
16
|
+
const signedVC = await dbVc.issue({
|
17
|
+
credential: unsignedCredential,
|
18
|
+
suite,
|
19
|
+
documentLoader: customDocumentLoader,
|
20
|
+
});
|
21
|
+
console.log('Signed VC:', signedVC);
|
22
|
+
}
|
23
|
+
catch (error) {
|
24
|
+
console.error('Error signing VC:', error.message);
|
25
|
+
if (error.details) {
|
26
|
+
console.error('Error details:', JSON.stringify(error.details, null, 2));
|
27
|
+
}
|
28
|
+
throw error;
|
29
|
+
}
|
30
|
+
return unsignedCredential;
|
31
|
+
}
|
32
|
+
generateUnsignedCredential({ formData, issuerDid }) {
|
33
|
+
const unsignedResumeVC = {
|
34
|
+
'@context': ['https://www.w3.org/2018/credentials/v1', inlineResumeContext['@context']],
|
35
|
+
id: `urn:uuid:${uuidv4()}`,
|
36
|
+
type: ['VerifiableCredential', 'LERRSCredential'],
|
37
|
+
issuer: issuerDid,
|
38
|
+
issuanceDate: new Date().toISOString(),
|
39
|
+
credentialSubject: {
|
40
|
+
type: 'Resume',
|
41
|
+
person: {
|
42
|
+
name: {
|
43
|
+
formattedName: formData.name || '',
|
44
|
+
},
|
45
|
+
primaryLanguage: 'en',
|
46
|
+
contact: {
|
47
|
+
fullName: formData.contact.fullName || '',
|
48
|
+
email: formData.contact.email || '',
|
49
|
+
phone: formData.contact.phone || '',
|
50
|
+
location: {
|
51
|
+
street: formData.contact.location.street || '',
|
52
|
+
city: formData.contact.location.city || '',
|
53
|
+
state: formData.contact.location.state || '',
|
54
|
+
country: formData.contact.location.country || '',
|
55
|
+
postalCode: formData.contact.location.postalCode || '',
|
56
|
+
},
|
57
|
+
socialLinks: {
|
58
|
+
linkedin: formData.contact.socialLinks.linkedin || '',
|
59
|
+
github: formData.contact.socialLinks.github || '',
|
60
|
+
portfolio: formData.contact.socialLinks.portfolio || '',
|
61
|
+
twitter: formData.contact.socialLinks.twitter || '',
|
62
|
+
},
|
63
|
+
},
|
64
|
+
},
|
65
|
+
narrative: {
|
66
|
+
text: formData.summary || '',
|
67
|
+
},
|
68
|
+
employmentHistory: formData.experience.items.map((exp) => ({
|
69
|
+
id: exp.id ? `urn:uuid${exp.id}` : `urn:uuid:${uuidv4()}`, // Ensure each entry has an ID
|
70
|
+
organization: {
|
71
|
+
tradeName: exp.company || '',
|
72
|
+
},
|
73
|
+
title: exp.title || '',
|
74
|
+
description: exp.description || '',
|
75
|
+
duration: exp.duration || '',
|
76
|
+
startDate: exp.startDate || '',
|
77
|
+
endDate: exp.endDate || '',
|
78
|
+
stillEmployed: exp.stillEmployed || false,
|
79
|
+
verificationStatus: exp.verificationStatus || 'unverified',
|
80
|
+
credentialLink: exp.credentialLink || null,
|
81
|
+
verifiedCredentials: exp.verifiedCredentials || [],
|
82
|
+
})),
|
83
|
+
educationAndLearning: formData.education.items.map((edu) => ({
|
84
|
+
id: edu.id ? `urn:uuid${edu.id}` : `urn:uuid:${uuidv4()}`,
|
85
|
+
institution: edu.institution || '',
|
86
|
+
degree: edu.degree || '',
|
87
|
+
fieldOfStudy: edu.fieldOfStudy || '',
|
88
|
+
duration: edu.duration || '',
|
89
|
+
startDate: edu.startDate || '',
|
90
|
+
endDate: edu.endDate || '',
|
91
|
+
verificationStatus: edu.verificationStatus || 'unverified',
|
92
|
+
credentialLink: edu.credentialLink || null,
|
93
|
+
verifiedCredentials: edu.verifiedCredentials || [],
|
94
|
+
})),
|
95
|
+
skills: formData.skills.items.map((skill) => ({
|
96
|
+
id: skill.id ? `urn:uuid${skill.id}` : `urn:uuid:${uuidv4()}`,
|
97
|
+
name: skill.name || '',
|
98
|
+
verificationStatus: skill.verificationStatus || 'unverified',
|
99
|
+
credentialLink: skill.credentialLink || null,
|
100
|
+
verifiedCredentials: skill.verifiedCredentials || [],
|
101
|
+
})),
|
102
|
+
certifications: formData.certifications.items.map((cert) => ({
|
103
|
+
id: cert.id ? `urn:uuid:${cert.id}` : `urn:uuid:${uuidv4()}`,
|
104
|
+
name: cert.name || '',
|
105
|
+
issuer: cert.issuer || '',
|
106
|
+
date: cert.date || '',
|
107
|
+
url: cert.url || '',
|
108
|
+
verificationStatus: cert.verificationStatus || 'unverified',
|
109
|
+
credentialLink: cert.credentialLink || null,
|
110
|
+
verifiedCredentials: cert.verifiedCredentials || [],
|
111
|
+
})),
|
112
|
+
projects: formData.projects.items.map((proj) => ({
|
113
|
+
id: proj.id ? `urn:uuid${proj.id}` : `urn:uuid:${uuidv4()}`,
|
114
|
+
name: proj.name || '',
|
115
|
+
description: proj.description || '',
|
116
|
+
url: proj.url || '',
|
117
|
+
duration: proj.duration || '',
|
118
|
+
startDate: proj.startDate || '',
|
119
|
+
endDate: proj.endDate || '',
|
120
|
+
verificationStatus: proj.verificationStatus || 'unverified',
|
121
|
+
credentialLink: proj.credentialLink || null,
|
122
|
+
verifiedCredentials: proj.verifiedCredentials || [],
|
123
|
+
})),
|
124
|
+
},
|
125
|
+
};
|
126
|
+
console.log('🚀 ~ ResumeVC ~ generateUnsignedCredential ~ unsignedResumeVC:', JSON.stringify(unsignedResumeVC));
|
127
|
+
return unsignedResumeVC;
|
128
|
+
}
|
129
|
+
generateKeyPair = async (address) => {
|
130
|
+
// Generate the key pair using the library's method
|
131
|
+
const keyPair = await Ed25519VerificationKey2020.generate();
|
132
|
+
// Configure key pair attributes
|
133
|
+
const a = address || keyPair.publicKeyMultibase;
|
134
|
+
keyPair.controller = `did:key:${a}`;
|
135
|
+
keyPair.id = `${keyPair.controller}#${a}`;
|
136
|
+
keyPair.revoked = false;
|
137
|
+
// The `signer` is already provided by the `Ed25519VerificationKey2020` instance
|
138
|
+
return keyPair;
|
139
|
+
};
|
140
|
+
/**
|
141
|
+
* Create a new DID with Digital Bazaar's Ed25519VerificationKey2020 key pair.
|
142
|
+
* @returns {Promise<{didDocument: object, keyPair: object}>} The created DID document and key pair.
|
143
|
+
* @throws Will throw an error if DID creation fails.
|
144
|
+
*/
|
145
|
+
async createDID({ keyPair }) {
|
146
|
+
try {
|
147
|
+
const didDocument = await generateDIDSchema(keyPair);
|
148
|
+
return didDocument;
|
149
|
+
}
|
150
|
+
catch (error) {
|
151
|
+
console.error('Error creating DID:', error);
|
152
|
+
throw error;
|
153
|
+
}
|
154
|
+
}
|
154
155
|
}
|
@@ -76,6 +76,11 @@ export declare class GoogleDriveStorage {
|
|
76
76
|
createRelationsFile({ vcFolderId }: {
|
77
77
|
vcFolderId: string;
|
78
78
|
}): Promise<any>;
|
79
|
+
updateResumeRelation({ authorFolderId, draftFileId, signedFileId, }: {
|
80
|
+
authorFolderId: string;
|
81
|
+
draftFileId: string;
|
82
|
+
signedFileId: string;
|
83
|
+
}): Promise<any>;
|
79
84
|
/**
|
80
85
|
* Delete file by id
|
81
86
|
* @param id
|
@@ -6,6 +6,8 @@ export declare const resumeFolderTypes: {
|
|
6
6
|
};
|
7
7
|
export declare class StorageHandler {
|
8
8
|
protected storage: GoogleDriveStorage;
|
9
|
+
private folderCreationPromises;
|
10
|
+
private createdFolders;
|
9
11
|
constructor(storage: GoogleDriveStorage);
|
10
12
|
getOrCreateFolder(folderName: string, parentId: string): Promise<any>;
|
11
13
|
protected findFilesInFolder(folderName: string): Promise<any[]>;
|
@@ -22,7 +24,7 @@ export declare class Resume extends StorageHandler {
|
|
22
24
|
}>;
|
23
25
|
getSignedResumes(): Promise<any[]>;
|
24
26
|
getNonSignedResumes(): Promise<any[]>;
|
25
|
-
|
27
|
+
saveResumeDraft(data: any, signedResumeId: string): Promise<any>;
|
26
28
|
private isResumeFolderExist;
|
27
29
|
}
|
28
30
|
export default Resume;
|