Skip to content

Commit

Permalink
feat: contribution file auth (#7)
Browse files Browse the repository at this point in the history
* build: added zod and essential dependencies

* build: updated configuration

* feat: added data validation feature

* feat: added file authentication and updated data fetching

* feat: updated API and front-end accordingly

* fix: cleaned up
  • Loading branch information
sboy99 committed Jan 11, 2023
1 parent 697ac1b commit d4d2ca6
Show file tree
Hide file tree
Showing 13 changed files with 582 additions and 81 deletions.
87 changes: 74 additions & 13 deletions lib/contributors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import octokit from '&/config/octokit';
import { TCont, TContributor } from '&validation/contributor.validation';
import axios from 'config/axios';
import { GetStaticPropsContext } from 'next';
// type TFetchAllContributors = (limit: number) => Promise<TCont[]>;
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
import { remark } from 'remark';
import remarkHtml from 'remark-html';

export type Contribution = {
content: string;
meta: {
[key: string]: string | number;
};
};

const fileDir = path.join(process.cwd() + '/contribution');

function asserHasPropertyArray(
value: unknown
Expand All @@ -15,34 +29,81 @@ function asserHasPropertyArray(
}
}

export const fetchAllContributors = async (limit: number) => {
console.log(limit);

export const fetchAllContributors = async (
limit: number
): Promise<{ count: number; contributors: (TCont & { _id: string })[] }> => {
try {
const { data } = await axios.get(
`/contributors?select=avatar_url,gh_username,name,occupation,createdAt,_id&limit=${limit}`
`/contributors?select=avatar_url,gh_username,name,occupation,bio,createdAt,_id&limit=${limit}`
);
return data;
} catch (error) {
throw new Error(`Something went wrong`);
}
};

export const fetchSingleContributor = async ({
params,
}: GetStaticPropsContext) => {
export const fetchSingleContributor = async (
contId: string,
contribution: Contribution
): Promise<Omit<TContributor, 'isDeleted'>> => {
// authentication & automatically throws error if no file found
const file = fs.readFileSync(path.join(fileDir + `/${contId}.mdx`), 'utf8');
const contFile = matter(file);
const userName = contFile.data.github_username;
// searching for the user if user is not found then create user
try {
const { data } = await axios.get(`/contributors/${params!.contId}`);
return data;
const {
// eslint-disable-next-line no-unused-vars
data: { isDeleted, ...contributor },
} = await axios.get(`/contributors/${userName}`);

return contributor;
// if not found
} catch (error) {
throw new Error(`Something went wrong`);
// fetching github user
const { data: gh_user } = await octokit.request('GET /users/{username}', {
username: userName,
});
// creating payload for new contributor
const contPayload: Omit<TContributor, 'isDeleted' | 'profile_views'> = {
avatar_url: gh_user.avatar_url,
bio: gh_user.bio,
content: contribution.content,
email: gh_user.email,
gh_username: gh_user.login,
ghid: gh_user.id,
html_url: gh_user.html_url,
name: contribution.meta.author as string,
occupation: contribution.meta.occupation as string,
location: gh_user.location,
};

const {
// eslint-disable-next-line no-unused-vars
data: { isDeleted, ...contributor },
} = await axios.post(`/contributors`, contPayload);
return contributor;
}
};

export const getContribution = async (
contId: string
): Promise<Contribution> => {
const file = fs.readFileSync(path.join(fileDir + `/${contId}.mdx`), 'utf8');

const matterResult = matter(file);
const content = await remark().use(remarkHtml).process(matterResult.content);

return {
content: content.value as string,
meta: matterResult.data,
};
};

export const getDynamicPaths = async () => {
const {
data: { contributors },
} = await axios.get('/contributors?select=gh_username');
} = await axios.get('/contributors?select=gh_username&limit=5');

asserHasPropertyArray(contributors);
return contributors.map((obj) => {
Expand Down
14 changes: 12 additions & 2 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
const mdx = require('@next/mdx');
const withMdx = mdx({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});

/** @type {import('next').NextConfig} */
const nextConfig = {
const nextConfig = withMdx({
reactStrictMode: true,
};
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
});

module.exports = nextConfig;
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
},
"dependencies": {
"@heroicons/react": "^2.0.13",
"@mdx-js/loader": "^2.2.1",
"@mdx-js/react": "^2.2.1",
"@next/font": "13.1.1",
"@next/mdx": "^13.1.1",
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
Expand Down Expand Up @@ -49,6 +52,7 @@
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.1",
"tailwindcss": "^3.2.4"
"tailwindcss": "^3.2.4",
"zod": "^3.20.2"
}
}
24 changes: 10 additions & 14 deletions server/controllers/contributors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ERR } from '&/errors';
import errorHandler from '&/middlewares/errorHandler';
import Contributor from '&/models/contributors';
import { assertHasProps, assertIsString } from '&/validator/assertionGuards';
import { assertIsString } from '&/validator/assertionGuards';
import type { TContributor } from '&validation/contributor.validation';
import ContV from '&validation/contributor.validation';
import { NextApiHandler } from 'next';
import { TContributor } from 'types/contributors';

function setUpdatableProperty<T, Prop extends keyof T>(
value: object,
Expand Down Expand Up @@ -57,7 +58,10 @@ export const getSingleContributor: NextApiHandler = errorHandler(
const { contId } = req.query;
assertIsString(contId, `Route does not exist`);
const contributor = await Contributor.findOne({ gh_username: contId });
// todo: enable isDeleted true
if (contributor!.isDeleted)
throw new ERR.Not_Found(
`Contributor ${contId} had deleted his/her account`
);
if (!contributor)
throw new ERR.Not_Found(`Contributor ${contId} has not contributed yet`);
res.status(200).json(contributor);
Expand All @@ -66,24 +70,16 @@ export const getSingleContributor: NextApiHandler = errorHandler(

export const createContributor: NextApiHandler = errorHandler(
async (req, res) => {
assertHasProps<TContributor, keyof TContributor>(req.body, [
'avatar_url',
'gh_username',
'content',
'ghid',
'html_url',
'name',
'occupation',
]);
const contData = ContV.parse(req.body);
const isExistingContributor = await Contributor.findOne({
gh_username: req.body.gh_username,
gh_username: contData.gh_username,
});
if (isExistingContributor)
throw new ERR.Bad_Request(
`Contributor ${req.body.name} already contributed`
);

const contributor = await Contributor.create(req.body);
const contributor = await Contributor.create(contData);
res.status(201).json(contributor);
}
);
Expand Down
3 changes: 1 addition & 2 deletions server/db/connectDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ if (!cached) {
export default function connectDB<T>(handler: NextApiHandler<T>) {
return async (req: NextApiRequest, res: NextApiResponse<T>) => {
if (cached.conn) {
console.log(`I'm here all is ok`);
return handler(req, res);
}

if (!cached.promise) {
const opts = {
bufferCommands: false,
};

mongoose.set('strictQuery', true);
cached.promise = mongoose.connect(MONGODB_URI!, opts).then((mongoose) => {
return mongoose;
});
Expand Down
5 changes: 2 additions & 3 deletions server/models/contributors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { TContributor } from '&validation/contributor.validation';
import mongoose, { Model, model } from 'mongoose';
import { TContributor } from 'types/contributors';

type ContributorModel = Model<TContributor>;

const contributorSchema = new mongoose.Schema<TContributor, ContributorModel>(
{
ghid: {
Expand Down Expand Up @@ -41,7 +40,7 @@ const contributorSchema = new mongoose.Schema<TContributor, ContributorModel>(
},
content: {
type: String,
require: true,
required: true,
},
profile_views: {
type: Number,
Expand Down
22 changes: 22 additions & 0 deletions server/models/validation/contributor.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

const ContV = z.object({
ghid: z.number(),
gh_username: z.string(),
name: z.string(),
avatar_url: z.string(),
email: z.string().email(`Not a valid email`).nullable(),
html_url: z.string(),
location: z.string().nullable(),
occupation: z.string(),
bio: z.string().nullable(),
content: z.string(),
profile_views: z.number().default(0),
isDeleted: z.boolean().default(false),
});
export default ContV;
export type TContributor = z.infer<typeof ContV>;
export type TCont = Pick<
TContributor,
'avatar_url' | 'gh_username' | 'name' | 'bio' | 'occupation'
>;
1 change: 0 additions & 1 deletion src/pages/api/v1/contributors/[contId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { NextApiHandler } from 'next';
// end-point: origin/api/v1/contributors
const handler: NextApiHandler = async (req, res) => {
const { method } = req;
console.log(req.query);
if (method === ('GET' as 'GET')) {
// handle get request
return getSingleContributor(req, res);
Expand Down
33 changes: 20 additions & 13 deletions src/pages/contributors/[contId].tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { fetchSingleContributor, getDynamicPaths } from 'lib/contributors';
import type { TContributor } from '&validation/contributor.validation';
import {
fetchSingleContributor,
getContribution,
getDynamicPaths,
} from 'lib/contributors';
import { GetStaticPaths, GetStaticProps } from 'next';
import { useRouter } from 'next/router';
import { FC } from 'react';
import { TCont } from 'types/contributors';

type SingleContributorProps = {
contributor: TCont & {
createdAt: Date;
contributor: Omit<TContributor, 'isDeleted'> & {
createdAt?: Date;
};
};

const SingleContributor: FC<SingleContributorProps> = ({ contributor }) => {
const router = useRouter();
const { contId } = router.query;
console.log(contributor);
return <>single contributor {contId} </>;
return <>single contributor {contributor.name} </>;
};

export default SingleContributor;
Expand All @@ -28,11 +28,18 @@ export const getStaticPaths: GetStaticPaths<{ contId: string }> = async () => {
};
};

export const getStaticProps: GetStaticProps<SingleContributorProps> = async (
context
) => {
export const getStaticProps: GetStaticProps<SingleContributorProps> = async ({
params,
}) => {
try {
const contributor = await fetchSingleContributor(context);
if (!params?.contId || Array.isArray(params.contId))
throw new Error(`Please specify a single contributor id`);
const contribution = await getContribution(params.contId); // autometically throws error if file not found
const contributor = await fetchSingleContributor(
contribution.meta.github_username as string,
contribution
);

return {
props: {
contributor,
Expand Down
20 changes: 13 additions & 7 deletions src/pages/contributors/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import type { TCont } from '&validation/contributor.validation';
import { fetchAllContributors } from 'lib/contributors';
import { GetStaticProps } from 'next';
import { FC } from 'react';
import { TCont } from 'types/contributors';

export type ContributorsProps = {
contributors: TCont[];
count: number;
contributors: (TCont & { _id: string })[];
};

const Contributors: FC<ContributorsProps> = ({ contributors }) => {
console.log(contributors);

return <>Contributors</>;
const conts = contributors.map((cont) => (
<div className="" key={cont._id}>
<p>{cont.name}</p>
</div>
));
return <>{conts}</>;
};

export default Contributors;

export const getStaticProps: GetStaticProps<ContributorsProps> = async () => {
try {
// fetch all contributors limit 10
const contributors = await fetchAllContributors(10);
const data = await fetchAllContributors(10);

return {
props: {
contributors,
count: data.count,
contributors: data.contributors,
},
revalidate: 15,
};
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@/*": ["src/*"],
"~/*": ["public/*"],
"&/*": ["server/*"],
"&validation/*": ["server/models/validation/*"],
"@/layouts/*": ["src/components/layouts/*"],
"@/utilities/*": ["src/components/utilities/*"],
"~/images/*": ["public/images/*"]
Expand Down
19 changes: 0 additions & 19 deletions types/contributors.ts

This file was deleted.

Loading

0 comments on commit d4d2ca6

Please sign in to comment.