cv_model.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. const pull = require('../server/node_modules/pull-stream');
  2. const { getConfig } = require('../configs/config-manager.js');
  3. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  4. const extractBlobId = str => {
  5. if (!str || typeof str !== 'string') return null;
  6. const match = str.match(/\(([^)]+\.sha256)\)/);
  7. return match ? match[1] : str.trim();
  8. };
  9. const parseCSV = str => str
  10. ? str.split(',').map(s => s.trim()).filter(Boolean)
  11. : [];
  12. module.exports = ({ cooler }) => {
  13. let ssb;
  14. const openSsb = async () => {
  15. if (!ssb) ssb = await cooler.open();
  16. return ssb;
  17. };
  18. return {
  19. type: 'curriculum',
  20. async createCV(data, photoBlobId) {
  21. const ssbClient = await openSsb();
  22. const userId = ssbClient.id;
  23. const content = {
  24. type: 'curriculum',
  25. author: userId,
  26. name: data.name,
  27. description: data.description,
  28. photo: extractBlobId(photoBlobId) || null,
  29. contact: userId,
  30. personalSkills: parseCSV(data.personalSkills),
  31. personalExperiences: data.personalExperiences || '',
  32. oasisExperiences: data.oasisExperiences || '',
  33. oasisSkills: parseCSV(data.oasisSkills),
  34. educationExperiences: data.educationExperiences || '',
  35. educationalSkills: parseCSV(data.educationalSkills),
  36. languages: data.languages || '',
  37. professionalExperiences: data.professionalExperiences || '',
  38. professionalSkills: parseCSV(data.professionalSkills),
  39. location: data.location || 'UNKNOWN',
  40. status: data.status || 'LOOKING FOR WORK',
  41. preferences: data.preferences || 'REMOTE WORKING',
  42. createdAt: new Date().toISOString()
  43. };
  44. return new Promise((resolve, reject) => {
  45. ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
  46. });
  47. },
  48. async updateCV(id, data, photoBlobId) {
  49. const ssbClient = await openSsb();
  50. const userId = ssbClient.id;
  51. const old = await new Promise((res, rej) =>
  52. ssbClient.get(id, (err, msg) =>
  53. err || !msg?.content
  54. ? rej(err || new Error('CV not found'))
  55. : res(msg)
  56. )
  57. );
  58. if (old.content.author !== userId) {
  59. throw new Error('Not the author');
  60. }
  61. const tombstone = {
  62. type: 'tombstone',
  63. target: id,
  64. deletedAt: new Date().toISOString()
  65. };
  66. await new Promise((res, rej) =>
  67. ssbClient.publish(tombstone, err => err ? rej(err) : res())
  68. );
  69. const content = {
  70. type: 'curriculum',
  71. author: userId,
  72. name: data.name,
  73. description: data.description,
  74. photo: extractBlobId(photoBlobId) || null,
  75. contact: userId,
  76. personalSkills: parseCSV(data.personalSkills),
  77. personalExperiences: data.personalExperiences || '',
  78. oasisExperiences: data.oasisExperiences || '',
  79. oasisSkills: parseCSV(data.oasisSkills),
  80. educationExperiences: data.educationExperiences || '',
  81. educationalSkills: parseCSV(data.educationalSkills),
  82. languages: data.languages || '',
  83. professionalExperiences: data.professionalExperiences || '',
  84. professionalSkills: parseCSV(data.professionalSkills),
  85. location: data.location || 'UNKNOWN',
  86. status: data.status || 'LOOKING FOR WORK',
  87. preferences: data.preferences || 'REMOTE WORKING',
  88. createdAt: old.content.createdAt,
  89. updatedAt: new Date().toISOString()
  90. };
  91. return new Promise((resolve, reject) => {
  92. ssbClient.publish(content, (err, msg) => err ? reject(err) : resolve(msg));
  93. });
  94. },
  95. async deleteCVById(id) {
  96. const ssbClient = await openSsb();
  97. const userId = ssbClient.id;
  98. const msg = await new Promise((res, rej) =>
  99. ssbClient.get(id, (err, msg) =>
  100. err || !msg?.content
  101. ? rej(new Error('CV not found'))
  102. : res(msg)
  103. )
  104. );
  105. if (msg.content.author !== userId) {
  106. throw new Error('Not the author');
  107. }
  108. const tombstone = {
  109. type: 'tombstone',
  110. target: id,
  111. deletedAt: new Date().toISOString()
  112. };
  113. return new Promise((resolve, reject) => {
  114. ssbClient.publish(tombstone, (err, result) => err ? reject(err) : resolve(result));
  115. });
  116. },
  117. async getCVByUserId(targetUserId) {
  118. const ssbClient = await openSsb();
  119. const userId = ssbClient.id;
  120. const authorId = targetUserId || userId;
  121. return new Promise((resolve, reject) => {
  122. pull(
  123. ssbClient.createLogStream({ limit: logLimit }),
  124. pull.collect((err, msgs) => {
  125. if (err) return reject(err);
  126. const tombstoned = new Set(
  127. msgs
  128. .filter(m => m.value?.content?.type === 'tombstone' && m.value.content.target)
  129. .map(m => m.value.content.target)
  130. );
  131. const cvMsgs = msgs
  132. .filter(m =>
  133. m.value?.content?.type === 'curriculum' &&
  134. m.value.content.author === authorId &&
  135. !tombstoned.has(m.key)
  136. )
  137. .sort((a, b) => b.value.timestamp - a.value.timestamp);
  138. if (!cvMsgs.length) {
  139. return resolve(null);
  140. }
  141. const latest = cvMsgs[0];
  142. resolve({ id: latest.key, ...latest.value.content });
  143. })
  144. );
  145. });
  146. }
  147. };
  148. };