Skip to content

Commit caae61e

Browse files
committed
fix: Generic REST API for any DB model + Base models created
1 parent 7c847c0 commit caae61e

File tree

16 files changed

+366
-55
lines changed

16 files changed

+366
-55
lines changed

.eslintrc.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
"no-nested-ternary": "off",
4040
"no-console": ["warn", { allow: ["warn", "error"] }],
4141
'import/prefer-default-export': 'off',
42+
'camelcase': 'off',
4243
},
4344
settings: {
4445
node: {

client/src/api/RecordRoutes.ts

+4-16
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
1+
import { Record } from '@fullstack-typescript-monorepo/prisma';
12
import Fetch from '../utils/fetcher';
23
import Super from './Super';
34
import { User } from './UserRoutes';
45

5-
export interface Record {
6-
id: number;
7-
actionTime: string;
8-
actionType: string;
9-
objectType: string;
6+
export type RecordWithAuthor = Record & {
107
author: User;
11-
}
12-
13-
interface RecordListProps {
14-
object?: string;
15-
fetchPath: string;
16-
}
8+
};
179

1810
const RecordRoutes = {
1911
...Super<Record>('record'),
20-
list: ({
21-
object,
22-
fetchPath,
23-
}: RecordListProps) => Fetch<Record[]>('/api/record/list', {
12+
list: (object?: string) => Fetch<RecordWithAuthor[]>('/api/record/list', {
2413
object,
25-
fetchPath,
2614
}),
2715
};
2816

client/src/api/RequestRoutes.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { Request as _Request } from '@fullstack-typescript-monorepo/prisma';
12
import Super from './Super';
23

3-
export interface Request {
4-
id: number;
5-
status: 'pending' | 'success' | 'error';
6-
response: object;
7-
}
4+
// Typescript reference error hack
5+
export type Request = {
6+
[key in keyof _Request]: Request[key];
7+
};
88

99
const RequestRoutes = {
1010
...Super<Request>('request'),

client/src/api/Super.ts

-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ const Super = <Model>(model: string) => ({
3030
state,
3131
}, 'POST'),
3232
update: (id: number, data: RecursivePartial<Model>, fetchPath?: string) => Fetch<Model>(`/api/${model}/${id}`, data, 'POST', { fetchPath }),
33-
import: (data: FormData) => Fetch<never>(`/api/${model}/import`, data, 'POST'),
3433
delete: (id: number) => Fetch<never>(`/api/${model}/${id}`, {}, 'DELETE'),
35-
empty: () => Fetch<never>(`/api/${model}/empty`),
3634
});
3735

3836
export default Super;

client/src/api/UserRoutes.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ export interface User extends Omit<_User, 'password'> {
77
}
88

99
const UserRoutes = {
10-
...Super<User>('app-user'),
11-
authenticate: (login: string, password: string): Promise<User> => Fetch<User>('/api/app-user/authenticate', {
10+
...Super<User>('user'),
11+
authenticate: (login: string, password: string): Promise<User> => Fetch<User>('/api/user/authenticate', {
1212
login,
1313
password,
1414
}, 'POST'),
15-
changePassword: (id: number, password: string) => Fetch(`/api/app-user/${id}/change-password`, { password }, 'POST'),
15+
changePassword: (id: number, password: string) => Fetch(`/api/user/${id}/change-password`, { password }, 'POST'),
1616
sendPasswordResetMail: (
1717
login: string,
18-
) => Fetch(`/api/app-user/${login}/send-password-reset-mail`),
18+
) => Fetch(`/api/user/${login}/send-password-reset-mail`),
1919
checkResetCodeValidity: (
2020
login: string,
2121
code: string,
22-
) => Fetch<boolean>(`/api/app-user/${login}/reset-code-check`, { code }),
22+
) => Fetch<boolean>(`/api/user/${login}/reset-code-check`, { code }),
2323
resetPassword: (
2424
login: string,
2525
code: string,
2626
password: string,
27-
) => Fetch('/api/app-user/reset-password', { login, code, password }, 'POST'),
27+
) => Fetch('/api/user/reset-password', { login, code, password }, 'POST'),
2828
};
2929

3030
export default UserRoutes;

client/src/components/ActionsTable.tsx

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
22
import moment from 'moment';
3-
import React, { useEffect, useMemo } from 'react';
3+
import React, { useEffect } from 'react';
44
import { useTranslation } from 'react-i18next';
55
import RecordRoutes from '../api/RecordRoutes';
66
import useStateAsync from '../hooks/useStateAsync';
@@ -18,15 +18,10 @@ const ActionsTable = ({
1818
}: ActionsTableProps) => {
1919
const { t } = useTranslation('actions');
2020

21-
const recordsFetchProps = useMemo(() => ({
22-
object,
23-
fetchPath: '(id, actionTime, actionType, objectType, author(person(firstName, lastName)))',
24-
}), [object]);
25-
2621
const { data: records, reload: reloadRecords } = useStateAsync(
2722
[],
2823
RecordRoutes.list,
29-
recordsFetchProps,
24+
object,
3025
);
3126

3227
// Reload table when new record is added and/or when reloadActions is updated
@@ -67,10 +62,10 @@ const ActionsTable = ({
6762
<TableCell component="th" scope="row">{t('anonymousAuthor')}</TableCell>
6863
)}
6964
{!object && (
70-
<TableCell align="right">{action.objectType}</TableCell>
65+
<TableCell align="right">{action.object}</TableCell>
7166
)}
72-
<TableCell align="right">{action.actionType}</TableCell>
73-
<TableCell align="right">{moment(action.actionTime).format('DD/MM/YYYY HH:mm')}</TableCell>
67+
<TableCell align="right">{action.action}</TableCell>
68+
<TableCell align="right">{moment(action.date).format('DD/MM/YYYY HH:mm')}</TableCell>
7469
</TableRow>
7570
))
7671
) : (

client/src/utils/longRequest.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RequestStatus } from '@fullstack-typescript-monorepo/prisma';
12
import RequestRoutes from '../api/RequestRoutes';
23

34
const longRequest = async (
@@ -12,15 +13,15 @@ const longRequest = async (
1213
const promise = new Promise((resolve, reject) => {
1314
const interval = setInterval(() => {
1415
RequestRoutes.get({ id: requestId, fetchPath: '(status, response)' }).then((req) => {
15-
if (req.status === 'success') {
16+
if (req.status === RequestStatus.SUCCESS) {
1617
resolve(req.response);
1718
clearInterval(interval);
1819

1920
// Delete request
2021
RequestRoutes.delete(requestId).catch(() => {
2122
console.error('Error while deleting request', requestId);
2223
});
23-
} else if (req.status === 'error') {
24+
} else if (req.status === RequestStatus.ERROR) {
2425
reject(req.response);
2526
clearInterval(interval);
2627

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- CreateEnum
2+
CREATE TYPE "RequestStatus" AS ENUM ('PENDING', 'SUCCESS', 'ERROR');
3+
4+
-- CreateEnum
5+
CREATE TYPE "RecordAction" AS ENUM ('CREATE', 'UPDATE', 'DELETE');
6+
7+
-- CreateTable
8+
CREATE TABLE "Request" (
9+
"id" SERIAL NOT NULL,
10+
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11+
"status" "RequestStatus" NOT NULL,
12+
13+
CONSTRAINT "Request_pkey" PRIMARY KEY ("id")
14+
);
15+
16+
-- CreateTable
17+
CREATE TABLE "Record" (
18+
"id" SERIAL NOT NULL,
19+
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20+
"action" "RecordAction" NOT NULL,
21+
"object" VARCHAR(255) NOT NULL,
22+
"newValue" VARCHAR(255) NOT NULL,
23+
"authorId" INTEGER NOT NULL,
24+
25+
CONSTRAINT "Record_pkey" PRIMARY KEY ("id")
26+
);
27+
28+
-- AddForeignKey
29+
ALTER TABLE "Record" ADD CONSTRAINT "Record_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `response` to the `Request` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "Request" ADD COLUMN "response" JSONB NOT NULL;

server/prisma/schema.prisma

+37-7
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,42 @@ model Person {
3535
}
3636

3737
model User {
38-
id Int @id @default(autoincrement())
39-
login String @unique @db.VarChar(255)
40-
admin Boolean @default(false)
38+
id Int @id @default(autoincrement())
39+
login String @unique @db.VarChar(255)
40+
admin Boolean @default(false)
4141
password String? @db.VarChar(255)
42-
active Boolean @default(true)
43-
connexionToken String @default("") @db.VarChar(255)
44-
person Person @relation(fields: [personId], references: [id])
45-
personId Int @unique
42+
active Boolean @default(true)
43+
connexionToken String @default("") @db.VarChar(255)
44+
person Person @relation(fields: [personId], references: [id])
45+
personId Int @unique
46+
records Record[]
47+
}
48+
49+
enum RequestStatus {
50+
PENDING
51+
SUCCESS
52+
ERROR
53+
}
54+
55+
model Request {
56+
id Int @id @default(autoincrement())
57+
date DateTime @default(now())
58+
status RequestStatus
59+
response Json
60+
}
61+
62+
enum RecordAction {
63+
CREATE
64+
UPDATE
65+
DELETE
66+
}
67+
68+
model Record {
69+
id Int @id @default(autoincrement())
70+
date DateTime @default(now())
71+
action RecordAction
72+
object String @db.VarChar(255)
73+
newValue String @db.VarChar(255)
74+
author User @relation(fields: [authorId], references: [id])
75+
authorId Int
4676
}

server/src/controllers/REST.ts

+79-7
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { PrismaClient } from '@fullstack-typescript-monorepo/prisma';
22
import { Request, Response } from 'express';
33
import auth from '../utils/auth';
44
import sendError from '../utils/sendError';
5+
import TableUtils, { TableRequestBody } from '../utils/TableUtils';
56

6-
interface GenericPrisma extends PrismaClient {
7+
export interface GenericPrisma extends PrismaClient {
78
[key: string]: unknown;
89
}
910

10-
interface PrismaModel {
11+
export interface MOCK_PrismaModel {
1112
create: (prop: { data: unknown }) => Promise<unknown>;
1213
findUniqueOrThrow: (prop: { where: { id: number } }) => Promise<unknown>;
13-
findMany: () => Promise<unknown[]>;
14+
findMany: (prop?: Record<string, unknown>) => Promise<unknown[]>;
15+
count: (prop: Record<string, unknown>) => Promise<number>;
16+
update: (prop: { where: { id: number }; data: unknown }) => Promise<unknown>;
17+
delete: (prop: { where: { id: number } }) => Promise<unknown>;
1418
}
1519

1620
/**
@@ -25,7 +29,7 @@ const insert = (model: string) => (prisma: PrismaClient) => async (
2529
await auth(prisma, req);
2630

2731
const { body } = req;
28-
const prismaModel = (prisma as GenericPrisma)[model] as PrismaModel;
32+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
2933

3034
const object = await prismaModel.create({
3135
data: body,
@@ -46,7 +50,7 @@ const get = (model: string) => (prisma: PrismaClient) => async (req: Request, re
4650
await auth(prisma, req);
4751

4852
const { id } = req.params;
49-
const prismaModel = (prisma as GenericPrisma)[model] as PrismaModel;
53+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
5054

5155
const object = await prismaModel.findUniqueOrThrow({
5256
where: { id: +id },
@@ -66,7 +70,7 @@ const getAll = (model: string) => (prisma: PrismaClient) => async (req: Request,
6670
try {
6771
await auth(prisma, req);
6872

69-
const prismaModel = (prisma as GenericPrisma)[model] as PrismaModel;
73+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
7074

7175
const objects = await prismaModel.findMany();
7276

@@ -87,7 +91,7 @@ const getAllAsCsv = (model: string) => (prisma: PrismaClient) => async (
8791
try {
8892
await auth(prisma, req);
8993

90-
const prismaModel = (prisma as GenericPrisma)[model] as PrismaModel;
94+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
9195

9296
const objects = await prismaModel.findMany() as Record<string, unknown>[];
9397

@@ -115,11 +119,79 @@ const getAllAsCsv = (model: string) => (prisma: PrismaClient) => async (
115119
}
116120
};
117121

122+
/**
123+
* Get objects for a paginated table
124+
* @param model
125+
*/
126+
const table = (model: string) => (prisma: PrismaClient) => async (
127+
req: Request<never, unknown, TableRequestBody>,
128+
res: Response,
129+
) => {
130+
try {
131+
await auth(prisma, req);
132+
133+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
134+
135+
res.json(TableUtils.getData(req, prismaModel));
136+
} catch (error) {
137+
sendError(res, error);
138+
}
139+
};
140+
141+
/**
142+
* Update an object in the database
143+
* @param model
144+
*/
145+
const update = (model: string) => (prisma: PrismaClient) => async (
146+
req: Request<{ id: string }, unknown, Record<string, unknown>>,
147+
res: Response,
148+
) => {
149+
try {
150+
await auth(prisma, req);
151+
152+
const { id } = req.params;
153+
const { body } = req;
154+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
155+
156+
const updatedObject = await prismaModel.update({
157+
where: { id: +id },
158+
data: { ...body },
159+
});
160+
161+
res.json(updatedObject);
162+
} catch (error) {
163+
sendError(res, error);
164+
}
165+
};
166+
167+
const deleteObject = (model: string) => (prisma: PrismaClient) => async (
168+
req: Request,
169+
res: Response,
170+
) => {
171+
try {
172+
await auth(prisma, req);
173+
174+
const { id } = req.params;
175+
const prismaModel = (prisma as GenericPrisma)[model] as MOCK_PrismaModel;
176+
177+
await prismaModel.delete({
178+
where: { id: +id },
179+
});
180+
181+
res.send();
182+
} catch (error) {
183+
sendError(res, error);
184+
}
185+
};
186+
118187
const REST = (model: string) => ({
119188
insert: insert(model),
120189
get: get(model),
121190
getAll: getAll(model),
122191
getAllAsCsv: getAllAsCsv(model),
192+
table: table(model),
193+
update: update(model),
194+
delete: deleteObject(model),
123195
});
124196

125197
export default REST;

0 commit comments

Comments
 (0)