Skip to content

Commit 2efe634

Browse files
committed
WIP - account & messages
1 parent e282053 commit 2efe634

30 files changed

+2736
-1949
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
parser: '@typescript-eslint/parser',
33
parserOptions: {
44
project: 'tsconfig.json',
5+
tsconfigRootDir: __dirname,
56
sourceType: 'module',
67
},
78
plugins: ['@typescript-eslint/eslint-plugin'],
@@ -12,6 +13,7 @@ module.exports = {
1213
root: true,
1314
env: {
1415
node: true,
16+
jest: true,
1517
},
1618
ignorePatterns: ['.eslintrc.js'],
1719
rules: {

.prettierrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"singleQuote": true,
55
"trailingComma": "all",
66
"arrowParens": "always",
7-
"printWidth": 150,
7+
"printWidth": 180,
88
"bracketSpacing": true
99
}

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{
2+
}

nest-cli.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"sourceRoot": "src",
55
"root": "src",
66
"compilerOptions": {
7+
"deleteOutDir": true,
78
"assets": [
89
"**/*.hbs",
910
"config/**/*",

package.json

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@libertech/mailrest",
2+
"name": "@libertech-fr/mailrest",
33
"version": "0.0.1",
44
"description": "An email retrieval system with a REST API built with NestJS in NodeJS",
55
"contributors": [
@@ -26,61 +26,69 @@
2626
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
2727
"start": "nest start",
2828
"generate": "nest g resource",
29+
"npm-check-updates": "npx npm-check-updates",
2930
"start:dev": "nest start --watch",
3031
"start:debug": "nest start --debug --watch",
3132
"start:prod": "node dist/main",
3233
"console": "node dist/console"
3334
},
3435
"dependencies": {
3536
"@nestjs-modules/ioredis": "^1.0.1",
36-
"@nestjs-modules/mailer": "^1.8.1",
37-
"@nestjs/common": "^9.0.11",
38-
"@nestjs/config": "^2.2.0",
39-
"@nestjs/core": "^9.0.11",
40-
"@nestjs/jwt": "^9.0.0",
41-
"@nestjs/mapped-types": "^1.2.0",
42-
"@nestjs/passport": "^9.0.0",
43-
"@nestjs/platform-express": "^9.0.11",
44-
"@nestjs/testing": "^9.2.1",
37+
"@nestjs-modules/mailer": "^1.9.1",
38+
"@nestjs/common": "^10.1.3",
39+
"@nestjs/config": "^3.0.0",
40+
"@nestjs/core": "^10.1.3",
41+
"@nestjs/jwt": "^10.1.0",
42+
"@nestjs/mapped-types": "^2.0.2",
43+
"@nestjs/passport": "^10.0.0",
44+
"@nestjs/platform-express": "^10.1.3",
45+
"@nestjs/testing": "^10.1.3",
4546
"class-transformer": "^0.5.1",
46-
"class-validator": "^0.13.2",
47-
"handlebars": "^4.7.7",
48-
"ioredis": "^4.0.0",
49-
"nest-commander": "^3.0.0",
50-
"nodemailer": "^6.8.0",
47+
"class-validator": "^0.14.0",
48+
"handlebars": "^4.7.8",
49+
"imapflow": "^1.0.136",
50+
"ioredis": "^5.3.2",
51+
"lru-cache": "^10.0.1",
52+
"mailparser": "^3.6.5",
53+
"nest-commander": "^3.11.1",
54+
"nodemailer": "^6.9.4",
5155
"passport": "^0.6.0",
52-
"passport-jwt": "^4.0.0",
56+
"passport-jwt": "^4.0.1",
57+
"radash": "^11.0.0",
5358
"reflect-metadata": "^0.1.13",
54-
"rxjs": "^7.2.0",
59+
"rxjs": "^7.8.1",
5560
"yaml": "^2.3.1"
5661
},
5762
"devDependencies": {
5863
"@babel/plugin-proposal-private-methods": "^7.18.6",
5964
"@compodoc/compodoc": "^1.1.21",
60-
"@nestjs/cli": "8.2.6",
61-
"@nestjs/schematics": "8.0.11",
62-
"@nestjs/swagger": "^7.1.6",
63-
"@types/express": "^4.17.13",
65+
"@nestjs/cli": "^10.1.12",
66+
"@nestjs/schematics": "10.0.2",
67+
"@nestjs/swagger": "^7.1.8",
68+
"@types/express": "^4.17.17",
69+
"@types/imapflow": "^1.0.13",
6470
"@types/ioredis": "^4.28.10",
65-
"@types/jest": "^29.2.4",
71+
"@types/jest": "^29.5.3",
72+
"@types/lru-cache": "^7.10.10",
73+
"@types/mailparser": "^3.4.0",
6674
"@types/multer": "^1.4.7",
67-
"@types/node": "^16.0.0",
75+
"@types/node": "^20.5.0",
6876
"@types/nodemailer": "^6.4.9",
69-
"@types/passport-jwt": "^3.0.6",
70-
"@typescript-eslint/eslint-plugin": "^4.28.2",
71-
"@typescript-eslint/parser": "^4.28.2",
72-
"eslint": "^7.30.0",
73-
"eslint-config-prettier": "^8.3.0",
74-
"eslint-plugin-prettier": "^3.4.0",
75-
"prettier": "^2.3.2",
76-
"rimraf": "^3.0.2",
77+
"@types/passport-jwt": "^3.0.9",
78+
"@typescript-eslint/eslint-plugin": "^6.4.0",
79+
"@typescript-eslint/parser": "^6.4.0",
80+
"eslint": "^8.47.0",
81+
"eslint-config-prettier": "^9.0.0",
82+
"eslint-plugin-prettier": "^5.0.0",
83+
"prettier": "^3.0.2",
84+
"rimraf": "^5.0.1",
7785
"swagger-themes": "^1.2.30",
78-
"ts-loader": "^9.2.3",
79-
"ts-node": "^10.0.0",
80-
"tsconfig-paths": "^3.10.1",
86+
"ts-loader": "^9.4.4",
87+
"ts-node": "^10.9.1",
88+
"tsconfig-paths": "^4.2.0",
8189
"types-package-json": "^2.0.39",
82-
"typescript": "^4.9.5",
83-
"webpack": "^5.64.4"
90+
"typescript": "^5.1.6",
91+
"webpack": "^5.88.2"
8492
},
8593
"peerDependencies": {
8694
"express": "^4"
Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,78 @@
1-
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
1+
import { BadRequestException, Body, Controller, Delete, Get, HttpException, Logger, Param, Patch, Post, Res, Sse } from '@nestjs/common'
2+
import { ModuleRef } from '@nestjs/core'
3+
import { Response } from 'express'
4+
import { AbstractController } from '~/abstract.controller'
5+
import { FSWatcher, watch } from 'fs'
6+
import { Observable } from 'rxjs'
27
import { AccountsService } from './accounts.service'
3-
import { CreateAccountDto } from './dto/create-account.dto'
4-
import { UpdateAccountDto } from './dto/update-account.dto'
8+
import { ACCOUNTS_FILE_PATH, AccountsMetadataV1, readAccountsFile } from './accounts.setup'
59

610
@Controller('accounts')
7-
export class AccountsController {
8-
constructor(private readonly accountsService: AccountsService) {}
11+
export class AccountsController extends AbstractController {
12+
public constructor(protected readonly moduleRef: ModuleRef, protected readonly service: AccountsService) {
13+
super(moduleRef)
14+
}
915

10-
@Post()
11-
create(@Body() createAccountDto: CreateAccountDto) {
12-
return this.accountsService.create(createAccountDto)
16+
@Sse('changes')
17+
public async sse(@Res() res: Response): Promise<Observable<MessageEvent>> {
18+
let subscriber: FSWatcher
19+
res.socket.on('close', () => {
20+
if (subscriber) {
21+
subscriber.close()
22+
Logger.debug(`Observer close connection from SSE<changes>`, this.constructor.name)
23+
}
24+
})
25+
return new Observable((observer) => {
26+
subscriber = watch(ACCOUNTS_FILE_PATH, async () => {
27+
const data = await readAccountsFile()
28+
observer.next({ data } as MessageEvent)
29+
})
30+
})
1331
}
1432

1533
@Get()
16-
findAll() {
17-
return this.accountsService.findAll()
34+
public async search(@Res() res: Response): Promise<Response> {
35+
const data = await this.service.search()
36+
return res.json(data)
37+
}
38+
39+
@Post()
40+
public async create(@Res() res: Response, @Body() data: AccountsMetadataV1): Promise<Response> {
41+
try {
42+
const account = await this.service.create(data)
43+
return res.json({ account })
44+
} catch (error) {
45+
throw error instanceof HttpException ? error : new BadRequestException(error.message, error)
46+
}
1847
}
1948

20-
@Get(':id')
21-
findOne(@Param('id') id: string) {
22-
return this.accountsService.findOne(+id)
49+
@Get(':account([\\w-.]+)')
50+
public async read(@Res() res: Response, @Param('account') id: string): Promise<Response> {
51+
try {
52+
const data = await this.service.read(id) // TODO: 404
53+
return res.json({ data })
54+
} catch (error) {
55+
throw error instanceof HttpException ? error : new BadRequestException(error.message, error)
56+
}
2357
}
2458

25-
@Patch(':id')
26-
update(@Param('id') id: string, @Body() updateAccountDto: UpdateAccountDto) {
27-
return this.accountsService.update(+id, updateAccountDto)
59+
@Patch(':account([\\w-.]+)')
60+
public async update(@Res() res: Response, @Param('account') id: string, @Body() data: AccountsMetadataV1): Promise<Response> {
61+
try {
62+
const account = await this.service.update(id, data) // TODO: 404
63+
return res.json({ account })
64+
} catch (error) {
65+
throw error instanceof HttpException ? error : new BadRequestException(error.message, error)
66+
}
2867
}
2968

30-
@Delete(':id')
31-
remove(@Param('id') id: string) {
32-
return this.accountsService.remove(+id)
69+
@Delete(':account([\\w-.]+)')
70+
public async delete(@Res() res: Response, @Param('account') id: string): Promise<Response> {
71+
try {
72+
const account = await this.service.delete(id) // TODO: 404
73+
return res.json({ account })
74+
} catch (error) {
75+
throw error instanceof HttpException ? error : new BadRequestException(error.message, error)
76+
}
3377
}
3478
}

src/accounts/accounts.service.ts

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,68 @@
1-
import { Injectable } from '@nestjs/common'
2-
import { CreateAccountDto } from './dto/create-account.dto'
3-
import { UpdateAccountDto } from './dto/update-account.dto'
1+
import { Injectable, Logger, NotFoundException } from '@nestjs/common'
2+
import { ModuleRef } from '@nestjs/core'
3+
import { ImapFlow } from 'imapflow'
4+
import { find, pick } from 'lodash'
5+
import { LRUCache } from 'lru-cache'
6+
import { AbstractService } from '~/abstract.service'
7+
import { InjectImapflow } from '~/imapflow/imapflow.decorators'
8+
import { AccountsFileV1, AccountsMetadataV1, readAccountsFile, writeAccountsFile } from './accounts.setup'
49

510
@Injectable()
6-
export class AccountsService {
7-
create(createAccountDto: CreateAccountDto) {
8-
return 'This action adds a new account'
11+
export class AccountsService extends AbstractService {
12+
protected cache: LRUCache<string, AccountsFileV1>
13+
protected logger: Logger = new Logger(AccountsService.name)
14+
public constructor(protected readonly moduleRef: ModuleRef, @InjectImapflow() protected imapflow: Map<string, ImapFlow>) {
15+
super({ moduleRef })
16+
this.cache = new LRUCache({
17+
max: 100,
18+
maxSize: 1000,
19+
sizeCalculation: () => 1,
20+
ttl: 1000 * 60 * 5,
21+
})
922
}
1023

11-
findAll() {
12-
return `This action returns all accounts`
24+
public async search(): Promise<Partial<AccountsFileV1>> {
25+
const data = await readAccountsFile(this.cache)
26+
return pick(data, ['accounts'])
1327
}
1428

15-
findOne(id: number) {
16-
return `This action returns a #${id} account`
29+
public async create(data: Partial<AccountsMetadataV1>): Promise<Partial<AccountsMetadataV1>> {
30+
const accounts = await readAccountsFile(this.cache)
31+
const account = new AccountsMetadataV1()
32+
Object.assign(account, data)
33+
accounts.accounts.push(account)
34+
await writeAccountsFile(accounts, this.cache)
35+
return account
1736
}
1837

19-
update(id: number, updateAccountDto: UpdateAccountDto) {
20-
return `This action updates a #${id} account`
38+
public async read(id: string): Promise<Partial<AccountsMetadataV1>> {
39+
const data = await readAccountsFile(this.cache)
40+
const account = find(data.accounts, { id })
41+
if (!account) {
42+
throw new NotFoundException(`Account not found: ${id}`)
43+
}
44+
return account
2145
}
2246

23-
remove(id: number) {
24-
return `This action removes a #${id} account`
47+
public async update(id: string, data: Partial<AccountsMetadataV1>): Promise<Partial<AccountsMetadataV1>> {
48+
const accounts = await readAccountsFile(this.cache)
49+
const account: AccountsMetadataV1 = find(accounts.accounts, { id })
50+
if (!account) {
51+
throw new NotFoundException(`Account not found: ${id}`)
52+
}
53+
Object.assign(account, data)
54+
await writeAccountsFile(accounts, this.cache)
55+
return account
56+
}
57+
58+
public async delete(id: string): Promise<Partial<AccountsMetadataV1>> {
59+
const accounts = await readAccountsFile(this.cache)
60+
const account = find(accounts.accounts, { id })
61+
if (!account) {
62+
throw new NotFoundException(`Account not found: ${id}`)
63+
}
64+
accounts.accounts = accounts.accounts.filter((a) => a.id !== id)
65+
await writeAccountsFile(accounts, this.cache)
66+
return account
2567
}
2668
}

0 commit comments

Comments
 (0)