Skip to content

Commit

Permalink
Merge vin/create-flyer to main (#104)
Browse files Browse the repository at this point in the history
* Fixed notification issue

* Added flyer notification

* Added flyer notification

* Updated Organization and Flyer models

* Updated test cases

* Added getFlyersByCategorySlug query

* Update query description

* Inline notification calls

* Add createFlyer mutation

* `deleteFlyer` mutation (#103)

* Add deleteFlyer mutation

* Address PR comments

* Update utils.ts

* Update FlyerRepo.ts
  • Loading branch information
vinnie4k committed Sep 13, 2023
1 parent d49b504 commit b0976c5
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 3 deletions.
15 changes: 15 additions & 0 deletions src/middlewares/FlyerMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MiddlewareFn } from 'type-graphql';
import { Context } from 'vm';
import utils from '../utils';

const FlyerUploadErrorInterceptor: MiddlewareFn<Context> = async ({ args, context }, next) => {
try {
// Upload image to our upload service
context.imageURL = await utils.uploadImage(args.imageB64);
return await next();
} catch (err) {
throw new Error('An error occured while uploading the flyer image.');
}
};

export default { FlyerUploadErrorInterceptor };
63 changes: 62 additions & 1 deletion src/repos/FlyerRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MAX_NUM_OF_TRENDING_FLYERS,
} from '../common/constants';
import { OrganizationModel } from '../entities/Organization';
import utils from '../utils';

const { IS_FILTER_ACTIVE } = process.env;

Expand Down Expand Up @@ -223,7 +224,7 @@ const refreshTrendingFlyers = async (): Promise<Flyer[]> => {

/**
* Increments number of times clicked on a flyer by one.
* @function
*
* @param {string} id - string representing the unique Object Id of a flyer.
*/
const incrementTimesClicked = async (id: string): Promise<Flyer> => {
Expand All @@ -238,7 +239,67 @@ const incrementTimesClicked = async (id: string): Promise<Flyer> => {
return flyer;
};

/**
* Create a new flyer.
*
* @param {string} categorySlug the slug for this flyer's category
* @param {string} endDate the end date for this flyer's event in UTC ISO8601 format
* @param {string} flyerURL the URL for this flyer when tapped
* @param {string} imageURL the URL representing this flyer's image
* @param {string} location the location for this flyer's event
* @param {string} organizationID the ID of the organization creating this flyer
* @param {string} startDate the start date for this flyer's event in UTC ISO8601 format
* @param {string} title the title for this flyer
* @returns the newly created Flyer
*/
const createFlyer = async (
categorySlug: string,
endDate: string,
flyerURL: string,
imageURL: string,
location: string,
organizationID: string,
startDate: string,
title: string,
): Promise<Flyer> => {
// Fetch organization given organization ID
// This call will fail if the organization cannot be found
const organization = await OrganizationModel.findById(new ObjectId(organizationID));
const organizationSlug = organization.slug;

const newFlyer = Object.assign(new Flyer(), {
categorySlug,
endDate,
flyerURL,
imageURL,
location,
organization,
organizationSlug,
startDate,
title,
});
return FlyerModel.create(newFlyer);
};

/**
* Delete a flyer
*
* @param id the flyer ID to remove
*/
const deleteFlyer = async (id: string): Promise<Flyer> => {
const flyerToRemove = await FlyerModel.findById(new ObjectId(id));
if (!flyerToRemove) {
return null;
}
// Remove image from our servers
await utils.removeImage(flyerToRemove.imageURL);
const flyer = await FlyerModel.findByIdAndDelete(new ObjectId(id));
return flyer;
};

export default {
createFlyer,
deleteFlyer,
getAllFlyers,
getFlyerByID,
getFlyersAfterDate,
Expand Down
51 changes: 49 additions & 2 deletions src/resolvers/FlyerResolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { Resolver, Mutation, Arg, Query, FieldResolver, Root } from 'type-graphql';
import {
Resolver,
Mutation,
Arg,
Query,
FieldResolver,
Root,
UseMiddleware,
Ctx,
} from 'type-graphql';
import { Context } from 'vm';
import { Flyer } from '../entities/Flyer';
import FlyerRepo from '../repos/FlyerRepo';
import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '../common/constants';
import FlyerRepo from '../repos/FlyerRepo';
import FlyerMiddleware from '../middlewares/FlyerMiddleware';

@Resolver((_of) => Flyer)
class FlyerResolver {
Expand Down Expand Up @@ -155,6 +166,42 @@ class FlyerResolver {
async incrementTimesClicked(@Arg('id') id: string) {
return FlyerRepo.incrementTimesClicked(id);
}

@Mutation((_returns) => Flyer, {
description: `Creates a single <Flyer> via given <categorySlug>, <endDate>, <flyerURL>, <imageB64>, <location>, <organizationID>, <startDate>, and <title>.
<startDate> and <endDate> must be in UTC ISO8601 format (e.g. YYYY-mm-ddTHH:MM:ssZ).
<imageB64> must be a Base64 encrypted string without 'data:image/png;base64,' prepended`,
})
@UseMiddleware(FlyerMiddleware.FlyerUploadErrorInterceptor)
async createFlyer(
@Arg('categorySlug') categorySlug: string,
@Arg('endDate') endDate: string,
@Arg('flyerURL', { nullable: true }) flyerURL: string,
@Arg('imageB64') imageB64: string,
@Arg('location') location: string,
@Arg('organizationID') organizationID: string,
@Arg('startDate') startDate: string,
@Arg('title') title: string,
@Ctx() ctx: Context,
) {
return FlyerRepo.createFlyer(
categorySlug,
endDate,
flyerURL,
ctx.imageURL,
location,
organizationID,
startDate,
title,
);
}

@Mutation((_returns) => Flyer, {
description: `Delete a flyer with the id <id>.`,
})
async deleteFlyer(@Arg('id') id: string) {
return FlyerRepo.deleteFlyer(id);
}
}

export default FlyerResolver;
20 changes: 20 additions & 0 deletions src/tests/flyer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,23 @@ describe('getFlyersByCategorySlug tests', () => {
expect(getFlyersResponse).toHaveLength(limit);
});
});

describe('deleteFlyer tests', () => {
test('flyer with ID exists', async () => {
const flyers = await FlyerFactory.create(2);
await FlyerModel.insertMany(flyers);

const fetchedFlyers = await FlyerRepo.getAllFlyers();

const deleteFlyerResponse = await FlyerRepo.deleteFlyer(fetchedFlyers[0].id);
expect(deleteFlyerResponse.id).toStrictEqual(fetchedFlyers[0].id);
});

test('flyer with ID does not exist', async () => {
const flyers = await FlyerFactory.create(2);
await FlyerModel.insertMany(flyers);

const deleteFlyerResponse = await FlyerRepo.deleteFlyer('64811792f910705ca1a981f8');
expect(deleteFlyerResponse).toBeNull();
});
});
53 changes: 53 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fetch from 'node-fetch-commonjs';

interface UploadResponse {
success: boolean;
data: string;
}

/**
* Upload an image to our servers
*
* @param imageB64 the base64 string to upload
* @returns the URl representing the image
*/
const uploadImage = async (imageB64: string): Promise<string> => {
// Upload image via a POST request
const imagePayload = {
bucket: process.env.UPLOAD_BUCKET,
image: `data:image/png;base64,${imageB64}`,
};
const response = await fetch(`${process.env.UPLOAD_SERVICE_URL}/upload/`, {
method: 'POST',
body: JSON.stringify(imagePayload),
});

const responseData = (await response.json()) as UploadResponse;
return responseData.data;
};

/**
* Remove an image from AppDev servers
*
* @param imageURL the image URL to remove
* @returns true if the deletion was successful; otherwise false
*/
const removeImage = async (imageURL: string): Promise<boolean> => {
// Delete image via a POST request
const payload = {
bucket: process.env.UPLOAD_BUCKET,
image_url: imageURL,
};
const response = await fetch(`${process.env.UPLOAD_SERVICE_URL}/remove/`, {
method: 'POST',
body: JSON.stringify(payload),
});

const responseData = (await response.json()) as UploadResponse;
if (!responseData.success) {
console.log(`Removing an image from our servers failed with image URL: ${imageURL}`);
}
return responseData.success;
};

export default { removeImage, uploadImage };

0 comments on commit b0976c5

Please sign in to comment.