tasks_model.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. const pull = require('../server/node_modules/pull-stream');
  2. const moment = require('../server/node_modules/moment');
  3. const { getConfig } = require('../configs/config-manager.js');
  4. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  5. module.exports = ({ cooler }) => {
  6. let ssb;
  7. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
  8. return {
  9. async createTask(title, description, startTime, endTime, priority, location = '', tagsRaw = [], isPublic) {
  10. const ssb = await openSsb();
  11. const userId = ssb.id;
  12. const start = moment(startTime);
  13. const end = moment(endTime);
  14. if (!start.isValid() || !end.isValid()) throw new Error('Invalid dates');
  15. if (start.isBefore(moment()) || end.isBefore(start)) throw new Error('Invalid time range');
  16. const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
  17. const content = {
  18. type: 'task',
  19. title,
  20. description,
  21. startTime: start.toISOString(),
  22. endTime: end.toISOString(),
  23. priority,
  24. location,
  25. tags,
  26. isPublic,
  27. assignees: [userId],
  28. createdAt: new Date().toISOString(),
  29. status: 'OPEN',
  30. author: userId
  31. };
  32. return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
  33. },
  34. async deleteTaskById(taskId) {
  35. const ssb = await openSsb();
  36. const userId = ssb.id;
  37. const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task)));
  38. if (task.content.author !== userId) throw new Error('Not the author');
  39. const tombstone = { type: 'tombstone', target: taskId, deletedAt: new Date().toISOString(), author: userId };
  40. return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
  41. },
  42. async updateTaskById(taskId, updatedData) {
  43. const ssb = await openSsb();
  44. const userId = ssb.id;
  45. const old = await new Promise((res, rej) =>
  46. ssb.get(taskId, (err, msg) => err || !msg ? rej(new Error('Task not found')) : res(msg))
  47. );
  48. const c = old.content;
  49. if (c.type !== 'task') throw new Error('Invalid type');
  50. const keys = Object.keys(updatedData || {}).filter(k => updatedData[k] !== undefined);
  51. const assigneesOnly = keys.length === 1 && keys[0] === 'assignees';
  52. const taskCreator = old.author || c.author;
  53. if (!assigneesOnly && taskCreator !== userId) throw new Error('Not the author');
  54. if (c.status === 'CLOSED') throw new Error('Cannot edit a closed task');
  55. const uniq = (arr) => Array.from(new Set((Array.isArray(arr) ? arr : []).filter(x => typeof x === 'string' && x.trim().length)));
  56. let nextAssignees = Array.isArray(c.assignees) ? uniq(c.assignees) : [];
  57. if (assigneesOnly) {
  58. const proposed = uniq(updatedData.assignees);
  59. const oldNoSelf = uniq(nextAssignees.filter(x => x !== userId)).sort();
  60. const newNoSelf = uniq(proposed.filter(x => x !== userId)).sort();
  61. if (oldNoSelf.length !== newNoSelf.length || oldNoSelf.some((v, i) => v !== newNoSelf[i])) {
  62. throw new Error('Not allowed');
  63. }
  64. const hadSelf = nextAssignees.includes(userId);
  65. const hasSelfNow = proposed.includes(userId);
  66. if (hadSelf === hasSelfNow) throw new Error('Not allowed');
  67. nextAssignees = proposed;
  68. }
  69. let newStart = c.startTime;
  70. if (updatedData.startTime != null && updatedData.startTime !== '') {
  71. const m = moment(updatedData.startTime);
  72. if (!m.isValid()) throw new Error('Invalid startTime');
  73. newStart = m.toISOString();
  74. }
  75. let newEnd = c.endTime;
  76. if (updatedData.endTime != null && updatedData.endTime !== '') {
  77. const m = moment(updatedData.endTime);
  78. if (!m.isValid()) throw new Error('Invalid endTime');
  79. newEnd = m.toISOString();
  80. }
  81. if (moment(newEnd).isBefore(moment(newStart))) {
  82. throw new Error('Invalid time range');
  83. }
  84. let newTags = c.tags || [];
  85. if (updatedData.tags !== undefined) {
  86. if (Array.isArray(updatedData.tags)) {
  87. newTags = updatedData.tags.filter(Boolean);
  88. } else if (typeof updatedData.tags === 'string') {
  89. newTags = updatedData.tags.split(',').map(t => t.trim()).filter(Boolean);
  90. } else {
  91. newTags = [];
  92. }
  93. }
  94. let newVisibility = c.isPublic;
  95. if (updatedData.isPublic !== undefined) {
  96. const v = String(updatedData.isPublic).toUpperCase();
  97. newVisibility = (v === 'PUBLIC' || v === 'PRIVATE') ? v : c.isPublic;
  98. }
  99. const updated = {
  100. ...c,
  101. title: updatedData.title ?? c.title,
  102. description: updatedData.description ?? c.description,
  103. startTime: newStart,
  104. endTime: newEnd,
  105. priority: updatedData.priority ?? c.priority,
  106. location: updatedData.location ?? c.location,
  107. tags: newTags,
  108. isPublic: newVisibility,
  109. status: updatedData.status ?? c.status,
  110. assignees: assigneesOnly ? nextAssignees : (updatedData.assignees !== undefined ? uniq(updatedData.assignees) : nextAssignees),
  111. updatedAt: new Date().toISOString(),
  112. replaces: taskId
  113. };
  114. return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
  115. },
  116. async updateTaskStatus(taskId, status) {
  117. if (!['OPEN', 'IN-PROGRESS', 'CLOSED'].includes(status)) throw new Error('Invalid status');
  118. return this.updateTaskById(taskId, { status });
  119. },
  120. async getTaskById(taskId) {
  121. const ssb = await openSsb();
  122. const now = moment();
  123. const task = await new Promise((res, rej) => ssb.get(taskId, (err, task) => err ? rej(new Error('Task not found')) : res(task)));
  124. const c = task.content;
  125. const status = c.status === 'OPEN' && moment(c.endTime).isBefore(now) ? 'CLOSED' : c.status;
  126. return { id: taskId, ...c, status };
  127. },
  128. async toggleAssignee(taskId) {
  129. const ssb = await openSsb();
  130. const userId = ssb.id;
  131. const task = await this.getTaskById(taskId);
  132. if (task.status === 'CLOSED') throw new Error('Cannot assign users to a closed task');
  133. let assignees = Array.isArray(task.assignees) ? [...task.assignees] : [];
  134. const idx = assignees.indexOf(userId);
  135. if (idx !== -1) {
  136. assignees.splice(idx, 1);
  137. } else {
  138. assignees.push(userId);
  139. }
  140. return this.updateTaskById(taskId, { assignees });
  141. },
  142. async listAll() {
  143. const ssb = await openSsb();
  144. const now = moment();
  145. return new Promise((resolve, reject) => {
  146. pull(ssb.createLogStream({ limit: logLimit }),
  147. pull.collect((err, results) => {
  148. if (err) return reject(err);
  149. const tombstoned = new Set();
  150. const replaced = new Map();
  151. const tasks = new Map();
  152. for (const r of results) {
  153. const { key, value: { content: c } } = r;
  154. if (!c) continue;
  155. if (c.type === 'tombstone') tombstoned.add(c.target);
  156. if (c.type === 'task') {
  157. if (c.replaces) replaced.set(c.replaces, key);
  158. const status = c.status === 'OPEN' && moment(c.endTime).isBefore(now) ? 'CLOSED' : c.status;
  159. tasks.set(key, { id: key, ...c, status });
  160. }
  161. }
  162. tombstoned.forEach(id => tasks.delete(id));
  163. replaced.forEach((_, oldId) => tasks.delete(oldId));
  164. resolve([...tasks.values()]);
  165. }));
  166. });
  167. }
  168. };
  169. };