Skip to content
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
71 changes: 71 additions & 0 deletions docs/caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Caching
Caching in Zibri is based on first creating a cache class and then using that class with the help of decorators.

## Defining a cache
Zibri provides caches for all combinations of write and read strategies.

```ts
import { Cache, CacheServiceInterface, HtmlResponse, Inject, InMemoryCacheStore, LoggerInterface, MetricsServiceInterface, WriteThroughReadThroughCache, ZIBRI_DI_TOKENS } from 'zibri';

@Cache()
export class StaticPagesCache extends WriteThroughReadThroughCache<string, HtmlResponse, 'StaticPagesCache'> {
constructor(
@Inject(ZIBRI_DI_TOKENS.LOGGER)
protected readonly logger: LoggerInterface,
@Inject(ZIBRI_DI_TOKENS.CACHE_SERVICE)
protected readonly cacheService: CacheServiceInterface,
@Inject(ZIBRI_DI_TOKENS.METRICS_SERVICE)
protected readonly metricsService: MetricsServiceInterface
) {
super('StaticPagesCache', new InMemoryCacheStore(), []);
}
}
```

## Using a cache
As you can see, the above uses a really simple in memory cache store. But you could also provide your own here, based eg. on redis.

To use your cache, you can use the provided decorators on whichever method that should be cached:

```ts
import { Cached, Get, GlobalRegistry, HtmlResponse, PreactUtilities, Response } from 'zibri';

import { HomePage } '../home';

@Cached(StaticPagesCache, () => 'index')
@Response.html()
@Get()
async index(): Promise<HtmlResponse> {
const html: string = await PreactUtilities.renderPage(HomePage, { appName: GlobalRegistry.getAppData('name') ?? '' });
return HtmlResponse.fromString(html);
}
```

Other decorators include:
- CacheDelete
- CacheInvalidate
- CacheWrite

## Multi Tier Caches
Multi Tier Caches are natively built into Zibri. They provide a clean way to define:<br>
Use the in memory cache if available. If it's not available: Look it up inside the redis cache. If that's also not available: Actually run the underlying method and fill all caches.

```ts
@Cache()
class TestMultiTierCache extends MultiTierCache<string, number, [FastCache, SlowCache]> {
constructor(
@Inject(ZIBRI_DI_TOKENS.LOGGER)
protected readonly logger: LoggerInterface,
@Inject(ZIBRI_DI_TOKENS.METRICS_SERVICE)
protected readonly metricsService: MetricsServiceInterface,
@Inject(ZIBRI_DI_TOKENS.CACHE_SERVICE)
protected readonly cacheService: CacheServiceInterface,
@Inject(FastCache)
fast: FastCache,
@Inject(SlowCache)
slow: SlowCache
) {
super('TestMulti', [fast, slow]);
}
}
```
4 changes: 2 additions & 2 deletions docs/creating-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class User {
id!: string;

@Property.string({ required: false })
name?: string;
name?: string | null;

@Property.string({ format: 'email' })
email!: string;
Expand Down Expand Up @@ -107,7 +107,7 @@ class User {
id!: string;

@Property.string({ required: false })
name?: string;
name?: string | null;

@Property.string({ format: 'email' })
email!: string;
Expand Down
6 changes: 3 additions & 3 deletions docs/cron.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ To define a job it needs to extend the `CronJob` class. Below is a simple exampl

```ts
// src/cron/status.cron-job.ts
import { CronJob, inject, Injectable, LoggerInterface, ZIBRI_DI_TOKENS, InitialCronConfig } from 'zibri';
import { CronExpression, CronJob, inject, Injectable, ZIBRI_DI_TOKENS, InitialCronConfig } from 'zibri';

@Injectable()
export class StatusCronJob extends CronJob {
readonly initialConfig: InitialCronConfig = {
name: 'Status',
cron: '* * * * * *',
cron: CronExpression.every(1, 'seconds').build(),
active: false
};

async onTick(): Promise<void> {
await inject<LoggerInterface>(ZIBRI_DI_TOKENS.LOGGER).info(`is running ${this.name}`);
await inject(ZIBRI_DI_TOKENS.LOGGER).info(`is running ${this.name}`);
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/data-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { Test } from '../../models';

@DataSource()
export class DbDataSource extends PostgresDataSource {
options: PostgresOptions = {
options: OmitStrict<PostgresOptions, 'type' | 'entities'> = {
host: 'localhost',
port: 5432,
username: 'postgres',
Expand Down
5 changes: 2 additions & 3 deletions docs/di.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ Let's say that you want for example to replace the default error handler with th

```ts
// src/my-error-handler.ts
import { NextFunction } from 'express';
import { GlobalErrorHandler, HttpRequest, HttpResponse } from 'zibri';
import { GlobalErrorHandler } from 'zibri';

export const myErrorHandler: GlobalErrorHandler = async (error: unknown, req: HttpRequest, res: HttpResponse, next: NextFunction) => {
export const myErrorHandler: GlobalErrorHandler = async (error, req, res, next) => {
// ...your custom logic
}
```
Expand Down
22 changes: 22 additions & 0 deletions docs/encryption-and-hashing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Encryption & Hashing
Encryption and hashing of entity properties can be easily defined via the `@Property.string` decorator:

```ts
// Alternatively you can also provide an options object to encryption instead of the boolean flag.
@Property.string({ encryption: true })
encryptedValue!: string;

// Alternatively you can also provide an options object to hash instead of the boolean flag.
@Property.string({ hash: true })
hashedValue!: HashString;
```

This will encrypt/hash any values that are stored in a data source via a repository.

## Decryption
When using the `@Property` decorator, encrypted values are automatically decrypted when read from the datasource. This can be configured when using the options object instead of the simple boolean flag. This configuration is pretty flexible with a callback, it allows for example to decrypt based on the current users role. So you could specify that Admins are allowed to decrypt, but normal Users aren't.

Alternatively and if sufficient for your use case, the [exclude functionality](./excluding-properties.md) can be used for this as well or in addition.

## Manual encryption/hashing
To manually encrypt or hash a value Zibri provides the `ZIBRI_DI_TOKENS.ENCRYPTION_SERVICE` and `ZIBRI_DI_TOKENS.HASH_SERVICE` injection tokens, as well as `.HASH_STRATEGIES` and `ENCRYPTION_STRATEGIES` if you simply want a different algorithm. By default bcrypt and aes-256-gcm are used.
17 changes: 17 additions & 0 deletions docs/excluding-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Excluding properties
At some point in time you will probably stumble across the problem of having some sensitive data like eg. a password where you want to make sure that it will never leave the server in some form. Be it via http, websocket connection or inside of logs.

To support that use case, Zibri provides a exclude flag:

```ts
// Alternatively you can also provide an options object instead of the boolean flag.
@Property.string({ hash: true, exclude: true })
password!: HashString;
```

The configuration object that can be used instead of the boolean flag here is pretty flexible with a callback. It allows for example to exclude based on the current users role. So you could specify that for Admins the password hash is not excluded, but for normal Users it is.

## CAVEAT: What this actually does
Be aware that this does NOT remove the property as soon as the result comes back from your repository call. It simply marks them as not enumarable, which results in the property never showing up when things like JSON.stringify etc. are used.

This allows you to work with the value while it's still on the server. If you have a login endpoint for example, you can access the password hash to compare it to the user input. But if you return the current user data afterwards, the password is removed.
6 changes: 5 additions & 1 deletion docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ export class MetricsController {
@Get('/dashboard')
async dashboard(): Promise<HtmlResponse> {
const version: string = GlobalRegistry.getAppData('version') ?? '-';
return await PreactUtilities.renderResponse(MetricsPage, { version, primary: '#0e456f', secondary: '#00b4d8' });
const html: string = await PreactUtilities.renderPage(
MetricsPage,
{ version, cacheNames, primary: '#0e456f', secondary: '#00b4d8' }
);
return HtmlResponse.fromString(html);
}
}
```
Expand Down
3 changes: 2 additions & 1 deletion docs/plugin.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# TODO
# TODO
# Plugin
6 changes: 5 additions & 1 deletion docs/templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export class PageController {
@Response.html()
@Get()
async index(): Promise<HtmlResponse> {
return await PreactUtilities.renderResponse(HomePage, { appName: GlobalRegistry.getAppData('name') ?? '' });
const html: string = await PreactUtilities.renderPage(
HomePage,
{ appName: GlobalRegistry.getAppData('name') ?? '' }
);
return HtmlResponse.fromString(html);
}
}
```
Expand Down
45 changes: 45 additions & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Versioning
Zibri uses the `package.json` as the single source of truth for versioning concerns.

It's used for [Handling migrations](./data-source.md#handling-migrations) and to check against endpoints supported versions.

## Defining supported versions on endpoints
Supported versions can be defined with the same syntax you would use inside your package.json:

```ts
@Controller('/some', { versions: ['^1.0.0'] })
class ValidatedController {
@Get('/endpoint-v1')
getEndpoint(): { ok: boolean } {
return { ok: true };
}

@Get('/endpoint-v2', { versions: ['^2'] })
getEndpointV2(): { ok: boolean } {
return { ok: true };
}

@Get('/endpoint-latest', { versions: ['^latest'] })
getEndpointLatest(): { ok: boolean } {
return { ok: true };
}
}
```

As you can see, versions can either be defined on the controller or on the route level.

There is also the special `'latest'` version, which resolves to the current highest major version. By default controllers and routes support the version `'^latest'`.

## Validation
Zibri aims to provide really strong guard rails when it comes to versioning. You might have already noticed that a `versions` folder in your project gets generated when you start it up the first time with a new `package.json` version. Inside the folder, each version is saved, in addition with its configured routes at that point in time. This allows for some really strong validation when the version inside the `package.json` is bumped.

Let's say you start with version `'1.0.0'` (the default) and later on change it to `'2.0.0'`. Now Zibri can complain about any endpoint that previously used `'latest'`, `'^latest'` or `'~latest'`, because latest now means something else, so it should have something like `['^1.0.0', '^latest']` as the supported versions defined. Otherwise, any user of version `'1.0.0'` would get an error that the endpoints they spoke to just fine now no longer exist.

# Version resolution
Versions are resolved by reading from a custom header (`'x-version'` by default). The provided value can either be:
- a concrete version, like `'1.0.0'`
- a json date, like `'2026-05-29T07:44:06.186Z'`

Zibris versioning service then handles resolving the correct version and endpoint for that header. Or throwing an error when it could not be found.

What version is resolved by the date is defined inside of the version files: They contain a startsAt and endsAt timestamp.
Loading
Loading