Skip to content

Commit 3e874ae

Browse files
authored
Merge pull request #87 from StreetSupport/refactor/simplify-banner-schema
refactor: simplify banner schema to single flexible type
2 parents 6dd3126 + 9304525 commit 3e874ae

File tree

9 files changed

+209
-492
lines changed

9 files changed

+209
-492
lines changed

src/controllers/bannerController.ts

Lines changed: 49 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response } from 'express';
22
import { asyncHandler } from '../utils/asyncHandler.js';
3-
import { BackgroundType, BannerTemplateType } from '../types/index.js';
3+
import { BackgroundType } from '../types/index.js';
44
import { validateBanner } from '../schemas/bannerSchema.js';
55
import Banner from '../models/bannerModel.js';
66
import { deleteFile } from '../middleware/uploadMiddleware.js';
@@ -9,23 +9,22 @@ import { sendSuccess, sendCreated, sendBadRequest, sendNotFound, sendPaginatedSu
99

1010
// Get all banners with optional filtering
1111
export const getBanners = asyncHandler(async (req: Request, res: Response) => {
12-
const {
12+
const {
1313
location,
14-
locations, // New: comma-separated list of locations for CityAdmin filtering
15-
templateType,
16-
isActive,
14+
locations,
15+
isActive,
1716
search,
18-
page = 1,
17+
page = 1,
1918
limit = 9,
2019
sortBy = 'Priority',
2120
sortOrder = 'asc'
2221
} = req.query;
2322

2423
const query: any = {};
25-
24+
2625
// Apply search filter
2726
if (search && typeof search === 'string') {
28-
const searchRegex = new RegExp(search, 'i'); // Case-insensitive search
27+
const searchRegex = new RegExp(search, 'i');
2928
query.$or = [
3029
{ Title: searchRegex },
3130
{ Description: searchRegex },
@@ -34,9 +33,7 @@ export const getBanners = asyncHandler(async (req: Request, res: Response) => {
3433
}
3534

3635
// Apply location filters
37-
// Priority: 'locations' (for CityAdmin bulk filtering) over 'location' (for single filter)
3836
if (locations && typeof locations === 'string') {
39-
// Multiple locations passed from admin side for CityAdmin users
4037
const locationArray = locations.split(',').map(loc => loc.trim()).filter(Boolean);
4138
if (locationArray.length > 0) {
4239
const locationQuery = {
@@ -46,44 +43,37 @@ export const getBanners = asyncHandler(async (req: Request, res: Response) => {
4643
{ LocationSlug: null }
4744
]
4845
};
49-
50-
// Combine with search query if it exists
46+
5147
if (query.$or) {
5248
query.$and = [
53-
{ $or: query.$or }, // Search conditions
54-
locationQuery // Location conditions
49+
{ $or: query.$or },
50+
locationQuery
5551
];
5652
delete query.$or;
5753
} else {
5854
query.$or = locationQuery.$or;
5955
}
6056
}
6157
} else if (location && typeof location === 'string') {
62-
// Single location filter from UI
6358
const locationQuery = {
6459
$or: [
6560
{ LocationSlug: location },
6661
{ LocationSlug: { $exists: false } },
6762
{ LocationSlug: null }
6863
]
6964
};
70-
71-
// Combine with search query if it exists
65+
7266
if (query.$or) {
7367
query.$and = [
74-
{ $or: query.$or }, // Search conditions
75-
locationQuery // Location conditions
68+
{ $or: query.$or },
69+
locationQuery
7670
];
7771
delete query.$or;
7872
} else {
7973
query.$or = locationQuery.$or;
8074
}
8175
}
82-
83-
if (templateType) {
84-
query.TemplateType = templateType;
85-
}
86-
76+
8777
if (isActive !== undefined) {
8878
query.IsActive = isActive === 'true';
8979
}
@@ -130,19 +120,16 @@ export const createBanner = asyncHandler(async (req: Request, res: Response) =>
130120

131121
// Validate and transform banner data using Zod (final validation after upload)
132122
const validation = validateBanner(processedData);
133-
123+
134124
if (!validation.success) {
135125
// Clean up any uploaded files since validation failed
136126
await cleanupUploadedFiles(processedData);
137127
const errorMessages = validation.errors.map(err => err.message).join(', ');
138128
return sendBadRequest(res, `Validation failed: ${errorMessages}`);
139129
}
140130

141-
// Handle resource project specific logic
142-
let finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
143-
144131
// Handle background image specific logic
145-
finalBannerData = handleBackgroundImageLogic(finalBannerData);
132+
const finalBannerData = handleBackgroundImageLogic({ ...validation.data });
146133

147134
// Add creator information and system fields
148135
const bannerData = {
@@ -161,9 +148,9 @@ export const createBanner = asyncHandler(async (req: Request, res: Response) =>
161148
// Update banner
162149
export const updateBanner = asyncHandler(async (req: Request, res: Response) => {
163150
const { id } = req.params;
164-
151+
165152
const banner = await Banner.findById(id);
166-
153+
167154
if (!banner) {
168155
return sendNotFound(res, 'Banner not found');
169156
}
@@ -173,7 +160,7 @@ export const updateBanner = asyncHandler(async (req: Request, res: Response) =>
173160

174161
// Validate and transform banner data using Zod (final validation after upload)
175162
const validation = validateBanner(processedData);
176-
163+
177164
if (!validation.success) {
178165
// Clean up any newly uploaded files since validation failed
179166
await cleanupUploadedFiles(processedData);
@@ -184,17 +171,8 @@ export const updateBanner = asyncHandler(async (req: Request, res: Response) =>
184171
// Store old banner data for file cleanup
185172
const oldBannerData = banner.toObject();
186173

187-
// Handle template type change from RESOURCE_PROJECT to another type
188-
if (oldBannerData.TemplateType === BannerTemplateType.RESOURCE_PROJECT &&
189-
validation?.data?.TemplateType !== BannerTemplateType.RESOURCE_PROJECT) {
190-
await handleResourceProjectTemplateChange(oldBannerData);
191-
}
192-
193-
// Handle resource project specific logic
194-
let finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
195-
196174
// Handle background image specific logic
197-
finalBannerData = handleBackgroundImageLogic(finalBannerData);
175+
const finalBannerData = handleBackgroundImageLogic({ ...validation.data });
198176

199177
// Preserve existing activation date fields and IsActive (not editable in edit form)
200178
finalBannerData.StartDate = banner.StartDate;
@@ -312,49 +290,6 @@ export const toggleBannerStatus = asyncHandler(async (req: Request, res: Respons
312290
return sendSuccess(res, updatedBanner, `Banner ${updatedBanner?.IsActive ? 'activated' : 'deactivated'} successfully`);
313291
});
314292

315-
// Private helper to handle resource project specific logic
316-
function _handleResourceProjectBannerLogic(bannerData: any): any {
317-
if (bannerData.TemplateType === BannerTemplateType.RESOURCE_PROJECT && bannerData.ResourceProject?.ResourceFile) {
318-
const resourceFile = bannerData.ResourceProject.ResourceFile;
319-
320-
// If a new file was uploaded, its URL is in `Url`. We make this the permanent `FileUrl`.
321-
if (resourceFile.Url && !resourceFile.FileUrl) {
322-
bannerData.ResourceProject.ResourceFile.FileUrl = resourceFile.Url;
323-
}
324-
325-
// Update the 'Download' CTA button URL to use the file URL
326-
const fileUrl = bannerData.ResourceProject.ResourceFile.FileUrl;
327-
if (bannerData.CtaButtons && bannerData.CtaButtons.length > 0 && fileUrl) {
328-
const downloadButtonIndex = 0;
329-
const button = bannerData.CtaButtons[downloadButtonIndex];
330-
if (button) {
331-
button.Url = fileUrl;
332-
}
333-
}
334-
}
335-
return bannerData;
336-
}
337-
338-
// Private helper to handle template type change from RESOURCE_PROJECT
339-
// Cleans up resource file and CTA button with blob URL when template type changes
340-
async function handleResourceProjectTemplateChange(oldBannerData: any): Promise<void> {
341-
// Check if old banner had a resource file with a blob URL
342-
if (oldBannerData.ResourceProject?.ResourceFile?.FileUrl) {
343-
const fileUrl = oldBannerData.ResourceProject.ResourceFile.FileUrl;
344-
345-
// Delete the resource file from blob storage if it's a blob URL
346-
if (fileUrl.includes('blob.core.windows.net')) {
347-
try {
348-
await deleteFile(fileUrl);
349-
console.log(`Cleaned up resource file during template type change: ${fileUrl}`);
350-
} catch (error) {
351-
console.error(`Failed to delete resource file ${fileUrl} during template type change:`, error);
352-
// Don't throw error - file cleanup failure shouldn't break the update
353-
}
354-
}
355-
}
356-
}
357-
358293
// Private helper to handle background image logic
359294
// Automatically populates Background.Value with BackgroundImage URL when Background.Type is 'image'
360295
function handleBackgroundImageLogic(bannerData: any): any {
@@ -381,113 +316,65 @@ function handleBackgroundImageLogic(bannerData: any): any {
381316
// Helper function to extract all file URLs from a banner
382317
function extractFileUrls(banner: any): string[] {
383318
const urls: string[] = [];
384-
319+
385320
// Main media assets
386321
if (banner.Logo?.Url) urls.push(banner.Logo.Url);
387322
if (banner.BackgroundImage?.Url) urls.push(banner.BackgroundImage.Url);
388323
if (banner.MainImage?.Url) urls.push(banner.MainImage.Url);
389-
390-
// Partner logos for partnership charter banners (nested structure)
391-
if (banner.PartnershipCharter?.PartnerLogos && Array.isArray(banner.PartnershipCharter.PartnerLogos)) {
392-
banner.PartnershipCharter.PartnerLogos.forEach((logo: any) => {
393-
if (logo.Url) urls.push(logo.Url);
394-
});
395-
}
396-
397-
// Resource files for resource project banners (nested structure)
398-
if (banner.ResourceProject?.ResourceFile && banner.ResourceProject.ResourceFile.FileUrl) {
399-
urls.push(banner.ResourceProject.ResourceFile.FileUrl);
400-
}
401-
324+
325+
// Uploaded file (PDFs, images, etc.)
326+
if (banner.UploadedFile?.FileUrl) urls.push(banner.UploadedFile.FileUrl);
327+
402328
return urls;
403329
}
404330

405331
// Helper function to process mixed media fields (existing assets + new files)
406332
function processMediaFields(req: Request): any {
407-
// Start with the clean, pre-validated data and merge the raw body to get file info
408-
// Note: I still not sure if it makes sense to use this merging instead of req.body
409333
const processedData = { ...req.body, ...req.preValidatedData };
410-
411-
['Logo', 'BackgroundImage', 'MainImage' /*, 'AccentGraphic'*/].forEach(field => {
334+
335+
// Process standard media asset fields (Logo, BackgroundImage, MainImage)
336+
['Logo', 'BackgroundImage', 'MainImage'].forEach(field => {
412337
const newFileData = processedData[`newfile_${field}`];
413-
const newMetadata = processedData[`newmetadata_${field}`]
414-
? JSON.parse(processedData[`newmetadata_${field}`])
338+
const newMetadata = processedData[`newmetadata_${field}`]
339+
? JSON.parse(processedData[`newmetadata_${field}`])
415340
: null;
416-
const existingMetadata = processedData[`existing_${field}`]
417-
? JSON.parse(processedData[`existing_${field}`])
341+
const existingMetadata = processedData[`existing_${field}`]
342+
? JSON.parse(processedData[`existing_${field}`])
418343
: null;
419344

420345
let finalAsset = null;
421346
if (newFileData) {
422-
// New file uploaded, merge with its metadata
423347
finalAsset = {
424-
...(newMetadata || {}), // Contains Position, Opacity, etc.
425-
...newFileData // Contains Url, Filename, Size from upload
348+
...(newMetadata || {}),
349+
...newFileData
426350
};
427351
} else if (existingMetadata) {
428-
// No new file, use existing metadata
429352
finalAsset = existingMetadata;
430353
}
431354

432-
// If finalAsset is null (removed by user), set to undefined to satisfy Zod's optional schema
433-
// Otherwise, assign the processed asset object.
434-
processedData[field] = finalAsset;// === null ? undefined : finalAsset;
355+
processedData[field] = finalAsset;
435356
});
436357

437-
438-
439-
// Process PartnerLogos array field
440-
const existingPartnerLogos = processedData.existing_PartnerLogos
441-
? JSON.parse(processedData.existing_PartnerLogos)
442-
: [];
443-
const newPartnerLogos = processedData.newfile_PartnerLogos || [];
444-
const combinedPartnerLogos = [
445-
...existingPartnerLogos,
446-
...(Array.isArray(newPartnerLogos) ? newPartnerLogos : [newPartnerLogos])
447-
].filter(Boolean);
448-
449-
if (processedData.PartnershipCharter) {
450-
const partnershipCharter = typeof processedData.PartnershipCharter === 'string'
451-
? JSON.parse(processedData.PartnershipCharter)
452-
: processedData.PartnershipCharter;
453-
454-
partnershipCharter.PartnerLogos = combinedPartnerLogos;
455-
processedData.PartnershipCharter = partnershipCharter;
456-
} else if (combinedPartnerLogos.length > 0) {
457-
processedData.PartnershipCharter = { PartnerLogos: combinedPartnerLogos };
458-
}
459-
460-
461-
462-
// Process ResourceFile
463-
const newResourceFileData = processedData.newfile_ResourceFile;
464-
const newResourceFileMetadata = processedData.newmetadata_ResourceFile
465-
? JSON.parse(processedData.newmetadata_ResourceFile)
358+
// Process UploadedFile (general file upload - PDFs, images, etc.)
359+
const newUploadedFileData = processedData.newfile_UploadedFile;
360+
const newUploadedFileMetadata = processedData.newmetadata_UploadedFile
361+
? JSON.parse(processedData.newmetadata_UploadedFile)
466362
: null;
467-
const existingResourceFile = processedData.existing_ResourceFile
468-
? JSON.parse(processedData.existing_ResourceFile)
363+
const existingUploadedFile = processedData.existing_UploadedFile
364+
? JSON.parse(processedData.existing_UploadedFile)
469365
: null;
470366

471-
let finalResourceFile = null;
472-
if (newResourceFileData) {
473-
finalResourceFile = {
474-
...(newResourceFileMetadata || {}),
475-
...newResourceFileData
367+
let finalUploadedFile = null;
368+
if (newUploadedFileData) {
369+
finalUploadedFile = {
370+
...(newUploadedFileMetadata || {}),
371+
...newUploadedFileData
476372
};
477-
} else if (existingResourceFile) {
478-
finalResourceFile = existingResourceFile;
373+
} else if (existingUploadedFile) {
374+
finalUploadedFile = existingUploadedFile;
479375
}
480376

481-
if (processedData.ResourceProject) {
482-
const resourceProject = typeof processedData.ResourceProject === 'string'
483-
? JSON.parse(processedData.ResourceProject)
484-
: processedData.ResourceProject;
485-
486-
resourceProject.ResourceFile = finalResourceFile;
487-
processedData.ResourceProject = resourceProject;
488-
} else if (finalResourceFile) {
489-
processedData.ResourceProject = { ResourceFile: finalResourceFile };
490-
}
377+
processedData.UploadedFile = finalUploadedFile;
491378

492379
// Clean up all temporary form data keys before validation
493380
Object.keys(processedData).forEach(key => {

0 commit comments

Comments
 (0)