@yrpri/api 9.0.220 → 9.0.221

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 (53) hide show
  1. package/agents/managers/subscriptionManager.js +2 -2
  2. package/app.js +11 -8
  3. package/controllers/communities.cjs +3 -1
  4. package/controllers/groups.cjs +1 -1
  5. package/controllers/images.cjs +12 -1
  6. package/models/domain.cjs +1 -1
  7. package/models/image.cjs +39 -12
  8. package/package.json +83 -63
  9. package/scripts/cloning/clearUsersForCommunitiesFromUrl.js +1 -1
  10. package/scripts/cloning/cloneFromUrlScript.js +1 -1
  11. package/scripts/cloning/cloneWBFromUrlScriptAndCreateLinks.js +1 -1
  12. package/scripts/cloning/cloneWBFromUrlScriptNoUsersOrPoints.js +1 -1
  13. package/scripts/cloning/cloneWBSerbianFromUrlScriptAndCreateLinks.js +1 -1
  14. package/scripts/cloning/copyCommunityConfigAndTranslationsFromURL.js +1 -1
  15. package/scripts/cloning/copyGroupConfigAndTranslationsFromURL.js +1 -1
  16. package/scripts/cloning/copyPostVideosFromURL.js +1 -1
  17. package/scripts/cloning/deepCloneSerbianWBFromUrlScriptAndCreateLinks.js +1 -1
  18. package/scripts/cloning/deepCloneWBFromUrlScriptAndCreateLinks.js +1 -1
  19. package/scripts/cloning/setAdminsFromURL.js +1 -1
  20. package/scripts/cloning/setExternalIdsFromURL.js +1 -1
  21. package/scripts/endorsementFraudDetection/bulkDeleteDuplicateEndorsmentsFromUrl.js +1 -1
  22. package/scripts/landUseGame/export3Ddata.js +1 -1
  23. package/scripts/movePostsToGroupsRecountGroupFromUrl.js +1 -1
  24. package/scripts/recountALLCommunityGroupCounts.js +1 -1
  25. package/scripts/recountCommunitesFromUrl.js +1 -1
  26. package/scripts/recountCommunity.js +1 -1
  27. package/scripts/setLanguageOnGroupCommunitesFromUrl.js +1 -1
  28. package/services/engine/allOurIdeas/aiHelper.d.ts +1 -2
  29. package/services/engine/analytics/manager.cjs +1 -1
  30. package/services/engine/analytics/plausible/manager.cjs +1 -1
  31. package/services/engine/analytics/utils.cjs +1 -1
  32. package/services/engine/notifications/emails_utils.cjs +10 -1
  33. package/services/engine/recommendations/events_manager.cjs +1 -1
  34. package/services/engine/reports/common_utils.cjs +1 -1
  35. package/services/llms/baseChatBot.d.ts +1 -2
  36. package/services/llms/imageGeneration/s3Service.js +25 -11
  37. package/services/scripts/translation_replace_text_from_url.js +1 -1
  38. package/services/utils/redisConnection.cjs +2 -3
  39. package/services/utils/translation_cloning.cjs +1 -1
  40. package/services/utils/translation_helpers.cjs +1 -1
  41. package/tests/emails_utils.test.cjs +130 -0
  42. package/tests/emails_utils.test.d.cts +1 -0
  43. package/tests/multerSharpS3Compat.test.cjs +229 -0
  44. package/tests/multerSharpS3Compat.test.d.cts +1 -0
  45. package/tests/requestCompat.test.cjs +288 -0
  46. package/tests/requestCompat.test.d.cts +1 -0
  47. package/utils/multerSharpS3Compat.cjs +230 -0
  48. package/utils/multerSharpS3Compat.d.cts +22 -0
  49. package/utils/passportSsoCompat.cjs +15 -0
  50. package/utils/passportSsoCompat.d.cts +2 -0
  51. package/utils/recount_utils.cjs +1 -1
  52. package/utils/requestCompat.cjs +180 -0
  53. package/utils/requestCompat.d.cts +9 -0
@@ -1,5 +1,4 @@
1
1
  import { OpenAI } from "openai";
2
- import { Stream } from "openai/streaming";
3
2
  import { WebSocket } from "ws";
4
3
  export declare class AiHelper {
5
4
  openaiClient: OpenAI;
@@ -16,7 +15,7 @@ export declare class AiHelper {
16
15
  getModerationResponse: (instructions: string, question: string, answerToModerate: string) => Promise<boolean>;
17
16
  streamChatCompletions(messages: any[]): Promise<void>;
18
17
  sendToClient(sender: string, message: string, type?: string): void;
19
- streamWebSocketResponses(stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
18
+ streamWebSocketResponses(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
20
19
  getAnswerIdeas(question: string, previousIdeas: string[] | null, firstMessage: string | null): Promise<string | null | undefined>;
21
20
  getAiAnalysis(questionId: number, contextPrompt: string, answers: AoiChoiceData[], cacheKeyForFullResponse: string, redisClient: any, locale: string, topOrBottomIdeasText: string, typeOfAnalysisText: string): Promise<string | null | undefined>;
22
21
  }
@@ -6,7 +6,7 @@ const importCommunity = require('./utils.cjs').importCommunity;
6
6
  const importGroup = require('./utils.cjs').importGroup;
7
7
  const importPost = require('./utils.cjs').importPost;
8
8
  const importPoint = require('./utils.cjs').importPoint;
9
- const request = require('request');
9
+ const request = require("../../../utils/requestCompat.cjs");
10
10
  const updateDomain = (domainId, done) => {
11
11
  log.info('updateDomain');
12
12
  models.Domain.unscoped().findOne({
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  const models = require("../../../../models/index.cjs");
3
3
  const log = require("../../../../utils/logger.cjs");
4
- const request = require("request");
4
+ const request = require("../../../../utils/requestCompat.cjs");
5
5
  const moment = require("moment");
6
6
  // This SQL is needed to allow the site API
7
7
  // UPDATE api_keys SET scopes = '{sites:provision:*}' WHERE name = 'Development';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  const _ = require('lodash');
3
- const request = require('request');
3
+ const request = require("../../../utils/requestCompat.cjs");
4
4
  const models = require('../../../models/index.cjs');
5
5
  const log = require('../../../utils/logger.cjs');
6
6
  const convertToString = (integer, type) => {
@@ -242,7 +242,9 @@ const filterNotificationForDelivery = function (notification, user, template, su
242
242
  }
243
243
  else {
244
244
  const redisKey = `${SUPPRESSION_KEYBASE}${user.id}`;
245
- redisConnection.get(redisKey, (error, found) => {
245
+ redisConnection
246
+ .get(redisKey)
247
+ .then((found) => {
246
248
  if (found) {
247
249
  log.info(`Suppressing emails for user ${user.email} settings ${LIMIT_EMAILS_FOR_SECONDS}`);
248
250
  callback();
@@ -250,6 +252,13 @@ const filterNotificationForDelivery = function (notification, user, template, su
250
252
  else {
251
253
  processNotification(notification, user, template, subject, callback);
252
254
  }
255
+ })
256
+ .catch((error) => {
257
+ log.warn("Redis suppression lookup failed, continuing delivery", {
258
+ err: error,
259
+ userId: user.id,
260
+ });
261
+ processNotification(notification, user, template, subject, callback);
253
262
  });
254
263
  }
255
264
  });
@@ -3,7 +3,7 @@ const models = require('../../../models/index.cjs');
3
3
  const _ = require('lodash');
4
4
  const async = require('async');
5
5
  const log = require('../../../utils/logger.cjs');
6
- const request = require('request');
6
+ const request = require("../../../utils/requestCompat.cjs");
7
7
  let airbrake = null;
8
8
  if (process.env.AIRBRAKE_PROJECT_ID) {
9
9
  airbrake = require('../../utils/airbrake.cjs');
@@ -6,7 +6,7 @@ const moment = require('moment');
6
6
  const skipEmail = false;
7
7
  const aws = require('aws-sdk');
8
8
  const log = require('../../../utils/logger.cjs');
9
- const request = require('request');
9
+ const request = require("../../../utils/requestCompat.cjs");
10
10
  const fs = require('fs');
11
11
  const downloadImage = (uri, filename, callback) => {
12
12
  request.head(uri, (err, res, body) => {
@@ -1,5 +1,4 @@
1
1
  import { OpenAI } from "openai";
2
- import { Stream } from "openai/streaming.js";
3
2
  import WebSocket from "ws";
4
3
  import ioredis from "ioredis";
5
4
  export declare class YpBaseChatBot {
@@ -26,7 +25,7 @@ export declare class YpBaseChatBot {
26
25
  sendAgentCompleted(name: string, lastAgent?: boolean, error?: string | undefined): void;
27
26
  sendAgentUpdate(message: string): void;
28
27
  sendToClient(sender: YpSenderType, message: string, type?: YpAssistantMessageType, uniqueToken?: string | undefined, hiddenContextMessage?: boolean): void;
29
- streamWebSocketResponses(stream: Stream<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
28
+ streamWebSocketResponses(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): Promise<void>;
30
29
  saveMemoryIfNeeded(): Promise<void>;
31
30
  setChatLog(chatLog: YpSimpleChatLog[]): Promise<void>;
32
31
  conversation(chatLog: YpSimpleChatLog[]): Promise<void>;
@@ -37,13 +37,17 @@ export class S3Service {
37
37
  const params = {
38
38
  Bucket: bucket,
39
39
  Key: key,
40
- ACL: "private",
41
40
  };
42
41
  log.info(`Disabling/Deleting Key from S3: ${JSON.stringify(params)}`);
43
42
  return new Promise((resolve, reject) => {
44
- s3.putObjectAcl(params, (err, data) => {
43
+ s3.deleteObject(params, (err, data) => {
45
44
  if (err) {
46
- log.error(`Error deleting image from S3: ${err}`);
45
+ log.error("Error deleting image from S3", {
46
+ imageUrl,
47
+ bucket,
48
+ key,
49
+ err,
50
+ });
47
51
  reject(err);
48
52
  }
49
53
  else {
@@ -87,25 +91,35 @@ export class S3Service {
87
91
  let bucket, key;
88
92
  const cfImageProxyDomain = process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN;
89
93
  const s3Bucket = process.env.S3_BUCKET;
94
+ const parsedUrl = new URL(imageUrl);
90
95
  if (cfImageProxyDomain && imageUrl.includes(cfImageProxyDomain)) {
91
- const urlPath = new URL(imageUrl).pathname;
92
- const [, ...pathParts] = urlPath.split("/");
96
+ const [, ...pathParts] = parsedUrl.pathname.split("/");
93
97
  bucket = s3Bucket;
94
98
  key = pathParts.join("/");
95
99
  }
100
+ else if (parsedUrl.hostname === process.env.S3_ENDPOINT) {
101
+ const [, maybeBucket, ...pathParts] = parsedUrl.pathname.split("/");
102
+ bucket = process.env.MINIO_ROOT_USER ? maybeBucket || s3Bucket : s3Bucket;
103
+ key = process.env.MINIO_ROOT_USER ? pathParts.join("/") : parsedUrl.pathname.slice(1);
104
+ }
96
105
  else {
97
- const match = imageUrl.match(/https:\/\/(.+?)\.s3\.amazonaws\.com\/(.+)/);
98
- if (match) {
99
- bucket = match[1];
100
- key = match[2];
106
+ const hostParts = parsedUrl.hostname.split(".");
107
+ if (hostParts.length >= 4 && hostParts[1] === "s3") {
108
+ bucket = hostParts[0];
109
+ key = parsedUrl.pathname.replace(/^\/+/, "");
101
110
  }
102
111
  }
103
112
  return { bucket, key };
104
113
  }
105
114
  async deleteMediaFormatsUrls(formats) {
106
115
  for (const url of formats) {
107
- await this.deleteS3Url(url);
108
- log.info(`Deleted image from S3: ${url}`);
116
+ try {
117
+ await this.deleteS3Url(url);
118
+ log.info(`Deleted image from S3: ${url}`);
119
+ }
120
+ catch (error) {
121
+ log.warn("Best-effort image cleanup failed", { url, error });
122
+ }
109
123
  }
110
124
  }
111
125
  }
@@ -2,7 +2,7 @@ const models = require('../../models/index.cjs');
2
2
  const async = require('async');
3
3
  const _ = require('lodash');
4
4
  const fs = require('fs');
5
- const request = require('request');
5
+ const request = require("../../utils/requestCompat.cjs");
6
6
  const farmhash = require('farmhash');
7
7
  //const communityId = "1264"; //process.argv[2];
8
8
  //const communityId = "1402"; //process.argv[2];
@@ -9,18 +9,17 @@ if (process.env.REDIS_URL) {
9
9
  }
10
10
  if (redisUrl.includes("rediss://")) {
11
11
  redisClient = redis.createClient({
12
- legacyMode: false,
13
12
  url: redisUrl,
14
13
  pingInterval: 10000,
15
14
  socket: { tls: true, rejectUnauthorized: false },
16
15
  });
17
16
  }
18
17
  else {
19
- redisClient = redis.createClient({ legacyMode: true, url: redisUrl });
18
+ redisClient = redis.createClient({ url: redisUrl });
20
19
  }
21
20
  }
22
21
  else {
23
- redisClient = redis.createClient({ legacyMode: true });
22
+ redisClient = redis.createClient();
24
23
  }
25
24
  redisClient.on("error", (err) => log.error("Backend Redis client error", err));
26
25
  redisClient.on("connect", () => log.info("Backend Redis client is connect"));
@@ -3,7 +3,7 @@ const models = require('../../models/index.cjs');
3
3
  const async = require('async');
4
4
  const _ = require('lodash');
5
5
  const fs = require('fs');
6
- const request = require('request');
6
+ const request = require("../../utils/requestCompat.cjs");
7
7
  const farmhash = require('farmhash');
8
8
  const fixTargetLocale = require('./translation_helpers.cjs').fixTargetLocale;
9
9
  // For post get translations in all locales (that exists)
@@ -3,7 +3,7 @@ const models = require("../../models/index.cjs");
3
3
  const async = require("async");
4
4
  const _ = require("lodash");
5
5
  const fs = require("fs");
6
- const request = require("request");
6
+ const request = require("../../utils/requestCompat.cjs");
7
7
  const farmhash = require("farmhash");
8
8
  const log = require('../../utils/logger.cjs');
9
9
  const fixTargetLocale = (itemTargetLocale) => {
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ const test = require("node:test");
3
+ const assert = require("node:assert/strict");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const vm = require("node:vm");
7
+ const Module = require("node:module");
8
+ const emailsUtilsPath = path.resolve(__dirname, "../services/engine/notifications/emails_utils.cjs");
9
+ const loadEmailsUtils = ({ logger, redisConnection, queue, models }) => {
10
+ const source = fs.readFileSync(emailsUtilsPath, "utf8");
11
+ const module = { exports: {} };
12
+ const baseRequire = Module.createRequire(emailsUtilsPath);
13
+ const mockedRequire = (request) => {
14
+ switch (request) {
15
+ case "../../../utils/logger.cjs":
16
+ return logger;
17
+ case "../../utils/redisConnection.cjs":
18
+ return redisConnection;
19
+ case "../../workers/queue.cjs":
20
+ return queue;
21
+ case "../../../models/index.cjs":
22
+ return models;
23
+ case "nodemailer":
24
+ return {
25
+ createTransport() {
26
+ return {
27
+ verify(callback) {
28
+ if (callback) {
29
+ callback(null, true);
30
+ }
31
+ },
32
+ };
33
+ },
34
+ };
35
+ case "../../utils/i18n.cjs":
36
+ return {
37
+ t(value) {
38
+ return value;
39
+ },
40
+ };
41
+ default:
42
+ return baseRequire(request);
43
+ }
44
+ };
45
+ const wrapped = Module.wrap(source);
46
+ const compiled = vm.runInThisContext(wrapped, {
47
+ filename: emailsUtilsPath,
48
+ });
49
+ compiled(module.exports, mockedRequire, module, emailsUtilsPath, path.dirname(emailsUtilsPath));
50
+ return module.exports;
51
+ };
52
+ test("filterNotificationForDelivery falls back to delivery when Redis suppression lookup fails", async () => {
53
+ const queuedEmails = [];
54
+ const redisSetExCalls = [];
55
+ const warnings = [];
56
+ const emailsUtils = loadEmailsUtils({
57
+ logger: {
58
+ info() { },
59
+ error() { },
60
+ warn(message, metadata) {
61
+ warnings.push({ message, metadata });
62
+ },
63
+ },
64
+ redisConnection: {
65
+ async get() {
66
+ throw new Error("redis down");
67
+ },
68
+ setEx(...args) {
69
+ redisSetExCalls.push(args);
70
+ },
71
+ },
72
+ queue: {
73
+ add(...args) {
74
+ queuedEmails.push(args);
75
+ },
76
+ },
77
+ models: {
78
+ AcNotification: {
79
+ METHOD_MUTED: "muted",
80
+ FREQUENCY_AS_IT_HAPPENS: "as_it_happens",
81
+ },
82
+ },
83
+ });
84
+ const group = {
85
+ name: "visible-group",
86
+ async hasGroupAdmins() {
87
+ return false;
88
+ },
89
+ };
90
+ const user = {
91
+ id: 42,
92
+ email: "person@example.com",
93
+ notifications_settings: {
94
+ point_activity: {
95
+ method: "email",
96
+ frequency: "as_it_happens",
97
+ },
98
+ },
99
+ };
100
+ const notification = {
101
+ from_notification_setting: "point_activity",
102
+ AcActivities: [
103
+ {
104
+ Domain: { id: 1 },
105
+ Community: null,
106
+ Group: group,
107
+ Point: {
108
+ Group: group,
109
+ },
110
+ Post: null,
111
+ },
112
+ ],
113
+ };
114
+ await new Promise((resolve, reject) => {
115
+ emailsUtils.filterNotificationForDelivery(notification, user, "point_activity", "A subject", (error) => {
116
+ if (error) {
117
+ reject(error);
118
+ }
119
+ else {
120
+ resolve();
121
+ }
122
+ });
123
+ });
124
+ assert.equal(warnings.length, 1);
125
+ assert.equal(warnings[0].message, "Redis suppression lookup failed, continuing delivery");
126
+ assert.equal(warnings[0].metadata.userId, 42);
127
+ assert.equal(queuedEmails.length, 1);
128
+ assert.equal(queuedEmails[0][0], "send-one-email");
129
+ assert.equal(redisSetExCalls.length, 1);
130
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ const test = require("node:test");
3
+ const assert = require("node:assert/strict");
4
+ const Module = require("node:module");
5
+ const { PassThrough } = require("node:stream");
6
+ const sharpPath = require.resolve("sharp");
7
+ const mimeTypesPath = require.resolve("mime-types");
8
+ const multerSharpS3CompatPath = require.resolve("../utils/multerSharpS3Compat.cjs");
9
+ const injectMockModule = (modulePath, moduleExports) => {
10
+ const mockModule = new Module(modulePath);
11
+ mockModule.filename = modulePath;
12
+ mockModule.loaded = true;
13
+ mockModule.exports = moduleExports;
14
+ require.cache[modulePath] = mockModule;
15
+ };
16
+ const loadMulterSharpS3Compat = ({ mockSharp, mockMimeTypes }) => {
17
+ const originalSharpModule = require.cache[sharpPath];
18
+ const originalMimeTypesModule = require.cache[mimeTypesPath];
19
+ const originalCompatModule = require.cache[multerSharpS3CompatPath];
20
+ injectMockModule(sharpPath, mockSharp);
21
+ injectMockModule(mimeTypesPath, mockMimeTypes);
22
+ delete require.cache[multerSharpS3CompatPath];
23
+ const multerSharpS3Compat = require(multerSharpS3CompatPath);
24
+ return {
25
+ multerSharpS3Compat,
26
+ restore() {
27
+ delete require.cache[multerSharpS3CompatPath];
28
+ if (originalSharpModule) {
29
+ require.cache[sharpPath] = originalSharpModule;
30
+ }
31
+ else {
32
+ delete require.cache[sharpPath];
33
+ }
34
+ if (originalMimeTypesModule) {
35
+ require.cache[mimeTypesPath] = originalMimeTypesModule;
36
+ }
37
+ else {
38
+ delete require.cache[mimeTypesPath];
39
+ }
40
+ if (originalCompatModule) {
41
+ require.cache[multerSharpS3CompatPath] = originalCompatModule;
42
+ }
43
+ },
44
+ };
45
+ };
46
+ const createMockSharp = (requestedFormats) => {
47
+ return () => {
48
+ let selectedFormat = null;
49
+ const stream = new PassThrough();
50
+ stream.resize = () => stream;
51
+ stream.toFormat = (format) => {
52
+ selectedFormat = format;
53
+ requestedFormats.push(format);
54
+ return stream;
55
+ };
56
+ process.nextTick(() => {
57
+ stream.end(Buffer.from(`formatted:${selectedFormat || "original"}`));
58
+ });
59
+ return stream;
60
+ };
61
+ };
62
+ const createMockS3 = ({ failKeys = [] } = {}) => {
63
+ const uploads = [];
64
+ const deletedObjects = [];
65
+ return {
66
+ uploads,
67
+ deletedObjects,
68
+ s3: {
69
+ upload(params) {
70
+ uploads.push(params);
71
+ return {
72
+ promise() {
73
+ return new Promise((resolve, reject) => {
74
+ const chunks = [];
75
+ params.Body.on("data", (chunk) => {
76
+ chunks.push(chunk);
77
+ });
78
+ params.Body.on("error", reject);
79
+ params.Body.on("end", () => {
80
+ if (failKeys.includes(params.Key)) {
81
+ reject(new Error(`upload failed for ${params.Key}`));
82
+ }
83
+ else {
84
+ resolve({
85
+ Bucket: params.Bucket,
86
+ Key: params.Key,
87
+ Location: `https://example.com/${params.Key}`,
88
+ uploadedBody: Buffer.concat(chunks).toString("utf8"),
89
+ });
90
+ }
91
+ });
92
+ });
93
+ },
94
+ };
95
+ },
96
+ deleteObject(params) {
97
+ deletedObjects.push(params);
98
+ return {
99
+ promise() {
100
+ return Promise.resolve();
101
+ },
102
+ };
103
+ },
104
+ },
105
+ };
106
+ };
107
+ test("multerSharpS3Compat honors file.outputFormat over the default format", async (t) => {
108
+ const requestedFormats = [];
109
+ const { s3, uploads } = createMockS3();
110
+ const { multerSharpS3Compat, restore } = loadMulterSharpS3Compat({
111
+ mockSharp: createMockSharp(requestedFormats),
112
+ mockMimeTypes: {
113
+ contentType(format) {
114
+ return `image/${format}`;
115
+ },
116
+ },
117
+ });
118
+ t.after(restore);
119
+ const storage = multerSharpS3Compat({
120
+ s3,
121
+ Bucket: "images",
122
+ toFormat: "png",
123
+ Key: "animated.gif",
124
+ });
125
+ const fileStream = new PassThrough();
126
+ fileStream.end(Buffer.from("source"));
127
+ const file = {
128
+ mimetype: "image/gif",
129
+ originalname: "animated.gif",
130
+ outputFormat: "gif",
131
+ stream: fileStream,
132
+ };
133
+ const result = await new Promise((resolve, reject) => {
134
+ storage._handleFile({}, file, (error, value) => {
135
+ if (error) {
136
+ reject(error);
137
+ }
138
+ else {
139
+ resolve(value);
140
+ }
141
+ });
142
+ });
143
+ assert.deepEqual(requestedFormats, ["gif"]);
144
+ assert.equal(uploads.length, 1);
145
+ assert.equal(uploads[0].ContentType, "image/gif");
146
+ assert.equal(result.default.ContentType, "image/gif");
147
+ assert.equal(result.mimetype, "image/gif");
148
+ });
149
+ test("multerSharpS3Compat rolls back earlier variants when a later upload fails", async (t) => {
150
+ const requestedFormats = [];
151
+ const failingKey = "up/animated-large-2.gif";
152
+ const { s3, deletedObjects } = createMockS3({ failKeys: [failingKey] });
153
+ const { multerSharpS3Compat, restore } = loadMulterSharpS3Compat({
154
+ mockSharp: createMockSharp(requestedFormats),
155
+ mockMimeTypes: {
156
+ contentType(format) {
157
+ return `image/${format}`;
158
+ },
159
+ },
160
+ });
161
+ t.after(restore);
162
+ const storage = multerSharpS3Compat({
163
+ s3,
164
+ Bucket: "images",
165
+ toFormat: "png",
166
+ Key: "animated.gif",
167
+ multiple: true,
168
+ resize: [
169
+ { suffix: "-small-1", directory: "up" },
170
+ { suffix: "-large-2", directory: "up" },
171
+ ],
172
+ });
173
+ const fileStream = new PassThrough();
174
+ fileStream.end(Buffer.from("source"));
175
+ const file = {
176
+ mimetype: "image/gif",
177
+ originalname: "animated.gif",
178
+ outputFormat: "gif",
179
+ stream: fileStream,
180
+ };
181
+ const error = await new Promise((resolve) => {
182
+ storage._handleFile({}, file, (uploadError) => {
183
+ resolve(uploadError);
184
+ });
185
+ });
186
+ assert.match(error.message, /upload failed/);
187
+ assert.deepEqual(requestedFormats, ["gif", "gif"]);
188
+ assert.deepEqual(deletedObjects, [
189
+ {
190
+ Bucket: "images",
191
+ Key: "up/animated-small-1.gif",
192
+ },
193
+ ]);
194
+ });
195
+ test("_removeFile deletes every uploaded variant in a multi-variant upload", async (t) => {
196
+ const { s3, deletedObjects } = createMockS3();
197
+ const { multerSharpS3Compat, restore } = loadMulterSharpS3Compat({
198
+ mockSharp: createMockSharp([]),
199
+ mockMimeTypes: {
200
+ contentType(format) {
201
+ return `image/${format}`;
202
+ },
203
+ },
204
+ });
205
+ t.after(restore);
206
+ const storage = multerSharpS3Compat({
207
+ s3,
208
+ Bucket: "images",
209
+ Key: "animated.gif",
210
+ });
211
+ await new Promise((resolve, reject) => {
212
+ storage._removeFile({}, {
213
+ default: { Bucket: "images", Key: "up/a.gif" },
214
+ "-small-1": { Bucket: "images", Key: "up/a-small.gif" },
215
+ mimetype: "image/gif",
216
+ }, (error) => {
217
+ if (error) {
218
+ reject(error);
219
+ }
220
+ else {
221
+ resolve();
222
+ }
223
+ });
224
+ });
225
+ assert.deepEqual(deletedObjects, [
226
+ { Bucket: "images", Key: "up/a.gif" },
227
+ { Bucket: "images", Key: "up/a-small.gif" },
228
+ ]);
229
+ });
@@ -0,0 +1 @@
1
+ export {};