Skip to content

Commit

Permalink
Move backend docu (#16)
Browse files Browse the repository at this point in the history
* copy over the most important files from server

* document code style guidelines

* add various code style rules from confluence

* some corrections for things that are no longer up to date
  • Loading branch information
Metauriel authored Nov 22, 2023
1 parent d5f6976 commit 93eddc8
Show file tree
Hide file tree
Showing 19 changed files with 1,234 additions and 0 deletions.
45 changes: 45 additions & 0 deletions docs/schulcloud-server/Coding-Guidelines/access-legacy-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Access legacy Code

## Access Feathers Service from NestJS

The `FeathersModule` provides functionality to access legacy code. In order to introduce strong typing, it is necessary to write an adapter service for the feathers service you want to access. Place this adapter within your module, and use the `FeathersServiceProvider` to access the service you need

```TypeScript
// inside your module, import the FeathersModule
@Module({
imports: [FeathersModule],
providers: [MyFeathersServiceAdapter],
})
export class MyModule {}

// inside of your service, inject the FeathersServiceProvider
@Injectable()
export class MyFeathersServiceAdapter {
constructor(private feathersServiceProvider: FeathersServiceProvider) {}

async get(): Promise<string[]> {
const service = this.feathersServiceProvider.getService(`path`);
const result = await service.get(...)

return result;
}

```
## Access NestJS injectable from Feathers
To access a NestJS service from a legacy Feathers service you need to make the NestJS service known to the Feathers service-collection in `main.ts`.
This possibility should not be used for new features in Feathers, but it can help if you want to refactor a Feathers service to NestJs although other Feathers services depend on it.
```TypeScript
// main.ts
async function bootstrap() {
// (...)
feathersExpress.services['nest-rocket-chat'] = nestApp.get(RocketChatService);
// (...)
}
```
Afterwards you can access it the same way as you access other Feathers services with
`app.service('/nest-rocket-chat');`
158 changes: 158 additions & 0 deletions docs/schulcloud-server/Coding-Guidelines/code-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Code Style

## Function

### Naming functions

The name of a function should clearly communicate what it does. There should be no need to read the implementation of a function to understand what it does.

There are a few keywords that we use with specific meaning:

#### "is..."

`isTask()`, `isPublished()`, `isAuthenticated()`, `isValid()`

A function with the prefix "is..." is checking wether the input belongs to a certain (sub)class, or fulfils a specific criteria.

The function should return a boolean, and have no sideeffects.

#### "check..."

`checkPermission()`, `checkInputIsValid()`

A function with the prefix "check..." is checking the condition described in its name, throwing an error if it does not apply.

#### "has..."

`hasPermission()`,

similar to "is...", the prefix "has..." means that the function is checking a condition, and returns a boolean. It does NOT throw an error.

### Avoid direct returns of computations

avoid directly returning the result of some computation. Instead, use a variable to give the result of the computation a name.

Exceptions can be made when the result of the computation is already clear from the function name, and the function is sufficiently simple to not occlude its meaning.

```typescript
public doSomething(): FileRecordParams[] {
// ... more logic here
const fileRecordParams = fileRecords.map((fileRecord) => Mapper.toParams(fileRecord));
// hint: this empty line can be increase the readability
return fileRecordParams;
}

public getName(): String {
return this.name;
}

public getInfo(): IInfo {
// ... more logic here
return { name, parentId, parentType }; // but if the return include many keys, please put it first to a const
}
```

### avoid directly passing function results as parameters

```typescript
function badExample(): void {
doSomething(this.extractFromParams(params), this.createNewConfiguration());
}

function goodExample(): void {
const neededParams = this.extractFromParams(params);
const configuration = this.createNewConfiguration();
doSomething(neededParams, configuration);
}
```

### explicit return type

```typescript
public doSomething(): FileRecords[] {
//...
return fileRecords
}
```

## Interfaces

### Avoid the "I" for interfaces

In most cases, it should not matter to the reader/external code wether something is an interface or an implementation. Only prefix the "I" when necessary, or when its specifically important to the external code to know that its an interface, for example when external code is required to implement the interface.

```Typescript
interface CourseRepo {
getById(id): Course
// ...
}

class InMemoryCourseRepo implements CourseRepo {
getById(id): Course {
return new Course()
}
}
```

## Classes

### Order of declarations

Classes are declared in the following order:

1. properties
2. constructor
3. methods

Example:

```Typescript
export class Course {
// 1. properties
name: string;

// more properties...

// 2. constructor
constructor(props: { name: string }) {
// ...
}

// 3. methods
public getShortTitle(): string {
// ...
}

// more methods...
}
```

### Naming classes

Classes should be named in CamelCase. They should have a Suffix with the kind of Object they represent, and from the beginning to the end go from specific to general.

- CourseController
- CourseCreateBodyParam
- CourseCreateQueryParam
- CourseCreateResponse
- CourseDtoMapper
- CourseUc
- CourseAuthorisationDto
- CourseDo
- CourseService
- CourseRepo
- CourseEntity
- CourseEntityMapper

## Do NOT use JsDoc

You should always try to write code in a way that does not require further explanation to understand. Use proper names for functions and variables, and extract code and partial results into functions or variables in order to name them. If you feel like a function needs a JsDoc, treat that as a codesmell, and try to rewrite the code in a way that is more self-explanatory.

## Do use empty lines

empty lines help to structure code. Use them wherever you want to seperate parts of a function from each other (and think about further extracting functions). Common uses include:

- before return statement
- before and after an if/else statement
- between test sections (arrange, act, assert)
- between "it()" statements in tests
65 changes: 65 additions & 0 deletions docs/schulcloud-server/Coding-Guidelines/controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Controller

A modules api layer is defined within of [controllers](https://docs.nestjs.com/controllers).

The main responsibilities of a controller is to define the REST API interface as openAPI specification and map DTO's to match the logic layers interfaces.

```TypeScript
@ApiOperation({ summary: 'some descriptive information that will show up in the API documentation' })
@ApiResponse({ status: 200, type: BoardResponse })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@Post()
async create(@CurrentUser() currentUser: ICurrentUser, @Body() params: CreateNewsParams): Promise<NewsResponse> {
const news = await this.newsUc.create(
currentUser.userId,
currentUser.schoolId,
NewsMapper.mapCreateNewsToDomain(params)
);
const dto = NewsMapper.mapToResponse(news);

return dto;
}
```

## JWT-Authentication

For **authentication**, use [guards](https://docs.nestjs.com/guards) like JwtAuthGuard. It can be applied to a whole controller or a single controller method only. Then, the authenticated user can be injected using the `@CurrentUser()` decorator.

## Validation

Global settings of the core-module ensure **request/response validation** against the api definition. Simple input types might additionally use a custom [pipe](https://docs.nestjs.com/pipes) while for complex types injected as query/body are validated by default when parsed as DTO class.

## DTOs

All data that leaves or enters the system has to be defined and typed using DTOs.

```typescript
export class CreateNewsParams {
@IsString()
@SanitizeHtml()
@ApiProperty({
description: 'Title of the News entity',
})
title!: string;

// ...
}
```

### DTO File naming

Complex input DTOs are defined like [create-news].params.ts (class-name: CreateNewsParams).

When DTO's are shared between multiple modules, locate them in the layer-related shared folder.

> **Security:** When exporting data, internal entities must be mapped to a response DTO class named like [news].response.dto. The mapping ensures which data of internal entities are exported.
### openAPI specification

Defining the request/response DTOs in a controller will define the openAPI specification automatically. Additional [validation rules](https://docs.nestjs.com/techniques/validation) and [openAPI definitions](https://docs.nestjs.com/openapi/decorators) can be added using decorators. For simplification, openAPI decorators should define a type and if a property is required, while additional decorators can be used from class-validator to validate content.

### Mapping

You should define a mapper to easily create dtos from the uc responses, and the datatypes expected by ucs from params.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Domain Object Validation

If you need to validate a domain object, please write an independent class, so that the domain object itself, its repo and services can reuse it.

Eric Evans suggests using the specification pattern.
A specification fulfills the following interface:

```typescript
public interface Specification<T> {
boolean isSatisfiedBy(T t);
}
```

A specification checks if a domain object fulfills the conditions of the specification.

A specification can simply specify that a domain object is valid. E.g. a `Task` has an owner and a description.
A specification can specify more complex and specialized conditions. E.g. `Task` where every student assigned to the task's course has handed in a submission.

The specification pattern in its full extend describes how to use logic operators to combine multiple specifications into combined specifications as well. Please don't build this as long as you don't need it. YAGNI.
[More about full specification pattern](https://medium.com/@pawel_klimek/domain-driven-design-specification-pattern-82867540305c)
74 changes: 74 additions & 0 deletions docs/schulcloud-server/Coding-Guidelines/exception-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Exception Handling

![exception hierarchy](./img/exception-hierarchy.svg)

We separate our business exceptions from technical exceptions. While for technical exceptions, we use the predefined HTTPExceptions from NestJS, business exceptions inherit from abstract BusinessException.

By default, implementations of BusinessException must define

```JSON
code: 500
type: "CUSTOM_ERROR_TYPE",
title: "Custom Error Type",
message: "Human readable details",
// additional: optionalData
```

There is a GlobalErrorFilter provided to handle exceptions, which cares about the response format of exceptions and logging. It overrides the default NestJS APP_FILTER in the core/error-module.

In client applications, for technical errors, evaluate the http-error-code, then for business exceptions, the type can be used as identifier and additional data can be evaluated.

For business errors we use 409/conflict as default to clearly have all business errors with one error code identified.

> Sample: For API validation errors, 400/Bad Request will be extended with `validationError: ValidationError[{ field: string, error: string }]` and a custom type `API_VALIDATION_ERROR`.
Pipes can be used as input validation. To get errors reported in the correct format, they can define a custom exception factory when they should produce api validation error or other exceptions, handled by clients.

## Chaining errors with the `cause` property

If you catch an error and throw a new one, put the original error in the [`cause` property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) of the new error. See example:

```typescript
try {
someMethod();
} catch(error) {
throw new ForbiddenException('some message', { cause: error });
}
```

## Loggable exceptions

If you want the error log to contain more information than just the exception message, use or create an exception which implements the `Loggable` interface. Don't put data directly in the exception message!

A loggable exception should extend the respective [Built-in HTTP exception](https://docs.nestjs.com/exception-filters#built-in-http-exceptions) from NestJS. For the name just put in "Loggable" before the word "Exception", e.g. "BadRequestLoggableException". Except for logging a loggable exception behaves like any other exception, specifically the error response is not affected by this.

See example below.

```TypeScript
export class UnauthorizedLoggableException extends UnauthorizedException implements Loggable {
constructor(private readonly username: string, private readonly systemId?: string) {
super();
}

getLogMessage(): ErrorLogMessage {
const message = {
type: 'UNAUTHORIZED_EXCEPTION',
stack: this.stack,
data: {
userName: this.username,
systemId: this.systemId,
},
};

return message;
}
}
```

```TypeScript
export class YourService {
public sampleServiceMethod(username, systemId) {
throw new UnauthorizedLoggableException(username, systemId);
}
}
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 93eddc8

Please sign in to comment.