Skip to content

Commit 3973d69

Browse files
Newsfeed management Updated and required changes (PalisadoesFoundation#1383)
* implementation of the newsfeed feature * implementation of the newsfeed feature
1 parent 810457b commit 3973d69

26 files changed

+414
-380
lines changed

schema.graphql

+3-2
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ type Post {
623623
likeCount: Int
624624
likedBy: [User]
625625
organization: Organization!
626+
pinned: Boolean
626627
text: String!
627628
title: String
628629
videoUrl: URL
@@ -669,10 +670,10 @@ enum PostOrderByInput {
669670
}
670671

671672
input PostUpdateInput {
672-
imageUrl: URL
673+
imageUrl: String
673674
text: String
674675
title: String
675-
videoUrl: URL
676+
videoUrl: String
676677
}
677678

678679
input PostWhereInput {

src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ app.use(
6868
);
6969

7070
app.use("/images", express.static(path.join(__dirname, "./../images")));
71+
app.use("/videos", express.static(path.join(__dirname, "./../videos")));
72+
7173
app.use(requestContext.middleware());
7274

7375
if (process.env.NODE_ENV !== "production")

src/models/EncodedVideo.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Types, Model } from "mongoose";
2+
import { Schema, model, models } from "mongoose";
3+
/**
4+
* This is an interface that represents a database(MongoDB) document for Encoded Video.
5+
*/
6+
export interface InterfaceEncodedVideo {
7+
_id: Types.ObjectId;
8+
fileName: string;
9+
content: string;
10+
numberOfUses: number;
11+
}
12+
/**
13+
* This describes the schema for a `encodedVideo` that corresponds to `InterfaceEncodedVideo` document.
14+
* @param fileName - File name.
15+
* @param content - Content.
16+
* @param numberOfUses - Number of Uses.
17+
*/
18+
const encodedVideoSchema = new Schema({
19+
fileName: {
20+
type: String,
21+
required: true,
22+
},
23+
content: {
24+
type: String,
25+
required: true,
26+
},
27+
numberOfUses: {
28+
type: Number,
29+
required: true,
30+
default: 1,
31+
},
32+
});
33+
34+
const encodedVideoModel = (): Model<InterfaceEncodedVideo> =>
35+
model<InterfaceEncodedVideo>("EncodedVideo", encodedVideoSchema);
36+
37+
// This syntax is needed to prevent Mongoose OverwriteModelError while running tests.
38+
export const EncodedVideo = (models.EncodedVideo ||
39+
encodedVideoModel()) as ReturnType<typeof encodedVideoModel>;

src/models/Post.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface InterfacePost {
1313
status: string;
1414
createdAt: Date;
1515
imageUrl: string | undefined | null;
16-
videoUrl: string | undefined;
16+
videoUrl: string | undefined | null;
1717
creator: PopulatedDoc<InterfaceUser & Document>;
1818
organization: PopulatedDoc<InterfaceOrganization & Document>;
1919
likedBy: PopulatedDoc<InterfaceUser & Document>[];

src/resolvers/Mutation/createPost.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
} from "../../constants";
1010
import { isValidString } from "../../libraries/validators/validateString";
1111
import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage";
12+
import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo";
1213
import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache";
1314
import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations";
15+
1416
/**
1517
* This function enables to create a post.
1618
* @param _parent - parent of current request
@@ -65,10 +67,18 @@ export const createPost: MutationResolvers["createPost"] = async (
6567
);
6668
}
6769

68-
let uploadImageFileName;
70+
let uploadImageFileName = null;
71+
let uploadVideoFileName = null;
6972

7073
if (args.file) {
71-
uploadImageFileName = await uploadEncodedImage(args.file, null);
74+
const dataUrlPrefix = "data:";
75+
if (args.file.startsWith(dataUrlPrefix + "image/")) {
76+
uploadImageFileName = await uploadEncodedImage(args.file, null);
77+
} else if (args.file.startsWith(dataUrlPrefix + "video/")) {
78+
uploadVideoFileName = await uploadEncodedVideo(args.file, null);
79+
} else {
80+
throw new Error("Unsupported file type.");
81+
}
7282
}
7383

7484
// Checks if the recieved arguments are valid according to standard input norms
@@ -118,7 +128,8 @@ export const createPost: MutationResolvers["createPost"] = async (
118128
pinned: args.data.pinned ? true : false,
119129
creator: context.userId,
120130
organization: args.data.organizationId,
121-
imageUrl: args.file ? uploadImageFileName : null,
131+
imageUrl: uploadImageFileName,
132+
videoUrl: uploadVideoFileName,
122133
});
123134

124135
if (args.data.pinned) {
@@ -141,8 +152,11 @@ export const createPost: MutationResolvers["createPost"] = async (
141152
// Returns createdPost.
142153
return {
143154
...createdPost.toObject(),
144-
imageUrl: createdPost.imageUrl
155+
imageUrl: uploadImageFileName
145156
? `${context.apiRootUrl}${uploadImageFileName}`
146157
: null,
158+
videoUrl: uploadVideoFileName
159+
? `${context.apiRootUrl}${uploadVideoFileName}`
160+
: null,
147161
};
148162
};

src/resolvers/Mutation/updatePost.ts

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
LENGTH_VALIDATION_ERROR,
99
} from "../../constants";
1010
import { isValidString } from "../../libraries/validators/validateString";
11+
import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage";
12+
import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo";
1113

1214
export const updatePost: MutationResolvers["updatePost"] = async (
1315
_parent,
@@ -51,6 +53,20 @@ export const updatePost: MutationResolvers["updatePost"] = async (
5153
);
5254
}
5355

56+
if (args.data?.imageUrl && args.data?.imageUrl !== null) {
57+
args.data.imageUrl = await uploadEncodedImage(
58+
args.data.imageUrl,
59+
post.imageUrl
60+
);
61+
}
62+
63+
if (args.data?.videoUrl && args.data?.videoUrl !== null) {
64+
args.data.videoUrl = await uploadEncodedVideo(
65+
args.data.videoUrl,
66+
post.videoUrl
67+
);
68+
}
69+
5470
// Checks if the recieved arguments are valid according to standard input norms
5571
const validationResultTitle = isValidString(args.data?.title ?? "", 256);
5672
const validationResultText = isValidString(args.data?.text ?? "", 500);

src/resolvers/Query/postsByOrganization.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const postsByOrganization: QueryResolvers["postsByOrganization"] =
2525
const postsWithImageURLResolved = postsInOrg.map((post) => ({
2626
...post,
2727
imageUrl: post.imageUrl ? `${context.apiRootUrl}${post.imageUrl}` : null,
28+
videoUrl: post.videoUrl ? `${context.apiRootUrl}${post.videoUrl}` : null,
2829
}));
2930

3031
return postsWithImageURLResolved;

src/resolvers/Query/postsByOrganizationConnection.ts

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export const postsByOrganizationConnection: QueryResolvers["postsByOrganizationC
5757
? `${context.apiRootUrl}${post.imageUrl}`
5858
: null;
5959

60+
post.videoUrl = post.videoUrl
61+
? `${context.apiRootUrl}${post.videoUrl}`
62+
: null;
6063
return post;
6164
});
6265

src/typeDefs/inputs.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ export const inputs = gql`
375375
input PostUpdateInput {
376376
text: String
377377
title: String
378-
imageUrl: URL
379-
videoUrl: URL
378+
imageUrl: String
379+
videoUrl: String
380380
}
381381
`;

src/typeDefs/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export const types = gql`
303303
comments: [Comment]
304304
likeCount: Int
305305
commentCount: Int
306+
pinned: Boolean
306307
}
307308
308309
"""

src/types/generatedGraphQLTypes.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,7 @@ export type Post = {
11301130
likeCount?: Maybe<Scalars['Int']>;
11311131
likedBy?: Maybe<Array<Maybe<User>>>;
11321132
organization: Organization;
1133+
pinned?: Maybe<Scalars['Boolean']>;
11331134
text: Scalars['String'];
11341135
title?: Maybe<Scalars['String']>;
11351136
videoUrl?: Maybe<Scalars['URL']>;
@@ -1174,10 +1175,10 @@ export type PostOrderByInput =
11741175
| 'videoUrl_DESC';
11751176

11761177
export type PostUpdateInput = {
1177-
imageUrl?: InputMaybe<Scalars['URL']>;
1178+
imageUrl?: InputMaybe<Scalars['String']>;
11781179
text?: InputMaybe<Scalars['String']>;
11791180
title?: InputMaybe<Scalars['String']>;
1180-
videoUrl?: InputMaybe<Scalars['URL']>;
1181+
videoUrl?: InputMaybe<Scalars['String']>;
11811182
};
11821183

11831184
export type PostWhereInput = {
@@ -2473,6 +2474,7 @@ export type PostResolvers<ContextType = any, ParentType extends ResolversParentT
24732474
likeCount?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
24742475
likedBy?: Resolver<Maybe<Array<Maybe<ResolversTypes['User']>>>, ParentType, ContextType>;
24752476
organization?: Resolver<ResolversTypes['Organization'], ParentType, ContextType>;
2477+
pinned?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
24762478
text?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
24772479
title?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
24782480
videoUrl?: Resolver<Maybe<ResolversTypes['URL']>, ParentType, ContextType>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { unlink } from "fs/promises";
2+
import path from "path";
3+
import { EncodedVideo } from "../../models/EncodedVideo";
4+
5+
export const deletePreviousVideo = async (
6+
videoToBeDeletedPath: string
7+
): Promise<void> => {
8+
const videoToBeDeleted = await EncodedVideo.findOne({
9+
fileName: videoToBeDeletedPath!,
10+
});
11+
12+
if (videoToBeDeleted?.numberOfUses === 1) {
13+
await unlink(path.join(__dirname, "../../../" + videoToBeDeleted.fileName));
14+
await EncodedVideo.deleteOne({
15+
fileName: videoToBeDeletedPath,
16+
});
17+
}
18+
19+
await EncodedVideo.findOneAndUpdate(
20+
{
21+
fileName: videoToBeDeletedPath,
22+
},
23+
{
24+
$inc: {
25+
numberOfUses: -1,
26+
},
27+
}
28+
);
29+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const encodedVideoExtentionCheck = (encodedUrl: string): boolean => {
2+
const extension = encodedUrl.substring(
3+
"data:".length,
4+
encodedUrl.indexOf(";base64")
5+
);
6+
7+
console.log(extension);
8+
9+
const isValidVideo = extension === "video/mp4";
10+
if (isValidVideo) {
11+
return true;
12+
}
13+
14+
return false;
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import shortid from "shortid";
2+
import * as fs from "fs";
3+
import { writeFile } from "fs/promises";
4+
import { encodedVideoExtentionCheck } from "./encodedVideoExtensionCheck";
5+
import { errors, requestContext } from "../../libraries";
6+
import { INVALID_FILE_TYPE } from "../../constants";
7+
import { EncodedVideo } from "../../models/EncodedVideo";
8+
import path from "path";
9+
import { deletePreviousVideo } from "./deletePreviousVideo";
10+
11+
export const uploadEncodedVideo = async (
12+
encodedVideoURL: string,
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
previousVideoPath?: string | null
15+
): Promise<string> => {
16+
const isURLValidVideo = encodedVideoExtentionCheck(encodedVideoURL);
17+
18+
if (!isURLValidVideo) {
19+
throw new errors.InvalidFileTypeError(
20+
requestContext.translate(INVALID_FILE_TYPE.MESSAGE),
21+
INVALID_FILE_TYPE.CODE,
22+
INVALID_FILE_TYPE.PARAM
23+
);
24+
}
25+
26+
const encodedVideoAlreadyExist = await EncodedVideo.findOne({
27+
content: encodedVideoURL,
28+
});
29+
30+
if (previousVideoPath) {
31+
await deletePreviousVideo(previousVideoPath);
32+
}
33+
34+
if (encodedVideoAlreadyExist) {
35+
await EncodedVideo.findOneAndUpdate(
36+
{
37+
content: encodedVideoURL,
38+
},
39+
{
40+
$inc: {
41+
numberOfUses: 1,
42+
},
43+
}
44+
);
45+
return encodedVideoAlreadyExist.fileName;
46+
}
47+
48+
let id = shortid.generate();
49+
50+
id = "videos/" + id + "video.mp4";
51+
52+
const uploadedEncodedVideo = await EncodedVideo.create({
53+
fileName: id,
54+
content: encodedVideoURL,
55+
});
56+
57+
const data = encodedVideoURL.replace(/^data:video\/\w+;base64,/, "");
58+
59+
const buf = Buffer.from(data, "base64");
60+
61+
if (!fs.existsSync(path.join(__dirname, "../../../videos"))) {
62+
fs.mkdir(path.join(__dirname, "../../../videos"), (error) => {
63+
if (error) {
64+
throw error;
65+
}
66+
});
67+
}
68+
69+
await writeFile(path.join(__dirname, "../../../" + id), buf);
70+
71+
return uploadedEncodedVideo.fileName;
72+
};

0 commit comments

Comments
 (0)