@lilaquadrat/studio 11.0.0 → 11.1.1

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.
Files changed (43) hide show
  1. package/lib/models.d.ts +6 -1
  2. package/lib/models.js +4 -1
  3. package/lib/models.js.map +1 -1
  4. package/lib/services.d.ts +4 -1
  5. package/lib/services.js +4 -1
  6. package/lib/services.js.map +1 -1
  7. package/lib/src/interfaces/Participant.d.ts +62 -0
  8. package/lib/src/interfaces/Participant.js +4 -0
  9. package/lib/src/interfaces/Participant.js.map +1 -0
  10. package/lib/src/models/conversation.model.js +30 -0
  11. package/lib/src/models/conversation.model.js.map +1 -1
  12. package/lib/src/models/license.model.d.ts +4 -0
  13. package/lib/src/models/license.model.js +70 -0
  14. package/lib/src/models/license.model.js.map +1 -0
  15. package/lib/src/models/participant.model.d.ts +4 -0
  16. package/lib/src/models/participant.model.js +50 -0
  17. package/lib/src/models/participant.model.js.map +1 -0
  18. package/lib/src/models/permissions.model.js +3 -0
  19. package/lib/src/models/permissions.model.js.map +1 -1
  20. package/lib/src/models/storage.model.js +44 -0
  21. package/lib/src/models/storage.model.js.map +1 -1
  22. package/lib/src/models/upload.model.js +34 -0
  23. package/lib/src/models/upload.model.js.map +1 -1
  24. package/lib/src/services/memory-cache.service.d.ts +25 -0
  25. package/lib/src/services/memory-cache.service.js +59 -0
  26. package/lib/src/services/memory-cache.service.js.map +1 -0
  27. package/lib/src/services/participants.service.d.ts +60 -0
  28. package/lib/src/services/participants.service.js +147 -0
  29. package/lib/src/services/participants.service.js.map +1 -0
  30. package/lib/src/services/storage.service.d.ts +17 -5
  31. package/lib/src/services/storage.service.js +56 -23
  32. package/lib/src/services/storage.service.js.map +1 -1
  33. package/lib/src/services/support-access.service.d.ts +35 -0
  34. package/lib/src/services/support-access.service.js +113 -0
  35. package/lib/src/services/support-access.service.js.map +1 -0
  36. package/lib/src/services/upload.service.d.ts +5 -1
  37. package/lib/src/services/upload.service.js +11 -6
  38. package/lib/src/services/upload.service.js.map +1 -1
  39. package/lib/tests/storageServiceToken.spec.js +14 -0
  40. package/lib/tests/storageServiceToken.spec.js.map +1 -1
  41. package/lib/tests/uploadServiceCreate.spec.js +19 -0
  42. package/lib/tests/uploadServiceCreate.spec.js.map +1 -1
  43. package/package.json +2 -2
@@ -0,0 +1,59 @@
1
+ import logger from '../logger.js';
2
+ class MemoryCache {
3
+ static clear() {
4
+ MemoryCache.cache = {};
5
+ }
6
+ static remove(cacheKeyOptions) {
7
+ delete MemoryCache.cache[MemoryCache.getCacheKey(cacheKeyOptions)];
8
+ }
9
+ static clearUser(user) {
10
+ const suffix = `:${user}`;
11
+ Object.keys(MemoryCache.cache).forEach(key => {
12
+ if (key.endsWith(suffix)) {
13
+ delete MemoryCache.cache[key];
14
+ }
15
+ });
16
+ }
17
+ static clearUsers(users) {
18
+ users.forEach((single) => MemoryCache.clearUser(single));
19
+ }
20
+ static upsert(cacheKey, data, lifetime) {
21
+ MemoryCache.cache[cacheKey.toString()] = {
22
+ data,
23
+ ...(lifetime ? { expiresAt: Date.now() + lifetime } : {}),
24
+ };
25
+ }
26
+ static get(cacheKey) {
27
+ const entry = MemoryCache.cache[cacheKey.toString()];
28
+ if (!entry)
29
+ return undefined;
30
+ if (entry.expiresAt && entry.expiresAt < Date.now()) {
31
+ delete MemoryCache.cache[cacheKey.toString()];
32
+ return undefined;
33
+ }
34
+ return entry.data;
35
+ }
36
+ static async use(cacheKeyOptions, fetchFunction) {
37
+ const cacheKey = MemoryCache.getCacheKey(cacheKeyOptions);
38
+ const cached = MemoryCache.get(cacheKey);
39
+ if (cached) {
40
+ logger.debug('cache.hit', { key: cacheKey });
41
+ return cached;
42
+ }
43
+ logger.debug('cache.miss', { key: cacheKey });
44
+ const fetchedData = await fetchFunction;
45
+ MemoryCache.upsert(cacheKey, fetchedData, cacheKeyOptions.lifetime);
46
+ return fetchedData;
47
+ }
48
+ static getCacheKey(options) {
49
+ const keyArray = [options.type];
50
+ if (options.id)
51
+ keyArray.push(options.id.toString());
52
+ if (options.user)
53
+ keyArray.push(options.user.toString());
54
+ return keyArray.join(':');
55
+ }
56
+ }
57
+ MemoryCache.cache = {};
58
+ export default MemoryCache;
59
+ //# sourceMappingURL=memory-cache.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-cache.service.js","sourceRoot":"","sources":["../../../src/services/memory-cache.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { ObjectId } from 'mongodb';
2
+ import Immutable from '../Immutable.class.js';
3
+ import Participant, { ParticipantRole, ParticipantState, Right } from '../interfaces/Participant.js';
4
+ /**
5
+ * participant collection and conversation access.
6
+ *
7
+ * The base service is shared by studio-chat (which extends it with conversation bookkeeping
8
+ * that needs the message model) and by studio-media (which uses it for the conversation
9
+ * attachment access checks). Access rules therefore exist exactly once.
10
+ */
11
+ export declare class ParticipantsService extends Immutable<Participant> {
12
+ model: import("../classes/modelv2.class.js").default<Participant, "create" | "changeState">;
13
+ addToConversation(participant: string, conversation: ObjectId, user: string, rights?: Right[], role?: ParticipantRole): Promise<import("@lilaquadrat/interfaces").CreateResult<import("@lilaquadrat/interfaces").BasicData<Participant>>>;
14
+ updateLastReadId(participant: string, conversation: ObjectId, lastReadId: number): Promise<import("mongodb").UpdateResult<Participant>>;
15
+ /**
16
+ * silence a conversation for one participant. Only ever touches the caller's own doc, so
17
+ * membership stays untouched — no refreshParticipants/refreshParticipantsHash needed.
18
+ *
19
+ * Unmuting $unsets instead of writing `false`: absent and false mean the same thing to
20
+ * every reader, and the docs of the (vast) majority who never mute stay clean.
21
+ */
22
+ setMuted(conversation: ObjectId, participant: string, muted: boolean): Promise<import("mongodb").UpdateResult<Participant>>;
23
+ updateLastestMessageId(participant: string, conversation: ObjectId, latestMessageId: number): Promise<import("mongodb").UpdateResult<Participant>>;
24
+ getByConversationAndUser(conversationId: ObjectId, user: string): Promise<import("mongodb").WithId<Participant> | null>;
25
+ /**
26
+ * mark participants as support team members. Separate from adding them because they are added
27
+ * through updateParticipants(), which is deliberately generic — and it also has to reach docs
28
+ * that already existed (a member who left and comes back with the team).
29
+ */
30
+ markSupportAgents(conversationId: ObjectId, users: string[]): Promise<void> | Promise<import("mongodb").UpdateResult<Participant>>;
31
+ /** the support team members of a conversation — who may see its system messages */
32
+ getSupportAgents(conversationId: ObjectId): Promise<string[]>;
33
+ /**
34
+ * all participant docs of a conversation (incl. their rights & state), excluding deleted.
35
+ */
36
+ listByConversation(conversationId: ObjectId): Promise<import("mongodb").WithId<Participant>[]>;
37
+ setRights(conversationId: ObjectId, target: string, rights: Right[]): Promise<import("mongodb").UpdateResult<Participant>>;
38
+ /**
39
+ * flip a participant's own state. Written for the per-participant delete ('deleted'), which
40
+ * hides the conversation from that one user's list without touching anybody else's copy —
41
+ * conversations.service list() and listByConversation() both already filter
42
+ * `state: { $ne: 'deleted' }`, so the read side needs no change.
43
+ *
44
+ * Callers moving a participant OUT of 'active' have to refresh participants/participantsHash
45
+ * themselves (leave() and block() do). The delete route leaves first, so the state is never
46
+ * 'active' by the time this runs.
47
+ */
48
+ setState(conversationId: ObjectId, target: string, state: ParticipantState): Promise<import("mongodb").UpdateResult<Participant>>;
49
+ /**
50
+ * membership plus the named rights on a conversation. The single access chokepoint for both
51
+ * transports (REST routes and websocket actions) and for media's conversation attachment
52
+ * endpoints.
53
+ */
54
+ checkAccess(requiredAnyStatus: string[], requiredRights: string[] | undefined, settings: {
55
+ conversationId: ObjectId;
56
+ user: string;
57
+ }): Promise<boolean>;
58
+ }
59
+ declare const _default: ParticipantsService;
60
+ export default _default;
@@ -0,0 +1,147 @@
1
+ import Immutable from '../Immutable.class.js';
2
+ import ParticipantModel from '../models/participant.model.js';
3
+ import ConversationModel from '../models/conversation.model.js';
4
+ import MemoryCache from './memory-cache.service.js';
5
+ import supportAccess from './support-access.service.js';
6
+ /**
7
+ * conversation and participant docs are cached per process; callers that mutate them
8
+ * (studio-chat) invalidate explicitly, but a service that only reads (studio-media) cannot,
9
+ * so entries age out instead of holding revoked access forever.
10
+ */
11
+ const ACCESS_CACHE_LIFETIME_MS = 30000;
12
+ /**
13
+ * participant collection and conversation access.
14
+ *
15
+ * The base service is shared by studio-chat (which extends it with conversation bookkeeping
16
+ * that needs the message model) and by studio-media (which uses it for the conversation
17
+ * attachment access checks). Access rules therefore exist exactly once.
18
+ */
19
+ export class ParticipantsService extends Immutable {
20
+ constructor() {
21
+ super(...arguments);
22
+ this.model = ParticipantModel;
23
+ }
24
+ addToConversation(participant, conversation, user, rights = ['read', 'write', 'files'], role) {
25
+ return this.create({
26
+ user: participant,
27
+ conversation,
28
+ state: 'active',
29
+ rights,
30
+ lastReadId: 0,
31
+ ...(role ? { role } : {})
32
+ }, String(user), 'chat', undefined, undefined, undefined, {
33
+ returnDocument: true
34
+ });
35
+ }
36
+ updateLastReadId(participant, conversation, lastReadId) {
37
+ return this.model.db.updateOne({
38
+ user: participant,
39
+ conversation
40
+ }, { $max: { lastReadId } });
41
+ }
42
+ /**
43
+ * silence a conversation for one participant. Only ever touches the caller's own doc, so
44
+ * membership stays untouched — no refreshParticipants/refreshParticipantsHash needed.
45
+ *
46
+ * Unmuting $unsets instead of writing `false`: absent and false mean the same thing to
47
+ * every reader, and the docs of the (vast) majority who never mute stay clean.
48
+ */
49
+ setMuted(conversation, participant, muted) {
50
+ return this.model.db.updateOne({
51
+ user: participant,
52
+ conversation
53
+ }, muted ? { $set: { muted: true } } : { $unset: { muted: '' } });
54
+ }
55
+ updateLastestMessageId(participant, conversation, latestMessageId) {
56
+ return this.model.db.updateOne({
57
+ user: participant,
58
+ conversation
59
+ }, { $set: { latestMessageId } });
60
+ }
61
+ getByConversationAndUser(conversationId, user) {
62
+ return this.model.db.findOne({ conversation: conversationId, user });
63
+ }
64
+ /**
65
+ * mark participants as support team members. Separate from adding them because they are added
66
+ * through updateParticipants(), which is deliberately generic — and it also has to reach docs
67
+ * that already existed (a member who left and comes back with the team).
68
+ */
69
+ markSupportAgents(conversationId, users) {
70
+ if (!users.length)
71
+ return Promise.resolve();
72
+ return this.model.db.updateMany({ conversation: conversationId, user: { $in: users } }, { $set: { role: 'agent' } });
73
+ }
74
+ /** the support team members of a conversation — who may see its system messages */
75
+ async getSupportAgents(conversationId) {
76
+ const agents = await this.model.db
77
+ .find({ conversation: conversationId, role: 'agent' }, { projection: { user: 1 } })
78
+ .toArray();
79
+ return agents.map((single) => single.user);
80
+ }
81
+ /**
82
+ * all participant docs of a conversation (incl. their rights & state), excluding deleted.
83
+ */
84
+ listByConversation(conversationId) {
85
+ return this.model.db.find({ conversation: conversationId, state: { $ne: 'deleted' } }).toArray();
86
+ }
87
+ setRights(conversationId, target, rights) {
88
+ return this.model.db.updateOne({ conversation: conversationId, user: target }, { $set: { rights } });
89
+ }
90
+ /**
91
+ * flip a participant's own state. Written for the per-participant delete ('deleted'), which
92
+ * hides the conversation from that one user's list without touching anybody else's copy —
93
+ * conversations.service list() and listByConversation() both already filter
94
+ * `state: { $ne: 'deleted' }`, so the read side needs no change.
95
+ *
96
+ * Callers moving a participant OUT of 'active' have to refresh participants/participantsHash
97
+ * themselves (leave() and block() do). The delete route leaves first, so the state is never
98
+ * 'active' by the time this runs.
99
+ */
100
+ setState(conversationId, target, state) {
101
+ return this.model.db.updateOne({ conversation: conversationId, user: target }, { $set: { state } });
102
+ }
103
+ /**
104
+ * membership plus the named rights on a conversation. The single access chokepoint for both
105
+ * transports (REST routes and websocket actions) and for media's conversation attachment
106
+ * endpoints.
107
+ */
108
+ async checkAccess(requiredAnyStatus, requiredRights, settings) {
109
+ const { conversationId, user } = settings;
110
+ // Fetch both in parallel with distinct cache keys to avoid collisions
111
+ const [conversation, participant] = await Promise.all([
112
+ MemoryCache.use({ type: "conversation", id: conversationId, lifetime: ACCESS_CACHE_LIFETIME_MS }, ConversationModel.db.findOne({ _id: conversationId })),
113
+ MemoryCache.use({ type: "participant", id: conversationId, user, lifetime: ACCESS_CACHE_LIFETIME_MS }, this.getByConversationAndUser(conversationId, user))
114
+ ]);
115
+ if (!participant)
116
+ return false;
117
+ if (!conversation)
118
+ return false;
119
+ /**
120
+ * Support staff revocation.
121
+ *
122
+ * The team reconcile is add-only by design (addMissingSupportMembers): nothing on a support
123
+ * conversation records who the customer is, so "remove everyone not on the team" could eject
124
+ * the customer themselves. The consequence is that taking someone's `support:member` scope
125
+ * away never removed their participant docs — without this check they would keep reading
126
+ * every support conversation they were ever added to, forever.
127
+ *
128
+ * `role: 'agent'` is only ever set on support participants (participantsService
129
+ * .markSupportAgents), so this touches nobody else — least of all a support agent's own
130
+ * support conversation, where they are the customer and carry no role.
131
+ */
132
+ if (conversation.operationMode === 'support' && participant.role === 'agent' && !await supportAccess.isStaff(user))
133
+ return false;
134
+ // Validate participant membership (cheap array includes)
135
+ if (!conversation.participants?.includes(user) && !requiredAnyStatus.some((single) => ['left', 'removed'].includes(single)))
136
+ return false;
137
+ // Status check
138
+ if (!requiredAnyStatus.includes(participant.state))
139
+ return false;
140
+ // Rights check (all required must be present)
141
+ if (requiredRights && !requiredRights.every(r => participant.rights?.includes(r)))
142
+ return false;
143
+ return true;
144
+ }
145
+ }
146
+ export default new ParticipantsService();
147
+ //# sourceMappingURL=participants.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participants.service.js","sourceRoot":"","sources":["../../../src/services/participants.service.ts"],"names":[],"mappings":"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"}
@@ -63,6 +63,7 @@ export declare class StorageService extends Immutable<Storage> {
63
63
  prefix?: string;
64
64
  customer?: string;
65
65
  list?: string;
66
+ conversation?: string;
66
67
  }): {
67
68
  token: string;
68
69
  expiresIn: number;
@@ -107,6 +108,7 @@ export declare class StorageService extends Immutable<Storage> {
107
108
  prefix?: string;
108
109
  customer?: string;
109
110
  list?: string;
111
+ conversation?: string;
110
112
  }): boolean;
111
113
  /**
112
114
  * Creates a secure download URL with time-limited token for a storage file.
@@ -132,11 +134,14 @@ export declare class StorageService extends Immutable<Storage> {
132
134
  app?: string;
133
135
  url?: string;
134
136
  prefix?: string;
137
+ conversation?: string;
135
138
  }): Promise<{
136
139
  url: string;
137
140
  expiresAt: number;
138
141
  }>;
139
- handleFile(uploadInternalId: ObjectId, index: number, file: MultipartFile | undefined): Promise<import("@lilaquadrat/interfaces").Stored<Upload>>;
142
+ handleFile(uploadInternalId: ObjectId, index: number, file: MultipartFile | undefined, options?: {
143
+ maxSize?: number;
144
+ }): Promise<import("@lilaquadrat/interfaces").Stored<Upload>>;
140
145
  complete(upload: Upload, options?: {
141
146
  useNode?: boolean;
142
147
  }): Promise<StorageFilePaths>;
@@ -196,8 +201,9 @@ export declare class StorageService extends Immutable<Storage> {
196
201
  * checks if the file was already successfully uploaded
197
202
  */
198
203
  exists(filename: string, prefix: string | undefined, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: {
199
- company: string;
200
- project: string | undefined;
204
+ company?: string;
205
+ project?: string | undefined;
206
+ conversation?: ObjectId;
201
207
  }): Promise<boolean>;
202
208
  /**
203
209
  * counts non-versioned storage documents bound to a specific assetId
@@ -210,13 +216,19 @@ export declare class StorageService extends Immutable<Storage> {
210
216
  checkAllowedSize(filesize: number, allowedSize: number): boolean;
211
217
  stats(company: string, project: string, customer?: ObjectId): Promise<MediaStats>;
212
218
  getByInternalId(internalId: ObjectId, options: Required<Options>): Promise<BasicData<Storage> | null>;
213
- getPathByFilename(filename: string, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options): Promise<WithId<Storage> | null>;
214
- getByFilename(filename: string, prefix: string | undefined, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Required<Options>): Promise<BasicData<Storage> | null>;
219
+ getPathByFilename(filename: string, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options & {
220
+ conversation?: ObjectId;
221
+ }): Promise<WithId<Storage> | null>;
222
+ getByFilename(filename: string, prefix: string | undefined, app: string, customer: ObjectId | undefined, list: ObjectId | undefined, options: Options & {
223
+ conversation?: ObjectId;
224
+ }): Promise<BasicData<Storage> | null>;
215
225
  getFilesNotInPaths(paths: string[], app: string, company: string, project: string): Promise<WithId<Storage>[]>;
216
226
  removeBlobMembers(internalId: ObjectId, customer: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
217
227
  removeBlob(internalId: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
228
+ removeBlobConversation(internalId: ObjectId, conversation: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
218
229
  removeBlobViaPath(storage: WithId<Storage>[], options: Required<UserAppOptionsRequired>): Promise<void>;
219
230
  removeBlobVersion(parentId: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
231
+ removeConversationBlobVersion(parentId: ObjectId, conversation: ObjectId, options: Required<UserAppOptionsRequired>): Promise<void>;
220
232
  }
221
233
  declare const _default: StorageService;
222
234
  export default _default;
@@ -205,6 +205,10 @@ export class StorageService extends Immutable {
205
205
  console.debug('[validateSecureToken] list mismatch', { token: tokenLimits.list, provided: checkLimits.list });
206
206
  return false;
207
207
  }
208
+ if (tokenLimits.conversation && tokenLimits.conversation !== checkLimits.conversation) {
209
+ console.debug('[validateSecureToken] conversation mismatch', { token: tokenLimits.conversation, provided: checkLimits.conversation });
210
+ return false;
211
+ }
208
212
  // For URL limit, check if requested URL starts with the token's allowed prefix
209
213
  if (tokenLimits.url && checkLimits.url && !checkLimits.url.startsWith(tokenLimits.url)) {
210
214
  console.debug('[validateSecureToken] url mismatch', { tokenUrl: tokenLimits.url, providedUrl: checkLimits.url });
@@ -247,6 +251,7 @@ export class StorageService extends Immutable {
247
251
  project: limits?.project || storage.project,
248
252
  app: limits?.app || storage.app,
249
253
  prefix: limits?.prefix || storage.prefix,
254
+ conversation: limits?.conversation || storage.conversation?.toString(),
250
255
  url: limits?.url,
251
256
  };
252
257
  // Generate secure token
@@ -254,13 +259,16 @@ export class StorageService extends Immutable {
254
259
  // Construct download URL
255
260
  const baseUrl = process.env.SECURE_URL;
256
261
  const path = storage.prefix ? `${storage.prefix}/${storage.filename}` : storage.filename;
257
- const url = `${baseUrl}/${storage.app}/${storage.company}/${storage.project}/${path}?token=${tokenData.token}`;
262
+ const scope = storage.company
263
+ ? `${storage.app}/${storage.company}/${storage.project}`
264
+ : `${storage.app}/${storage.conversation}`;
265
+ const url = `${baseUrl}/${scope}/${path}?token=${tokenData.token}`;
258
266
  return {
259
267
  url,
260
268
  expiresAt: tokenData.expiresAt,
261
269
  };
262
270
  }
263
- async handleFile(uploadInternalId, index, file) {
271
+ async handleFile(uploadInternalId, index, file, options) {
264
272
  if (!file)
265
273
  throw new Error('FILE_MISSING');
266
274
  /**
@@ -278,26 +286,20 @@ export class StorageService extends Immutable {
278
286
  await fs.promises.unlink(path.file);
279
287
  throw new Error('FILE_SIZE');
280
288
  }
281
- // this is not correct.
282
- // the idea is, the development does not work like that.
283
- // needs to be reintroduced
284
- // if (uploadFile.size) {
285
- //
286
- // const chunkStat = await fs.promises.stat(path.file);
287
- // const existingSize = uploadFile.paths?.length
288
- // ? (await Promise.all(uploadFile.paths.map((p) => fs.promises.stat(p.path).then((s) => s.size).catch(() => 0))))
289
- // .reduce((sum, s) => sum + s, 0)
290
- // : 0;
291
- // if (existingSize + chunkStat.size > uploadFile.size) {
292
- // await fs.promises.unlink(path.file);
293
- // throw new Error('FILE_SIZE');
294
- // }
295
- // }
296
289
  const updloadState = await UploadService.addPathToUpload(uploadInternalId, index, path.file);
297
290
  /**
298
291
  * if all chunks are there, complete the file and the upload
299
292
  */
300
293
  if (updloadState.chunks === updloadState.paths?.length) {
294
+ if (options?.maxSize) {
295
+ const sizes = await Promise.all((updloadState.paths ?? [])
296
+ .map((single) => fs.promises.stat(single.path).then((stat) => stat.size).catch(() => 0)));
297
+ const total = sizes.reduce((sum, size) => sum + size, 0);
298
+ if (total > options.maxSize) {
299
+ await this.removeUploadFiles(updloadState);
300
+ throw new Error('FILE_SIZE');
301
+ }
302
+ }
301
303
  await UploadService.setState(updloadState._id, 'waiting');
302
304
  await JetStreamService.publish('actions.files.uploaded', updloadState);
303
305
  }
@@ -903,16 +905,20 @@ export class StorageService extends Immutable {
903
905
  */
904
906
  exists(filename, prefix, app, customer, list, options) {
905
907
  const query = {
906
- company: options.company,
907
- project: options.project,
908
908
  filename,
909
909
  prefix,
910
910
  app,
911
911
  };
912
+ if (options.company)
913
+ query.company = options.company;
914
+ if (options.project)
915
+ query.project = options.project;
912
916
  if (customer)
913
917
  query.customer = customer;
914
918
  if (list)
915
919
  query.list = list;
920
+ if (options.conversation)
921
+ query.conversation = options.conversation;
916
922
  return StorageModel.db.countDocuments(query)
917
923
  .then((exists) => exists >= 1);
918
924
  }
@@ -992,14 +998,18 @@ export class StorageService extends Immutable {
992
998
  const query = {
993
999
  filename: filenameObject.filename,
994
1000
  prefix: filenameObject.directory,
995
- company: options.company,
996
- project: options.project,
997
1001
  app,
998
1002
  };
1003
+ if (options.company)
1004
+ query.company = options.company;
1005
+ if (options.project)
1006
+ query.project = options.project;
999
1007
  if (customer)
1000
1008
  query.customer = customer;
1001
1009
  if (list)
1002
1010
  query.list = list;
1011
+ if (options.conversation)
1012
+ query.conversation = options.conversation;
1003
1013
  return StorageModel.db.findOne(query, {
1004
1014
  projection: { path: 1, metadata: 1 },
1005
1015
  });
@@ -1008,14 +1018,18 @@ export class StorageService extends Immutable {
1008
1018
  const query = {
1009
1019
  filename,
1010
1020
  prefix,
1011
- company: options.company,
1012
- project: options.project,
1013
1021
  app,
1014
1022
  };
1023
+ if (options.company)
1024
+ query.company = options.company;
1025
+ if (options.project)
1026
+ query.project = options.project;
1015
1027
  if (customer)
1016
1028
  query.customer = customer;
1017
1029
  if (list)
1018
1030
  query.list = list;
1031
+ if (options.conversation)
1032
+ query.conversation = options.conversation;
1019
1033
  return StorageModel.db.findOne(query);
1020
1034
  }
1021
1035
  getFilesNotInPaths(paths, app, company, project) {
@@ -1051,6 +1065,15 @@ export class StorageService extends Immutable {
1051
1065
  await this.delete(internalId, options.user, options.app);
1052
1066
  await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
1053
1067
  }
1068
+ async removeBlobConversation(internalId, conversation, options) {
1069
+ const storage = await this.model.db.findOne({ _id: internalId, app: 'conversations', conversation });
1070
+ if (!storage)
1071
+ throw new Error('BLOB_NOT_FOUND');
1072
+ await fs.promises.unlink(`${this.options.dataFolder}/${storage.path}`);
1073
+ await this.removeConversationBlobVersion(internalId, conversation, options);
1074
+ await this.delete(internalId, options.user, options.app);
1075
+ await jetstreamService.publish(`edge.${storage.app}.removed`, storage);
1076
+ }
1054
1077
  async removeBlobViaPath(storage, options) {
1055
1078
  if (!storage.length)
1056
1079
  throw new Error('BLOB_NOT_FOUND');
@@ -1071,6 +1094,16 @@ export class StorageService extends Immutable {
1071
1094
  }));
1072
1095
  }
1073
1096
  }
1097
+ async removeConversationBlobVersion(parentId, conversation, options) {
1098
+ const versions = await this.model.db.find({ parent: parentId, conversation }).toArray();
1099
+ if (versions.length) {
1100
+ await Promise.allSettled(versions.map(async (single) => {
1101
+ await fs.promises.unlink(`${this.options.dataFolder}/${single.path}`);
1102
+ await this.delete(single._id, options.user, options.app);
1103
+ await jetstreamService.publish(`edge.${single.app}.removed`, single);
1104
+ }));
1105
+ }
1106
+ }
1074
1107
  }
1075
1108
  export default new StorageService({
1076
1109
  dataFolder: process.env.MEDIA_FOLDERS_DATA,