Skip to content

Commit f462a96

Browse files
authored
Merge pull request #65 from DouglasNeuroInformatics/v8
V8
2 parents 43f461b + d97624e commit f462a96

File tree

105 files changed

+909
-3261
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+909
-3261
lines changed

.bruno/Auth/Login.bru

Lines changed: 0 additions & 18 deletions
This file was deleted.

.bruno/collection.bru

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,3 @@
11
vars:pre-request {
22
BASE_URL: http://localhost:5500
3-
}
4-
5-
script:pre-request {
6-
const axios = require("axios");
7-
8-
const getVar = (key) => {
9-
const value = bru.getVar(key);
10-
if (value === undefined) {
11-
throw new Error(`Could not get variable: ${key}`)
12-
}
13-
return value;
14-
}
15-
16-
if (!req.url.endsWith('/auth/login')) {
17-
const baseUrl = 'http://localhost:5500';
18-
const username = 'admin';
19-
const password = 'password';
20-
const response = await axios.post(`${baseUrl}/auth/login`, {
21-
username,
22-
password
23-
});
24-
req.setHeader('Authorization', `Bearer ${response.data.accessToken}`)
25-
}
26-
27-
28-
29-
30-
31-
323
}

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": ["Bash(npm run lint)", "Bash(npm test)", "Bash(npm test:*)"],
4+
"deny": [],
5+
"ask": []
6+
}
7+
}

deno.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

example/app.controller.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

example/app.module.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Module } from '@nestjs/common';
2+
import { PrismaClient } from '@prisma/client';
3+
4+
import { PrismaModule } from '../src/index.js';
5+
import { CatsModule } from './cats/cats.module.js';
6+
7+
@Module({
8+
imports: [
9+
CatsModule,
10+
PrismaModule.forRootAsync({
11+
inject: ['MONGO_CONNECTION'],
12+
provideInjectionTokensFrom: [
13+
{
14+
provide: 'MONGO_CONNECTION',
15+
useFactory: async (): Promise<string> => {
16+
const { MongoMemoryReplSet } = await import('mongodb-memory-server');
17+
const replSet = await MongoMemoryReplSet.create({ replSet: { count: 1, name: 'rs0' } });
18+
return new URL(replSet.getUri('test')).href;
19+
}
20+
}
21+
],
22+
useFactory: (mongoConnection: string) => {
23+
return {
24+
client: new PrismaClient({
25+
datasourceUrl: mongoConnection
26+
})
27+
};
28+
}
29+
})
30+
]
31+
})
32+
export class AppModule {}

example/app.test.ts

Lines changed: 6 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import { randomBytes } from 'crypto';
2-
3-
import { Document, Window } from 'happy-dom';
4-
import { afterAll, beforeAll, beforeEach, describe, expect, vi } from 'vitest';
1+
import { describe, expect, vi } from 'vitest';
52

63
import { e2e } from '../src/testing/index.js';
74
import app from './app.js';
@@ -11,48 +8,6 @@ import type { $Cat, $CreateCatData } from './cats/schemas/cat.schema.js';
118
vi.unmock('@prisma/client');
129

1310
e2e(app, ({ api }) => {
14-
describe('/', (it) => {
15-
let document: Document;
16-
let window: Window;
17-
18-
beforeAll(() => {
19-
window = new Window({
20-
innerHeight: 768,
21-
innerWidth: 1024,
22-
url: 'http://localhost:5500'
23-
});
24-
document = window.document as any;
25-
26-
window.console.error = (...args) => {
27-
console.error(...args);
28-
};
29-
});
30-
31-
afterAll(async () => {
32-
await window.happyDOM.close();
33-
});
34-
35-
it('should render html', async () => {
36-
const response = await api.get('/');
37-
expect(response.type).toBe('text/html');
38-
});
39-
40-
it('should render an interactive UI', async () => {
41-
const response = await api.get('/');
42-
document.write(response.text);
43-
await window.happyDOM.waitUntilComplete();
44-
const h1 = document.querySelector('h1')!;
45-
expect(h1.innerText).toBe('Welcome to the Example App');
46-
const ul = document.querySelector('ul')!;
47-
expect(ul.style.display).toBe('none');
48-
const button = document.querySelector('button')!;
49-
button.click();
50-
await window.happyDOM.waitUntilComplete();
51-
52-
// expect(ul.style.display).toBe('block');
53-
});
54-
});
55-
5611
describe('/docs', (it) => {
5712
it('should configure the documentation spec', async () => {
5813
const response = await api.get('/docs/spec.json');
@@ -65,67 +20,14 @@ e2e(app, ({ api }) => {
6520
});
6621
});
6722

68-
describe('/auth/login', (it) => {
69-
it('should return status code 400 if the request body does not include credentials', async () => {
70-
const response = await api.post('/auth/login');
71-
expect(response.status).toBe(400);
72-
});
73-
it('should return status code 400 if the request body does not include a username', async () => {
74-
const response = await api.post('/auth/login').send({ username: 'admin' });
75-
expect(response.status).toBe(400);
76-
});
77-
it('should return status code 400 if the request body does not include a password', async () => {
78-
const response = await api.post('/auth/login').send({ password: 'password' });
79-
expect(response.status).toBe(400);
80-
});
81-
it('should return status code 400 if username and password are empty strings', async () => {
82-
const response = await api.post('/auth/login').send({ password: '', username: '' });
83-
expect(response.status).toBe(400);
84-
});
85-
it('should return status code 400 if password is a number', async () => {
86-
const response = await api.post('/auth/login').send({ password: 123, username: 'admin' });
87-
expect(response.status).toBe(400);
88-
});
89-
it('should return status code 401 if the user does not exist', async () => {
90-
const response = await api.post('/auth/login').send({ password: 'password', username: 'user' });
91-
expect(response.status).toBe(401);
92-
});
93-
it('should return status code 200 and an access token if the credentials are correct', async () => {
94-
const response = await api.post('/auth/login').send({ password: 'password', username: 'admin' });
95-
expect(response.status).toBe(200);
96-
expect(response.body).toStrictEqual({
97-
accessToken: expect.stringMatching(/^[A-Za-z0-9-_]+\.([A-Za-z0-9-_]+)\.[A-Za-z0-9-_]+$/)
98-
});
99-
});
100-
});
101-
10223
describe('/cats', (it) => {
103-
let accessToken: string;
10424
let createdCat: $Cat;
10525

106-
beforeEach(async () => {
107-
const response = await api.post('/auth/login').send({ password: 'password', username: 'admin' });
108-
accessToken = response.body.accessToken;
109-
});
110-
111-
it('should return status code 401 if there is no access token provided', async () => {
112-
const response = await api.get('/cats');
113-
expect(response.status).toBe(401);
114-
});
115-
116-
it('should return status code 401 if there is an invalid access token provided', async () => {
117-
const response = await api.get('/cats').set('Authorization', `Bearer ${randomBytes(12).toString('base64')}`);
118-
expect(response.status).toBe(401);
119-
});
120-
12126
it('should allow a POST request', async () => {
122-
const response = await api
123-
.post('/cats')
124-
.set('Authorization', `Bearer ${accessToken}`)
125-
.send({
126-
age: 1,
127-
name: 'Winston'
128-
} satisfies $CreateCatData);
27+
const response = await api.post('/cats').send({
28+
age: 1,
29+
name: 'Winston'
30+
} satisfies $CreateCatData);
12931
expect(response.status).toBe(201);
13032
expect(response.body).toMatchObject({
13133
_id: expect.any(String),
@@ -136,7 +38,7 @@ e2e(app, ({ api }) => {
13638
});
13739

13840
it('should allow a GET request', async () => {
139-
const response = await api.get('/cats').set('Authorization', `Bearer ${accessToken}`);
41+
const response = await api.get('/cats');
14042
expect(response.status).toBe(200);
14143
expect(response.body).toStrictEqual([createdCat]);
14244
});

example/app.ts

Lines changed: 7 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,16 @@
1-
/* eslint-disable @typescript-eslint/explicit-function-return-type */
2-
3-
import { PrismaClient } from '@prisma/client';
4-
import { z } from 'zod/v4';
5-
6-
import { $BaseEnv, AppFactory, AuthModule, CryptoService } from '../src/index.js';
7-
import { AppController } from './app.controller.js';
8-
import { CatsModule } from './cats/cats.module.js';
1+
import { $BaseEnv, acceptLanguage, AppFactory } from '../src/index.js';
2+
import { AppModule } from './app.module.js';
93

104
export default AppFactory.create({
11-
controllers: [AppController],
5+
configureMiddleware: (consumer) => {
6+
const middleware = acceptLanguage({ fallbackLanguage: 'en', supportedLanguages: ['en', 'fr'] });
7+
consumer.apply(middleware).forRoutes('*');
8+
},
129
docs: {
1310
path: '/docs',
1411
title: 'Example API'
1512
},
1613
envSchema: $BaseEnv,
17-
imports: [
18-
AuthModule.forRootAsync({
19-
inject: [CryptoService],
20-
useFactory: (cryptoService: CryptoService) => {
21-
return {
22-
defineAbility: (ability, payload) => {
23-
// We cannot correctly declare UserTypes here until we migrate to a monorepo setup
24-
if ((payload as { isAdmin: true }).isAdmin) {
25-
ability.can('manage', 'all');
26-
}
27-
},
28-
schemas: {
29-
loginCredentials: z.object({
30-
password: z.string().min(1),
31-
username: z.string().min(1)
32-
})
33-
},
34-
userQuery: async ({ username }) => {
35-
if (username !== 'admin') {
36-
return null;
37-
}
38-
return {
39-
hashedPassword: await cryptoService.hashPassword('password'),
40-
tokenPayload: {
41-
isAdmin: true
42-
}
43-
};
44-
}
45-
};
46-
}
47-
}),
48-
CatsModule
49-
],
50-
jsx: {
51-
baseDir: import.meta.dirname,
52-
importMap: {
53-
index: () => import('./pages/index.js')
54-
}
55-
},
56-
prisma: {
57-
client: {
58-
constructor: PrismaClient
59-
},
60-
dbPrefix: 'libnest-example',
61-
useInMemoryDbForTesting: true
62-
},
14+
imports: [AppModule],
6315
version: null
6416
});

example/cats/cats.controller.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Body, Controller, Get, Post } from '@nestjs/common';
22
import { ApiOperation } from '@nestjs/swagger';
33

4-
import { RouteAccess } from '../../src/index.js';
54
import { CatsService } from './cats.service.js';
65
import { $Cat, $CreateCatData } from './schemas/cat.schema.js';
76

@@ -11,14 +10,12 @@ export class CatsController {
1110

1211
@ApiOperation({ summary: 'Create Cat' })
1312
@Post()
14-
@RouteAccess({ action: 'create', subject: 'Cat' })
1513
async create(@Body() data: $CreateCatData): Promise<$Cat> {
1614
return this.catsService.create(data);
1715
}
1816

1917
@ApiOperation({ summary: 'Get All Cats' })
2018
@Get()
21-
@RouteAccess({ action: 'read', subject: 'Cat' })
2219
async findAll(): Promise<$Cat[]> {
2320
return this.catsService.findAll();
2421
}

example/cats/cats.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
/* eslint-disable perfectionist/sort-objects */
22

33
import { Injectable, InternalServerErrorException } from '@nestjs/common';
4+
import type { PrismaClient } from '@prisma/client';
45

56
import { InjectPrismaClient } from '../../src/index.js';
67

7-
import type { PrismaClientLike } from '../../src/index.js';
88
import type { $Cat } from './schemas/cat.schema.js';
99

1010
@Injectable()
1111
export class CatsService {
12-
constructor(@InjectPrismaClient() private readonly prismaClient: PrismaClientLike) {}
12+
constructor(@InjectPrismaClient() private readonly prismaClient: PrismaClient) {}
1313

1414
async create(cat: Omit<$Cat, '_id'>): Promise<$Cat> {
1515
const id = crypto.randomUUID();

0 commit comments

Comments
 (0)