Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/utils/src/routes/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ApiRoutes } from "@/routes/route";

type ApiBase = keyof typeof ApiRoutes;

type RouteDescriptor<
BASE extends ApiBase,
SUB extends keyof (typeof ApiRoutes)[BASE]["routes"],
Comment on lines +6 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think TS convention involves writing generics all in capitals letters.

> = {
base: BASE;
subRoute?: SUB;
};

export class ApiRoutesManager {
private readonly apiBasePath: string;

constructor() {
this.apiBasePath = "/api/v1";
}

private replaceParams<Path extends string>(
path: Path,
params: Record<string, string | number>
): string {
return path.replace(/:([a-zA-Z0-9]+)/g, (_, key) => {
const value = params[key];
if (value === undefined) {
throw new Error(`Missing parameter: ${key}`);
}
return value.toString();
});
}

generateUrl<
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a full URL... let call it generatePath, or generateRoutePath.

I love abbreviations like gen instead of generate 😄. However, that's not how we name things in this project.

BASE extends ApiBase,
SUB extends keyof (typeof ApiRoutes)[BASE]["routes"],
>({
route,
params = {},
type = "full",
}: {
route: RouteDescriptor<BASE, SUB>;
params?: Record<string, string | number>;
type?: "full" | "base";
}): string {
const baseRoute = ApiRoutes[route.base].base;
const subPath = route.subRoute
? ApiRoutes[route.base].routes[route.subRoute]
: "";

let pathSegment = "";
switch (type) {
case "full":
pathSegment = `${baseRoute}${subPath}`;
break;
case "base":
pathSegment = baseRoute;
break;
}

const replacedPath = this.replaceParams(pathSegment, params);
return `${this.apiBasePath}${replacedPath}`;
}
}
9 changes: 8 additions & 1 deletion packages/utils/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export { Web, Landing, Dashboard, StudentSettingsTabId } from "@/routes/route";
export {
Web,
Landing,
Dashboard,
StudentSettingsTabId,
ApiRoutes,
} from "@/routes/route";
export { clients } from "@/routes/clients";
export {
RoutesManager,
isValidRoute,
PayloadOf,
UrlParamsOf,
} from "@/routes/core";
export { ApiRoutesManager } from "@/routes/api";
249 changes: 249 additions & 0 deletions packages/utils/src/routes/route.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose exporting all these types and constant in one scope, say ApiRoute. And then renaming each of them accordingly.

ApiRoutesType -> ApiRoutesMap -> ApiRoute.Map
ApiRouteBaseRoute -> ApiRoute.BaseRoute
ApiRoutes -> ApiRoute.Routes

Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,252 @@ export type StudentSettingsTabId =
| "password"
| "notifications"
| "topics";

export const ApiRouteBaseRoute = "/api/v1";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming here is quiet repetitive. We may call it ApiRouteMainBase.


type ApiRoutesType = Record<
string,
{ base: ApiRouteBase; routes: Record<string, string> }
>;
Comment on lines +76 to +79
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this ApiRoutesMap. WDYT?


export type ApiRouteBase =
| "/auth"
| "/contact-request"
| "/user"
| "/lesson"
| "/interview"
| "/availability-slot"
| "/rating"
| "/chat"
| "/plan"
| "/coupon"
| "/invite"
| "/invoice"
| "/topic"
| "/asset"
| "/cache"
| "/session"
| "/fawry"
| "/tx"
| "/sub"
| "/confirmation-code"
| "/report";

export const ApiRoutes: ApiRoutesType = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Object.freeze.

auth: {
base: "/auth",
routes: {
loginWithPassword: "/password",
loginWithGoogle: "/google",
refreshToken: "/refresh-token",
},
},
contactRequest: {
base: "/contact-request",
routes: {
create: "/",
},
},
user: {
base: "/user",
routes: {
create: "/",
selectInterviewer: "/interviewer/select",
findCurrentUser: "/current",
findUsers: "/list",
uploadUserImage: "/asset",
uploadTutorAssets: "/asset/tutor",
findTutorMeta: "/tutor/meta",
findTutorInfo: "/tutor/info/:tutorId",
findOnboardedTutors: "/tutor/list/onboarded",
findPersonalizedTutorStats: "/tutor/stats/personalized",
findUncontactedTutors: "/tutor/list/uncontacted",
findTutorStats: "/tutor/stats/:tutor",
findTutorActivityScores: "/tutor/activity/:tutor",
findStudioTutors: "/tutor/all/for/studio",
findStudioTutor: "/tutor/:tutorId/for/studio",
findFullTutors: "/tutor/full-tutors",
findTutoringMinutes: "/tutor/tutoring-minutes",
findPersonalizedStudentStats: "/student/stats/personalized",
findStudentStats: "/student/stats/:student",
findStudios: "/studio/list",
findById: "/:id",
update: "/:id",
},
},
lesson: {
base: "/lesson",
routes: {
create: "/",
update: "/",
findAttendedLessonsStats: "/attended/stats",
findLessons: "/list",
findLessonById: "/:id",
cancel: "/:lessonId",
},
},
interview: {
base: "/interview",
routes: {
create: "/",
update: "/",
find: "/list",
selectInterviewer: "/select",
},
},
availabilitySlot: {
base: "/availability-slot",
routes: {
find: "/",
set: "/",
},
},
rating: {
base: "/rating",
routes: {
createRating: "/",
findRaterRatings: "/list/rater/:id",
findRatings: "/list",
findRateeRatings: "/list/ratee/:id",
findTutorRatings: "/list/tutor/:id",
findRatingById: "/:id",
updateRating: "/:id",
deleteRating: "/:id",
},
},
chat: {
base: "/chat",
routes: {
createRoom: "/new",
findUserRooms: "/list/rooms/:userId",
findRoomMessages: "/list/:roomId/messages",
findRoomByMembers: "/room/by/members/",
findRoomMembers: "/room/members/:roomId",
updateRoom: "/room/:roomId",
},
},
plan: {
base: "/plan",
routes: {
create: "/",
find: "/list",
findById: "/:id",
update: "/:id",
delete: "/:id",
},
},
coupon: {
base: "/coupon",
routes: {
create: "/",
findAll: "/list",
findByCode: "/code/:code",
findById: "/:id",
update: "/:id",
delete: "/:id",
},
},
invite: {
base: "/invite",
routes: {
create: "/",
findAll: "/list",
findById: "/:id",
update: "/:id",
delete: "/:id",
},
},
invoice: {
base: "/invoice",
routes: {
find: "/",
stats: "/stats/:tutorId",
create: "/",
update: "/:invoiceId",
},
},
topic: {
base: "/topic",
routes: {
createTopic: "/",
updateTopic: "/:id",
deleteTopic: "/:id",
findTopics: "/list",
findUserTopics: "/of/user",
addUserTopics: "/of/user",
deleteUserTopics: "/of/user",
replaceUserTopics: "/of/user",
},
},
asset: {
base: "/asset",
routes: {
sample: "/sample",
},
},
cache: {
base: "/cache",
routes: {
flush: "/flush",
},
},
session: {
base: "/session",
routes: {
getSessionToken: "/token",
findSessionMembers: "/:sessionId",
},
},
fawry: {
base: "/fawry",
routes: {
payWithCard: "/pay/card",
payWithRefNum: "/pay/ref-num",
payWithEWallet: "/pay/e-wallet",
payWithBankInstallments: "/pay/bank-installments",
cancelUnpaidOrder: "/cancel-unpaid-order",
refund: "/refund",
getAddCardTokenUrl: "/card-token/url",
findCardTokens: "/card-token/list",
deleteCardToken: "/card-token",
getPaymentStatus: "/payment-status",
setPaymentStatus: "/payment-status",
syncPaymentStatus: "/payment-status/sync",
},
},
transaction: {
base: "/tx",
routes: {
findLast: "/last",
find: "/list",
findById: "/:id",
},
},
subscription: {
base: "/sub",
routes: {
findUserSubscription: "/user",
find: "/list",
findById: "/:id",
},
},
confirmationCode: {
base: "/confirmation-code",
routes: {
sendVerifyPhoneCode: "/phone/send",
verifyPhoneCode: "/phone/verify",
sendForgetPasswordCode: "/password/send",
confirmForgetPasswordCode: "/password/confirm",
sendEmailVerificationCode: "/email/send",
confirmEmailVerificationCode: "/email/confirm",
},
},
report: {
base: "/report",
routes: {
find: "/list",
create: "/",
update: "/",
},
},
};
Loading
Loading