@yrpri/api 9.0.221 → 9.0.223
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.
package/models/image.cjs
CHANGED
|
@@ -192,6 +192,29 @@ module.exports = (sequelize, DataTypes) => {
|
|
|
192
192
|
});
|
|
193
193
|
return formats;
|
|
194
194
|
};
|
|
195
|
+
const getBelongsToManyAssociations = () => {
|
|
196
|
+
return Object.entries(Image.associations || {}).filter(([, association]) => association.associationType === "BelongsToMany");
|
|
197
|
+
};
|
|
198
|
+
const hasRemainingCollectionAssociations = async (imageId) => {
|
|
199
|
+
const includes = getBelongsToManyAssociations().map(([associationName]) => ({
|
|
200
|
+
association: associationName,
|
|
201
|
+
required: false,
|
|
202
|
+
through: { attributes: [] },
|
|
203
|
+
}));
|
|
204
|
+
if (!includes.length) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const imageWithAssociations = await Image.findByPk(imageId, {
|
|
208
|
+
include: includes,
|
|
209
|
+
});
|
|
210
|
+
if (!imageWithAssociations) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return includes.some(({ association }) => {
|
|
214
|
+
const associatedRecords = imageWithAssociations[association];
|
|
215
|
+
return Array.isArray(associatedRecords) && associatedRecords.length > 0;
|
|
216
|
+
});
|
|
217
|
+
};
|
|
195
218
|
Image.removeImageFromCollection = async (req, res) => {
|
|
196
219
|
const imageId = req.params.imageId;
|
|
197
220
|
const groupId = req.params.groupId;
|
|
@@ -199,14 +222,17 @@ module.exports = (sequelize, DataTypes) => {
|
|
|
199
222
|
const domainId = req.params.domainId;
|
|
200
223
|
const postId = req.params.postId;
|
|
201
224
|
const image = await Image.findByPk(imageId);
|
|
225
|
+
const removeByUserIdOnly = Boolean(req.query.removeByUserIdOnly);
|
|
226
|
+
const requestUserId = req.user?.id;
|
|
227
|
+
const isOwnedByRequestUser = image && requestUserId !== undefined && image.user_id === requestUserId;
|
|
202
228
|
let removed = false;
|
|
229
|
+
let remainingCollectionAssociations;
|
|
203
230
|
if (image) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
231
|
+
if (removeByUserIdOnly && !isOwnedByRequestUser) {
|
|
232
|
+
log.error("Could not remove image from collection not same user", {
|
|
233
|
+
imageId,
|
|
234
|
+
userId: requestUserId,
|
|
235
|
+
});
|
|
210
236
|
}
|
|
211
237
|
else if (groupId) {
|
|
212
238
|
const group = await sequelize.models.Group.findByPk(groupId);
|
|
@@ -296,6 +322,14 @@ module.exports = (sequelize, DataTypes) => {
|
|
|
296
322
|
});
|
|
297
323
|
if (postWithImage?.PostHeaderImages?.length) {
|
|
298
324
|
await post.removePostHeaderImage(image);
|
|
325
|
+
const remainingHeaderImageCount = typeof post.countPostHeaderImages === "function"
|
|
326
|
+
? await post.countPostHeaderImages()
|
|
327
|
+
: null;
|
|
328
|
+
if (remainingHeaderImageCount === 0 &&
|
|
329
|
+
post.cover_media_type === "image") {
|
|
330
|
+
post.cover_media_type = "none";
|
|
331
|
+
await post.save();
|
|
332
|
+
}
|
|
299
333
|
removed = true;
|
|
300
334
|
}
|
|
301
335
|
else if (postWithImage?.PostImages?.length) {
|
|
@@ -308,30 +342,35 @@ module.exports = (sequelize, DataTypes) => {
|
|
|
308
342
|
}
|
|
309
343
|
}
|
|
310
344
|
}
|
|
345
|
+
if (!removed && removeByUserIdOnly && isOwnedByRequestUser) {
|
|
346
|
+
remainingCollectionAssociations = await hasRemainingCollectionAssociations(image.id);
|
|
347
|
+
removed = !remainingCollectionAssociations;
|
|
348
|
+
}
|
|
311
349
|
if (removed) {
|
|
350
|
+
if (remainingCollectionAssociations === undefined) {
|
|
351
|
+
remainingCollectionAssociations = await hasRemainingCollectionAssociations(image.id);
|
|
352
|
+
}
|
|
353
|
+
if (remainingCollectionAssociations) {
|
|
354
|
+
log.info("Detached shared image from collection", { imageId: image.id });
|
|
355
|
+
return res
|
|
356
|
+
.status(200)
|
|
357
|
+
.json({ message: "Image removed from collection" });
|
|
358
|
+
}
|
|
312
359
|
image.deleted = true;
|
|
313
360
|
await image.save();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
imageId: image.id,
|
|
322
|
-
error,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
log.info("Deleted image", { imageId: image.id });
|
|
326
|
-
res.status(200).json({ message: "Image removed from collection" });
|
|
327
|
-
}).catch((error) => {
|
|
328
|
-
log.warn("Best-effort image cleanup import failed", {
|
|
361
|
+
try {
|
|
362
|
+
const { S3Service } = await import("../services/llms/imageGeneration/s3Service.js");
|
|
363
|
+
const mediaManager = new S3Service(process.env.CLOUDFLARE_API_KEY, process.env.CLOUDFLARE_ZONE_ID);
|
|
364
|
+
await mediaManager.deleteMediaFormatsUrls(image.formats ? JSON.parse(image.formats) : []);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
log.warn("Best-effort image cleanup failed", {
|
|
329
368
|
imageId: image.id,
|
|
330
369
|
error,
|
|
331
370
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
371
|
+
}
|
|
372
|
+
log.info("Deleted image", { imageId: image.id });
|
|
373
|
+
return res.status(200).json({ message: "Image removed from collection" });
|
|
335
374
|
}
|
|
336
375
|
else {
|
|
337
376
|
log.error("Could not remove image from collection");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yrpri/api",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.223",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Robert Bjarnason & Citizens Foundation",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@google-cloud/vertexai": "^1.10.0",
|
|
26
26
|
"@google-cloud/vision": "^5.3.5",
|
|
27
27
|
"@node-saml/passport-saml": "^5.1.0",
|
|
28
|
-
"@policysynth/agents": "^1.3.
|
|
28
|
+
"@policysynth/agents": "^1.3.174",
|
|
29
29
|
"async": "^3.2.6",
|
|
30
30
|
"authorized": "^1.0.0",
|
|
31
31
|
"aws-sdk": "^2.1693.0",
|
|
@@ -167,8 +167,5 @@
|
|
|
167
167
|
},
|
|
168
168
|
"cacheDirectories": [
|
|
169
169
|
"node_modules"
|
|
170
|
-
]
|
|
171
|
-
"engines": {
|
|
172
|
-
"node": "24.11.1"
|
|
173
|
-
}
|
|
170
|
+
]
|
|
174
171
|
}
|
|
@@ -2,6 +2,49 @@ import AWS from "aws-sdk";
|
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import axios from "axios";
|
|
4
4
|
import log from "../../../utils/loggerTs.js";
|
|
5
|
+
const normalizeEndpointHost = (endpoint) => {
|
|
6
|
+
if (!endpoint) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const normalizedUrl = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`);
|
|
11
|
+
return {
|
|
12
|
+
host: normalizedUrl.host,
|
|
13
|
+
hostname: normalizedUrl.hostname,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {
|
|
18
|
+
host: endpoint,
|
|
19
|
+
hostname: endpoint,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getBucketFromVirtualHost = (parsedUrl, normalizedEndpoint) => {
|
|
24
|
+
if (!normalizedEndpoint) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const hostSuffix = `.${normalizedEndpoint.host}`;
|
|
28
|
+
const hostnameSuffix = `.${normalizedEndpoint.hostname}`;
|
|
29
|
+
if (parsedUrl.host.endsWith(hostSuffix)) {
|
|
30
|
+
return parsedUrl.host.slice(0, -hostSuffix.length) || null;
|
|
31
|
+
}
|
|
32
|
+
if (parsedUrl.hostname.endsWith(hostnameSuffix)) {
|
|
33
|
+
return parsedUrl.hostname.slice(0, -hostnameSuffix.length) || null;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const getAwsBucketFromHostname = (hostname) => {
|
|
38
|
+
const s3MarkerIndex = hostname.indexOf(".s3.");
|
|
39
|
+
if (s3MarkerIndex > 0) {
|
|
40
|
+
return hostname.slice(0, s3MarkerIndex) || null;
|
|
41
|
+
}
|
|
42
|
+
const legacyS3MarkerIndex = hostname.indexOf(".s3-");
|
|
43
|
+
if (legacyS3MarkerIndex > 0) {
|
|
44
|
+
return hostname.slice(0, legacyS3MarkerIndex) || null;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
5
48
|
export class S3Service {
|
|
6
49
|
constructor(cloudflareApiKey, cloudflareZoneId) {
|
|
7
50
|
this.cloudflareApiKey = cloudflareApiKey;
|
|
@@ -91,21 +134,25 @@ export class S3Service {
|
|
|
91
134
|
let bucket, key;
|
|
92
135
|
const cfImageProxyDomain = process.env.CLOUDFLARE_IMAGE_PROXY_DOMAIN;
|
|
93
136
|
const s3Bucket = process.env.S3_BUCKET;
|
|
137
|
+
const normalizedEndpoint = normalizeEndpointHost(process.env.S3_ENDPOINT);
|
|
94
138
|
const parsedUrl = new URL(imageUrl);
|
|
95
139
|
if (cfImageProxyDomain && imageUrl.includes(cfImageProxyDomain)) {
|
|
96
140
|
const [, ...pathParts] = parsedUrl.pathname.split("/");
|
|
97
141
|
bucket = s3Bucket;
|
|
98
142
|
key = pathParts.join("/");
|
|
99
143
|
}
|
|
100
|
-
else if (
|
|
144
|
+
else if (normalizedEndpoint &&
|
|
145
|
+
(parsedUrl.hostname === normalizedEndpoint.hostname ||
|
|
146
|
+
parsedUrl.host === normalizedEndpoint.host)) {
|
|
101
147
|
const [, maybeBucket, ...pathParts] = parsedUrl.pathname.split("/");
|
|
102
148
|
bucket = process.env.MINIO_ROOT_USER ? maybeBucket || s3Bucket : s3Bucket;
|
|
103
149
|
key = process.env.MINIO_ROOT_USER ? pathParts.join("/") : parsedUrl.pathname.slice(1);
|
|
104
150
|
}
|
|
105
151
|
else {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
152
|
+
const virtualHostBucket = getBucketFromVirtualHost(parsedUrl, normalizedEndpoint) ||
|
|
153
|
+
getAwsBucketFromHostname(parsedUrl.hostname);
|
|
154
|
+
if (virtualHostBucket) {
|
|
155
|
+
bucket = virtualHostBucket;
|
|
109
156
|
key = parsedUrl.pathname.replace(/^\/+/, "");
|
|
110
157
|
}
|
|
111
158
|
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const Module = require("node:module");
|
|
5
|
+
const imageModelPath = require.resolve("../models/image.cjs");
|
|
6
|
+
const loggerPath = require.resolve("../utils/logger.cjs");
|
|
7
|
+
const injectMockModule = (modulePath, moduleExports) => {
|
|
8
|
+
const mockModule = new Module(modulePath);
|
|
9
|
+
mockModule.filename = modulePath;
|
|
10
|
+
mockModule.loaded = true;
|
|
11
|
+
mockModule.exports = moduleExports;
|
|
12
|
+
require.cache[modulePath] = mockModule;
|
|
13
|
+
};
|
|
14
|
+
const loadImageModelFactory = () => {
|
|
15
|
+
const originalLoggerModule = require.cache[loggerPath];
|
|
16
|
+
const originalImageModelModule = require.cache[imageModelPath];
|
|
17
|
+
injectMockModule(loggerPath, {
|
|
18
|
+
error() { },
|
|
19
|
+
info() { },
|
|
20
|
+
warn() { },
|
|
21
|
+
});
|
|
22
|
+
delete require.cache[imageModelPath];
|
|
23
|
+
return {
|
|
24
|
+
createImageModel: require(imageModelPath),
|
|
25
|
+
restore() {
|
|
26
|
+
delete require.cache[imageModelPath];
|
|
27
|
+
if (originalLoggerModule) {
|
|
28
|
+
require.cache[loggerPath] = originalLoggerModule;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
delete require.cache[loggerPath];
|
|
32
|
+
}
|
|
33
|
+
if (originalImageModelModule) {
|
|
34
|
+
require.cache[imageModelPath] = originalImageModelModule;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const createDataTypesStub = () => new Proxy({}, {
|
|
40
|
+
get() {
|
|
41
|
+
return {};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const createResponse = () => {
|
|
45
|
+
return {
|
|
46
|
+
body: null,
|
|
47
|
+
statusCode: null,
|
|
48
|
+
status(code) {
|
|
49
|
+
this.statusCode = code;
|
|
50
|
+
return this;
|
|
51
|
+
},
|
|
52
|
+
json(body) {
|
|
53
|
+
this.body = body;
|
|
54
|
+
return this;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const setupImageModel = ({ imageRecord, postWithImage, groupWithImage, remainingAssociations, remainingPostHeaderImageCount = 0, createImageModel, associations, }) => {
|
|
59
|
+
const post = {
|
|
60
|
+
cover_media_type: undefined,
|
|
61
|
+
saveCallCount: 0,
|
|
62
|
+
removedHeaderImages: [],
|
|
63
|
+
removedPostImages: [],
|
|
64
|
+
removedUserImages: [],
|
|
65
|
+
async save() {
|
|
66
|
+
this.saveCallCount += 1;
|
|
67
|
+
},
|
|
68
|
+
async removePostHeaderImage(image) {
|
|
69
|
+
this.removedHeaderImages.push(image.id);
|
|
70
|
+
},
|
|
71
|
+
async countPostHeaderImages() {
|
|
72
|
+
return remainingPostHeaderImageCount;
|
|
73
|
+
},
|
|
74
|
+
async removePostImage(image) {
|
|
75
|
+
this.removedPostImages.push(image.id);
|
|
76
|
+
},
|
|
77
|
+
async removePostUserImage(image) {
|
|
78
|
+
this.removedUserImages.push(image.id);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const group = {
|
|
82
|
+
removedLogoImages: [],
|
|
83
|
+
async removeGroupLogoImage(image) {
|
|
84
|
+
this.removedLogoImages.push(image.id);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const sequelize = {
|
|
88
|
+
define() {
|
|
89
|
+
return {};
|
|
90
|
+
},
|
|
91
|
+
models: {
|
|
92
|
+
Group: {
|
|
93
|
+
async findByPk() {
|
|
94
|
+
return groupWithImage !== undefined ? group : null;
|
|
95
|
+
},
|
|
96
|
+
async findOne() {
|
|
97
|
+
return groupWithImage;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
Post: {
|
|
101
|
+
async findByPk() {
|
|
102
|
+
return post;
|
|
103
|
+
},
|
|
104
|
+
async findOne() {
|
|
105
|
+
return postWithImage;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
const Image = createImageModel(sequelize, createDataTypesStub());
|
|
111
|
+
sequelize.models.Image = Image;
|
|
112
|
+
Image.associations =
|
|
113
|
+
associations || {
|
|
114
|
+
PostImages: { associationType: "BelongsToMany" },
|
|
115
|
+
PostHeaderImages: { associationType: "BelongsToMany" },
|
|
116
|
+
PostUserImages: { associationType: "BelongsToMany" },
|
|
117
|
+
};
|
|
118
|
+
Image.findByPk = async (_imageId, options) => {
|
|
119
|
+
if (options?.include) {
|
|
120
|
+
return remainingAssociations;
|
|
121
|
+
}
|
|
122
|
+
return imageRecord;
|
|
123
|
+
};
|
|
124
|
+
return { Image, post, group };
|
|
125
|
+
};
|
|
126
|
+
test("removeImageFromCollection detaches shared post header images without deleting the image", async (t) => {
|
|
127
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
128
|
+
t.after(restore);
|
|
129
|
+
const imageRecord = {
|
|
130
|
+
id: 42,
|
|
131
|
+
deleted: false,
|
|
132
|
+
saveCallCount: 0,
|
|
133
|
+
async save() {
|
|
134
|
+
this.saveCallCount += 1;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const { Image, post } = setupImageModel({
|
|
138
|
+
createImageModel,
|
|
139
|
+
imageRecord,
|
|
140
|
+
postWithImage: {
|
|
141
|
+
PostHeaderImages: [{ id: 42 }],
|
|
142
|
+
PostImages: [],
|
|
143
|
+
PostUserImages: [],
|
|
144
|
+
},
|
|
145
|
+
remainingAssociations: {
|
|
146
|
+
PostImages: [],
|
|
147
|
+
PostHeaderImages: [{ id: 42 }],
|
|
148
|
+
PostUserImages: [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const res = createResponse();
|
|
152
|
+
await Image.removeImageFromCollection({
|
|
153
|
+
params: {
|
|
154
|
+
imageId: "42",
|
|
155
|
+
postId: "10",
|
|
156
|
+
},
|
|
157
|
+
query: {},
|
|
158
|
+
}, res);
|
|
159
|
+
assert.deepEqual(post.removedHeaderImages, [42]);
|
|
160
|
+
assert.equal(imageRecord.deleted, false);
|
|
161
|
+
assert.equal(imageRecord.saveCallCount, 0);
|
|
162
|
+
assert.equal(res.statusCode, 200);
|
|
163
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
164
|
+
});
|
|
165
|
+
test("removeImageFromCollection still deletes unshared post images", async (t) => {
|
|
166
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
167
|
+
t.after(restore);
|
|
168
|
+
const imageRecord = {
|
|
169
|
+
id: 99,
|
|
170
|
+
deleted: false,
|
|
171
|
+
saveCallCount: 0,
|
|
172
|
+
async save() {
|
|
173
|
+
this.saveCallCount += 1;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const { Image, post } = setupImageModel({
|
|
177
|
+
createImageModel,
|
|
178
|
+
imageRecord,
|
|
179
|
+
postWithImage: {
|
|
180
|
+
PostHeaderImages: [],
|
|
181
|
+
PostImages: [{ id: 99 }],
|
|
182
|
+
PostUserImages: [],
|
|
183
|
+
},
|
|
184
|
+
remainingAssociations: {
|
|
185
|
+
PostImages: [],
|
|
186
|
+
PostHeaderImages: [],
|
|
187
|
+
PostUserImages: [],
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const res = createResponse();
|
|
191
|
+
await Image.removeImageFromCollection({
|
|
192
|
+
params: {
|
|
193
|
+
imageId: "99",
|
|
194
|
+
postId: "10",
|
|
195
|
+
},
|
|
196
|
+
query: {},
|
|
197
|
+
}, res);
|
|
198
|
+
assert.deepEqual(post.removedPostImages, [99]);
|
|
199
|
+
assert.equal(imageRecord.deleted, true);
|
|
200
|
+
assert.equal(imageRecord.saveCallCount, 1);
|
|
201
|
+
assert.equal(res.statusCode, 200);
|
|
202
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
203
|
+
});
|
|
204
|
+
test("removeByUserIdOnly detaches the current collection before preserving shared images", async (t) => {
|
|
205
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
206
|
+
t.after(restore);
|
|
207
|
+
const imageRecord = {
|
|
208
|
+
id: 77,
|
|
209
|
+
user_id: 850,
|
|
210
|
+
deleted: false,
|
|
211
|
+
saveCallCount: 0,
|
|
212
|
+
async save() {
|
|
213
|
+
this.saveCallCount += 1;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
const { Image, group } = setupImageModel({
|
|
217
|
+
createImageModel,
|
|
218
|
+
imageRecord,
|
|
219
|
+
groupWithImage: {
|
|
220
|
+
GroupLogoImages: [{ id: 77 }],
|
|
221
|
+
},
|
|
222
|
+
remainingAssociations: {
|
|
223
|
+
GroupLogoImages: [{ id: 77 }],
|
|
224
|
+
CommunityLogoImages: [],
|
|
225
|
+
},
|
|
226
|
+
associations: {
|
|
227
|
+
GroupLogoImages: { associationType: "BelongsToMany" },
|
|
228
|
+
CommunityLogoImages: { associationType: "BelongsToMany" },
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const res = createResponse();
|
|
232
|
+
await Image.removeImageFromCollection({
|
|
233
|
+
params: {
|
|
234
|
+
groupId: "55",
|
|
235
|
+
imageId: "77",
|
|
236
|
+
},
|
|
237
|
+
query: {
|
|
238
|
+
removeByUserIdOnly: "true",
|
|
239
|
+
},
|
|
240
|
+
user: {
|
|
241
|
+
id: 850,
|
|
242
|
+
},
|
|
243
|
+
}, res);
|
|
244
|
+
assert.deepEqual(group.removedLogoImages, [77]);
|
|
245
|
+
assert.equal(imageRecord.deleted, false);
|
|
246
|
+
assert.equal(imageRecord.saveCallCount, 0);
|
|
247
|
+
assert.equal(res.statusCode, 200);
|
|
248
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
249
|
+
});
|
|
250
|
+
test("removeByUserIdOnly directly deletes unattached uploads owned by the current user", async (t) => {
|
|
251
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
252
|
+
t.after(restore);
|
|
253
|
+
const imageRecord = {
|
|
254
|
+
id: 88,
|
|
255
|
+
user_id: 850,
|
|
256
|
+
deleted: false,
|
|
257
|
+
saveCallCount: 0,
|
|
258
|
+
async save() {
|
|
259
|
+
this.saveCallCount += 1;
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const { Image, group } = setupImageModel({
|
|
263
|
+
createImageModel,
|
|
264
|
+
imageRecord,
|
|
265
|
+
groupWithImage: null,
|
|
266
|
+
remainingAssociations: {
|
|
267
|
+
GroupLogoImages: [],
|
|
268
|
+
},
|
|
269
|
+
associations: {
|
|
270
|
+
GroupLogoImages: { associationType: "BelongsToMany" },
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
const res = createResponse();
|
|
274
|
+
await Image.removeImageFromCollection({
|
|
275
|
+
params: {
|
|
276
|
+
groupId: "55",
|
|
277
|
+
imageId: "88",
|
|
278
|
+
},
|
|
279
|
+
query: {
|
|
280
|
+
removeByUserIdOnly: "true",
|
|
281
|
+
},
|
|
282
|
+
user: {
|
|
283
|
+
id: 850,
|
|
284
|
+
},
|
|
285
|
+
}, res);
|
|
286
|
+
assert.deepEqual(group.removedLogoImages, []);
|
|
287
|
+
assert.equal(imageRecord.deleted, true);
|
|
288
|
+
assert.equal(imageRecord.saveCallCount, 1);
|
|
289
|
+
assert.equal(res.statusCode, 200);
|
|
290
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
291
|
+
});
|
|
292
|
+
test("removing the only post header image resets cover_media_type", async (t) => {
|
|
293
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
294
|
+
t.after(restore);
|
|
295
|
+
const imageRecord = {
|
|
296
|
+
id: 123,
|
|
297
|
+
deleted: false,
|
|
298
|
+
saveCallCount: 0,
|
|
299
|
+
async save() {
|
|
300
|
+
this.saveCallCount += 1;
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const { Image, post } = setupImageModel({
|
|
304
|
+
createImageModel,
|
|
305
|
+
imageRecord,
|
|
306
|
+
postWithImage: {
|
|
307
|
+
PostHeaderImages: [{ id: 123 }],
|
|
308
|
+
PostImages: [],
|
|
309
|
+
PostUserImages: [],
|
|
310
|
+
},
|
|
311
|
+
remainingAssociations: {
|
|
312
|
+
PostImages: [],
|
|
313
|
+
PostHeaderImages: [],
|
|
314
|
+
PostUserImages: [],
|
|
315
|
+
},
|
|
316
|
+
remainingPostHeaderImageCount: 0,
|
|
317
|
+
});
|
|
318
|
+
post.cover_media_type = "image";
|
|
319
|
+
const res = createResponse();
|
|
320
|
+
await Image.removeImageFromCollection({
|
|
321
|
+
params: {
|
|
322
|
+
imageId: "123",
|
|
323
|
+
postId: "10",
|
|
324
|
+
},
|
|
325
|
+
query: {},
|
|
326
|
+
}, res);
|
|
327
|
+
assert.deepEqual(post.removedHeaderImages, [123]);
|
|
328
|
+
assert.equal(post.cover_media_type, "none");
|
|
329
|
+
assert.equal(post.saveCallCount, 1);
|
|
330
|
+
assert.equal(res.statusCode, 200);
|
|
331
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
332
|
+
});
|
|
333
|
+
test("removing one post header image keeps cover_media_type when others remain", async (t) => {
|
|
334
|
+
const { createImageModel, restore } = loadImageModelFactory();
|
|
335
|
+
t.after(restore);
|
|
336
|
+
const imageRecord = {
|
|
337
|
+
id: 124,
|
|
338
|
+
deleted: false,
|
|
339
|
+
saveCallCount: 0,
|
|
340
|
+
async save() {
|
|
341
|
+
this.saveCallCount += 1;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
const { Image, post } = setupImageModel({
|
|
345
|
+
createImageModel,
|
|
346
|
+
imageRecord,
|
|
347
|
+
postWithImage: {
|
|
348
|
+
PostHeaderImages: [{ id: 124 }],
|
|
349
|
+
PostImages: [],
|
|
350
|
+
PostUserImages: [],
|
|
351
|
+
},
|
|
352
|
+
remainingAssociations: {
|
|
353
|
+
PostImages: [],
|
|
354
|
+
PostHeaderImages: [{ id: 999 }],
|
|
355
|
+
PostUserImages: [],
|
|
356
|
+
},
|
|
357
|
+
remainingPostHeaderImageCount: 1,
|
|
358
|
+
});
|
|
359
|
+
post.cover_media_type = "image";
|
|
360
|
+
const res = createResponse();
|
|
361
|
+
await Image.removeImageFromCollection({
|
|
362
|
+
params: {
|
|
363
|
+
imageId: "124",
|
|
364
|
+
postId: "10",
|
|
365
|
+
},
|
|
366
|
+
query: {},
|
|
367
|
+
}, res);
|
|
368
|
+
assert.deepEqual(post.removedHeaderImages, [124]);
|
|
369
|
+
assert.equal(post.cover_media_type, "image");
|
|
370
|
+
assert.equal(post.saveCallCount, 0);
|
|
371
|
+
assert.equal(res.statusCode, 200);
|
|
372
|
+
assert.deepEqual(res.body, { message: "Image removed from collection" });
|
|
373
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|