Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 107: Refactor Backend Structure #141

Merged
merged 1 commit into from
May 30, 2024
Merged
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ npm run postinstall
#### 4. Build Local Services
Generate prisma-client for database-access:
```bash
cd apps/server-asset-sg/
npm run prisma -- generate
```

Expand Down Expand Up @@ -98,7 +97,7 @@ To do so, use the following commands.
Be aware that you need to manually insert the `{DB_*}` values beforehand.
```bash
cd development
docker compose exec db sh -c 'pg_dump --dbname=postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_DATABASE} --data-only --exclude-table asset_user _prisma_migrations -n public > /dump.sql'
docker compose exec db sh -c 'pg_dump --dbname=postgresql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_DATABASE} --data-only --exclude-table asset_user --exclude-table _prisma_migrations -n public > /dump.sql'
```
> The export will output warnings related to circular foreign-key constraints.
> These can be safely ignored.
Expand Down
Empty file.
2 changes: 1 addition & 1 deletion apps/server-asset-sg/http-client.env.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"dev": {
"host": "http://localhost:3333",
"user": "daniel.vonatzigen@ebp.ch"
"user": "[email protected].ch"
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
output = "../../../../../node_modules/.prisma/client"
output = "../../../node_modules/.prisma/client"
previewFeatures = ["multiSchema", "views"]
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
22 changes: 7 additions & 15 deletions apps/server-asset-sg/project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "server-asset-sg",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/server-asset-sg/src",
"sourceRoot": "apps/server-asset-sg",
"projectType": "application",
"targets": {
"build": {
Expand All @@ -10,7 +10,7 @@
"parallel": false,
"commands": [
{
"command": "npx nx run server-asset-sg:gen-prisma-client"
"command": "npm run prisma -- generate"
},
{
"command": "npx nx run server-asset-sg:build-app:production"
Expand All @@ -22,10 +22,10 @@
"command": "npx shx mkdir dist/apps/server-asset-sg/prisma"
},
{
"command": "npx shx cp ./apps/server-asset-sg/src/app/prisma/schema.prisma dist/apps/server-asset-sg/prisma/"
"command": "npx shx cp ./apps/server-asset-sg/prisma/schema.prisma dist/apps/server-asset-sg/prisma/"
},
{
"command": "npx shx cp -R ./apps/server-asset-sg/src/app/prisma/migrations dist/apps/server-asset-sg/prisma/"
"command": "npx shx cp -R ./apps/server-asset-sg/prisma/migrations dist/apps/server-asset-sg/prisma/"
},
{
"command": "npx shx sed -i \"s/(\\.\\.\\/)*node_modules/\\.\\/node_modules/g\" dist/apps/server-asset-sg/prisma/schema.prisma"
Expand All @@ -45,7 +45,7 @@
"main": "apps/server-asset-sg/src/main.ts",
"tsConfig": "apps/server-asset-sg/tsconfig.app.json",
"assets": [
"apps/server-asset-sg/src/assets"
"apps/server-asset-sg/static"
],
"externalDependencies": "all",
"generatePackageJson": true
Expand All @@ -57,8 +57,8 @@
"inspect": false,
"fileReplacements": [
{
"replace": "apps/server-asset-sg/src/environments/environment.ts",
"with": "apps/server-asset-sg/src/environments/environment.prod.ts"
"replace": "apps/server-asset-sg/environments/environment.ts",
"with": "apps/server-asset-sg/environments/environment.prod.ts"
}
]
}
Expand Down Expand Up @@ -96,14 +96,6 @@
"passWithNoTests": true,
"codeCoverage": true
}
},
"gen-prisma-client": {
"executor": "nx:run-commands",
"outputs": [],
"options": {
"command": "npx prisma generate --schema ./src/app/prisma/schema.prisma",
"cwd": "apps/server-asset-sg"
}
}
},
"tags": []
Expand Down
280 changes: 280 additions & 0 deletions apps/server-asset-sg/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpException,
HttpStatus,
Param,
ParseIntPipe,
Patch,
Post,
Put,
Query,
Redirect,
Req,
UploadedFile,
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { plainToInstance } from 'class-transformer';
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';

import { DT, decodeError, unknownToError } from '@asset-sg/core';
import { AssetByTitle, PatchAsset, PatchContact, isEditor } from '@asset-sg/shared';
import { User as AssetUser } from '@asset-sg/shared';

import { RequireRole } from '@/core/decorators/require-role.decorator';
import { AssetEditRepo } from '@/features/asset-old/asset-edit.repo';
import { AssetEditDetail, AssetEditService } from '@/features/asset-old/asset-edit.service';
import { AssetSearchService } from '@/features/assets/search/asset-search.service';
import { Contact, ContactData, ContactDataBoundary, ContactId } from '@/features/contacts/contact.model';
import { ContactRepo } from '@/features/contacts/contact.repo';
import { ContactsController } from '@/features/contacts/contacts.controller';
import { Role, User, UserDataBoundary, UserId } from '@/features/users/user.model';
import { UserRepo } from '@/features/users/user.repo';
import { UsersController } from '@/features/users/users.controller';
import { JwtRequest } from '@/models/jwt-request';
import { permissionDeniedError } from '@/utils/errors';

@Controller('/')
export class AppController {
constructor(
private readonly assetEditRepo: AssetEditRepo,
private readonly assetEditService: AssetEditService,
private readonly userRepo: UserRepo,
private readonly contactRepo: ContactRepo,
private readonly assetSearchService: AssetSearchService,
) {}

@Get('/oauth-config/config')
getConfig() {
return {
oauth_issuer: process.env.OAUTH_ISSUER,
oauth_clientId: process.env.OAUTH_CLIENT_ID,
oauth_scope: process.env.OAUTH_SCOPE,
oauth_responseType: process.env.OAUTH_RESPONSE_TYPE,
oauth_showDebugInformation: !!process.env.OAUTH_SHOW_DEBUG_INFORMATION,
oauth_tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT,
};
}

/**
* @deprecated
*/
@Get('/user')
@Redirect('users/current', 301)
getUser() {
// deprecated
}

/**
* @deprecated
*/
@Get('/user/favourite')
@Redirect('../users/current/favorites', 301)
async getFavourites() {
// deprecated
}

/**
* @deprecated
*/
@Get('/admin/user')
@Redirect('../users', 301)
getUsers() {
// deprecated
}

/**
* @deprecated
*/
@Patch('/admin/user/:id')
@RequireRole(Role.Admin)
updateUser(
@Param('id') id: UserId,
@Body(new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true }))
data: UserDataBoundary,
): Promise<User> {
return new UsersController(this.userRepo).update(id, data);
}

/**
* @deprecated
*/
@Delete('/admin/user/:id')
@RequireRole(Role.Admin)
@HttpCode(HttpStatus.NO_CONTENT)
async deleteUser(@Param('id') id: UserId): Promise<void> {
await new UsersController(this.userRepo).delete(id);
}

/**
* @deprecated
*/
@Put('/contact-edit')
@RequireRole(Role.Editor)
@HttpCode(HttpStatus.CREATED)
async createContact(@Body() patch: PatchContact) {
const data: ContactData = patch;
const boundary = plainToInstance(ContactDataBoundary, data);
return new ContactsController(this.contactRepo).create(boundary);
}

/**
* @deprecated
*/
@Patch('/contact-edit/:id')
@RequireRole(Role.Editor)
updateContact(@Param('id', ParseIntPipe) id: ContactId, patch: PatchContact): Promise<Contact> {
const data: ContactData = patch;
const boundary = plainToInstance(ContactDataBoundary, data);
return new ContactsController(this.contactRepo).update(id, boundary);
}

/**
* @deprecated
*/
@Get('/asset-edit/search')
async searchAssetsByTitle(@Query('title') title: string): Promise<AssetByTitle[]> {
try {
return await this.assetSearchService.searchByTitle(title);
} catch (e) {
throw new HttpException(unknownToError(e).message, 500);
}
}

/**
* @deprecated
*/
@Get('/asset-edit/:assetId')
async getAsset(@Param('assetId') assetId: string): Promise<unknown> {
const id = parseInt(assetId);
if (isNaN(id)) {
throw new HttpException('Resource not found', 404);
}
const asset = await this.assetEditRepo.find(id);
if (asset === null) {
throw new HttpException('Resource not found', 404);
}
return AssetEditDetail.encode(asset);
}

/**
* @deprecated
*/
@Put('/asset-edit')
async createAsset(@Req() req: JwtRequest, @Body() patchAsset: PatchAsset) {
const e = await pipe(
TE.of(req.user as unknown as AssetUser),
TE.filterOrElseW(
user => isEditor(user),
() => permissionDeniedError('Not an editor'),
),
TE.bindTo('user'),
TE.bindW('patchAsset', () => TE.fromEither(pipe(PatchAsset.decode(patchAsset), E.mapLeft(decodeError)))),
TE.chainW(({ patchAsset, user }) => this.assetEditService.createAsset(user, patchAsset)),
)();
if (E.isLeft(e)) {
console.error(e.left);
// if (e.left._tag === 'decodeError') {
// throw new HttpException(e.left.message, 400);
// }
throw new HttpException(e.left.message, 500);
}
return e.right;
}

/**
* @deprecated
*/
@Patch('/asset-edit/:assetId')
async updateAsset(@Req() req: JwtRequest, @Param('assetId') id: string, @Body() patchAsset: PatchAsset) {
const e = await pipe(
TE.of(req.user as unknown as AssetUser),
TE.filterOrElseW(
user => isEditor(user),
() => permissionDeniedError('Not an editor'),
),
TE.bindTo('user'),
TE.bindW('id', () => TE.fromEither(pipe(DT.IntFromString.decode(id), E.mapLeft(decodeError)))),
TE.bindW('patchAsset', () => TE.fromEither(pipe(PatchAsset.decode(patchAsset), E.mapLeft(decodeError)))),
TE.chainW(({ id, patchAsset, user }) => this.assetEditService.updateAsset(user, id, patchAsset)),
)();
if (E.isLeft(e)) {
console.error(e.left);
// if (e.left._tag === 'decodeError') {
// throw new HttpException(e.left.message, 400);
// }
throw new HttpException(e.left.message, 500);
}
return e.right;
}

/**
* @deprecated
*/
@Post('/asset-edit/:assetId/file')
@UseInterceptors(FileInterceptor('file', { limits: { fileSize: 250 * 1024 * 1024 } }))
async uploadAssetFile(
@Req() req: JwtRequest,
@Param('assetId') id: string,
@UploadedFile() file: { originalname: string; buffer: Buffer; size: number; mimetype: string },
) {
const e = await pipe(
TE.of(req.user as unknown as AssetUser),
TE.filterOrElseW(
user => isEditor(user),
() => permissionDeniedError('Not an editor'),
),
TE.bindTo('user'),
TE.bindW('id', () => TE.fromEither(pipe(DT.IntFromString.decode(id), E.mapLeft(decodeError)))),
TE.chainW(({ user, id }) =>
this.assetEditService.uploadFile(user, id, {
name: file.originalname,
buffer: file.buffer,
size: file.size,
mimetype: file.mimetype,
}),
),
)();
if (E.isLeft(e)) {
console.error(e.left);
// if (e.left._tag === 'decodeError') {
// throw new HttpException(e.left.message, 400);
// }
throw new HttpException(e.left.message, 500);
}
return e.right;
}

/**
* @deprecated
*/
@Delete('/asset-edit/:assetId/file/:fileId')
async deleteAssetFile(@Req() req: JwtRequest, @Param('assetId') assetId: string, @Param('fileId') fileId: string) {
const e = await pipe(
TE.of(req.user as unknown as AssetUser),
TE.filterOrElseW(
user => isEditor(user),
() => permissionDeniedError('Not an editor'),
),
TE.bindTo('user'),
TE.bindW('assetId', () => TE.fromEither(pipe(DT.IntFromString.decode(assetId), E.mapLeft(decodeError)))),
TE.bindW('fileId', () => TE.fromEither(pipe(DT.IntFromString.decode(fileId), E.mapLeft(decodeError)))),
TE.chainW(({ user, assetId, fileId }) => this.assetEditService.deleteFile(user, assetId, fileId)),
)();
if (E.isLeft(e)) {
console.error(e.left);
// if (e.left._tag === 'decodeError') {
// throw new HttpException(e.left.message, 400);
// }
throw new HttpException(e.left.message, 500);
}
return e.right;
}
}
Loading
Loading