├── .gitignore ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20221211070011_removed_post │ │ └── migration.sql │ └── 20221211062144_init │ │ └── migration.sql ├── schema.prisma └── seed.ts ├── tsconfig.json ├── package.json ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | *.env* 4 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "dist", 5 | "strict": true, 6 | "lib": ["esnext"], 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = "file:./dev.db" 8 | } 9 | 10 | model User { 11 | id Int @id @default(autoincrement()) 12 | email String @unique 13 | name String? 14 | } 15 | -------------------------------------------------------------------------------- /prisma/migrations/20221211070011_removed_post/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | PRAGMA foreign_keys=off; 9 | DROP TABLE "Post"; 10 | PRAGMA foreign_keys=on; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-express", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "ts-node src/index.ts", 7 | "postinstall": "npx prisma migrate dev" 8 | }, 9 | "dependencies": { 10 | "@prisma/client": "4.7.1", 11 | "express": "4.18.2" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "4.17.14", 15 | "@types/node": "18.11.12", 16 | "prisma": "4.7.1", 17 | "ts-node": "10.9.1", 18 | "typescript": "4.9.4" 19 | }, 20 | "prisma": { 21 | "seed": "ts-node prisma/seed.ts" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /prisma/migrations/20221211062144_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "email" TEXT NOT NULL, 5 | "name" TEXT 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Post" ( 10 | "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 11 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" DATETIME NOT NULL, 13 | "title" TEXT NOT NULL, 14 | "content" TEXT, 15 | "published" BOOLEAN NOT NULL DEFAULT false, 16 | "viewCount" INTEGER NOT NULL DEFAULT 0, 17 | "authorId" INTEGER, 18 | CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 23 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Prisma } from '@prisma/client' 2 | 3 | const prisma = new PrismaClient() 4 | 5 | const userData: Prisma.UserCreateInput[] = [ 6 | { 7 | name: 'Alice', 8 | email: 'alice@prisma.io', 9 | }, 10 | { 11 | name: 'Nilu', 12 | email: 'nilu@prisma.io', 13 | }, 14 | { 15 | name: 'Mahmoud', 16 | email: 'mahmoud@prisma.io', 17 | }, 18 | ] 19 | 20 | async function main() { 21 | console.log(`Start seeding ...`) 22 | for (const u of userData) { 23 | const user = await prisma.user.create({ 24 | data: u, 25 | }) 26 | console.log(`Created user with id: ${user.id}`) 27 | } 28 | console.log(`Seeding finished.`) 29 | } 30 | 31 | main() 32 | .then(async () => { 33 | await prisma.$disconnect() 34 | }) 35 | .catch(async (e) => { 36 | console.error(e) 37 | await prisma.$disconnect() 38 | process.exit(1) 39 | }) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prisma Auditing 2 | 3 | This example is for database audit logs 4 | 5 | > Users are idenfied based on `Authorization` headers 6 | 7 | ### Setup 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | ### Running Server 14 | 15 | ```bash 16 | npm run dev 17 | ``` 18 | 19 | ### Making Requests 20 | 21 | #### List Users 22 | 23 | ```bash 24 | curl localhost:3000/users 25 | ``` 26 | 27 | #### Create User 28 | 29 | ```bash 30 | curl localhost:3000/users \ 31 | -X POST \ 32 | -H "Authorization: Bearer foo" \ 33 | -H "Content-Type: application/json" \ 34 | -d '{ "email": "foo@example.com", "name": "John Doe" }' 35 | ``` 36 | 37 | #### Update User 38 | 39 | ```bash 40 | curl localhost:3000/users/1 \ 41 | -X PATCH \ 42 | -H "Authorization: Bearer foo" \ 43 | -H "Content-Type: application/json" \ 44 | -d '{ "name": "Jane Doe" }' 45 | ``` 46 | 47 | #### Delete User 48 | 49 | ```bash 50 | curl localhost:3000/users/1 \ 51 | -X DELETE \ 52 | -H "Authorization: Bearer foo" 53 | ``` 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from '@prisma/client' 2 | import { AsyncLocalStorage } from 'async_hooks' 3 | import express from 'express' 4 | 5 | function fetchUser(token: string) { 6 | const users: Record = { 7 | foo: "User 1", 8 | bar: "User 2", 9 | baz: "User 3" 10 | } 11 | return users[token] 12 | } 13 | const userContext = new AsyncLocalStorage() 14 | const prisma = new PrismaClient() 15 | 16 | // Auditing middleware 17 | prisma.$use(async (params, next) => { 18 | const { action, model, args } = params 19 | const actionMap: Record = { 20 | 'create': 'created', 21 | 'createMany': 'created', 22 | 'update': "updated", 23 | 'updateMany': "updated", 24 | 'upsert': "upserted", 25 | 'delete': "deleted", 26 | 'deleteMany': "deleted", 27 | } 28 | if (action in actionMap) { 29 | const user = userContext.getStore() 30 | const result = await next(params) 31 | console.log("Timestamp:", new Date().toISOString()) 32 | let actionString 33 | const data = JSON.stringify(args.data) 34 | const where = JSON.stringify(args.where) 35 | if (actionMap[action] === "updated") { 36 | actionString = `with ${data} where ${where}` 37 | } else if (actionMap[action] === "deleted") { 38 | actionString = `where ${where}` 39 | } else { 40 | actionString = `with ${data}` 41 | } 42 | console.log(user, actionMap[action], model, actionString) 43 | console.log() 44 | return result 45 | } 46 | return next(params) 47 | }) 48 | 49 | const app = express() 50 | app.use(express.json()) 51 | 52 | // Attaching User Context 53 | app.use((req, _res, next) => { 54 | if ("authorization" in req.headers) { 55 | const token = req.headers.authorization?.split(" ")[1] || "" 56 | const user = fetchUser(token) 57 | userContext.run(user, () => next()) 58 | return 59 | } 60 | next() 61 | }) 62 | 63 | app.post(`/users`, async (req, res) => { 64 | const { name, email} = req.body 65 | 66 | const result = await prisma.user.create({ 67 | data: { 68 | name, 69 | email, 70 | }, 71 | }) 72 | return res.status(201).json(result) 73 | }) 74 | 75 | app.patch('/users/:id', async (req, res) =>{ 76 | const { id } = req.params 77 | const { name, email} = req.body 78 | 79 | const result = await prisma.user.update({ 80 | where: { id: Number(id) }, 81 | data: { 82 | name, 83 | email, 84 | }, 85 | }) 86 | return res.status(200).json(result) 87 | }) 88 | 89 | app.delete('/users/:id', async (req, res) => { 90 | const { id } = req.params 91 | const users = await prisma.user.delete({ 92 | where: { id: Number(id) } 93 | }) 94 | return res.status(200).json(users) 95 | }) 96 | 97 | app.get('/users', async (_req, res) => { 98 | const users = await prisma.user.findMany() 99 | console.log(users) 100 | return res.status(200).json(users) 101 | }) 102 | 103 | 104 | app.listen(3000, () => 105 | console.log(`🚀 Server ready at: http://localhost:3000`) 106 | ) 107 | --------------------------------------------------------------------------------