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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion docs/assets.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Assets
Zibri offers a simple way to serve static files:

Anything you put under your projects assets folder will served under `/assets/{asset-name-with-file-ending}` (With the default configuration).
Anything you put under your projects assets/public folder will served under `/assets/{asset-name-with-file-ending}` (With the default configuration).

For documentation purposes there is also a simple "file explorer" page provided by Zibri under `/assets` when not specifying an asset, that can also be reached from the root page:

Expand Down
14 changes: 7 additions & 7 deletions docs/creating-endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ You can try the example above and navigate to your applications open api explore
You should now see a new route get `/users` with the correctly defined return type.

## Inheritance for controllers
You can use inheritance for controllers using generics and even with the provided helper types like "OmitType", "PickType" etc. Zibri additionally provides a helper type for defining base crud endpoints that you can also extend from:
You can use inheritance for controllers using generics and even with the provided helper types like "OmitClass", "PickClass" etc. Zibri additionally provides a helper type for defining base crud endpoints that you can also extend from:

```ts
// src/controllers/test-crud.controller.ts
import { CombinedType, Controller, CrudController, OmitType, PickType } from 'zibri';
import { IntersectionClass, Controller, CrudController, OmitClass, PickClass } from 'zibri';

import { Test, TestCreateDTO, TestUpdateDTO } from '../models';
import { MetricsController } from './metrics.controller';

@Controller('/tests-crud')
export class TestCrudController extends CombinedType(
OmitType(CrudController(Test, TestCreateDTO, TestUpdateDTO), ['deleteById']),
PickType(MetricsController, ['dashboard'])
export class TestCrudController extends IntersectionClass(
OmitClass(CrudController(Test, TestCreateDTO, TestUpdateDTO), ['deleteById']),
PickClass(MetricsController, ['dashboard'])
) {}
```

Expand Down Expand Up @@ -123,7 +123,7 @@ export class UserController {
@Body(UserCreateDTO) // <- Automatically handles validation for you
createData: UserCreateDTO
): Promise<User> {
const createdUser = // ... create a new user
const createdUser: User = // ... create a new user
return createdUser;
}

Expand Down Expand Up @@ -152,7 +152,7 @@ export class NewsletterController {
@Param.path('id', { type: 'string', format: 'uuid' })
newsletterId: string,
@Param.header('User-Agent')
userAgent: string
userAgent: string,
@Param.query('affiliateId', { required: false })
affiliateId?: string,
): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion docs/cron.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class StatusCronJob extends CronJob {
```

## Registering the cron job
For Zibri to be actual able to pickup the cron job start it automatically you need to provide its class at the `index.ts`:
For Zibri to be actual able to pickup the cron job and start it automatically you need to provide its class at the `index.ts`:

```ts
// src/index.ts
Expand Down
19 changes: 11 additions & 8 deletions docs/di.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ Alternatively, you can also add them to the providers array:

```ts
// src/providers.ts
import { DiProvider, ZIBRI_DI_TOKENS } from 'zibri';
import { DiProvider, defineProvider, InjectionToken, ZIBRI_DI_TOKENS } from 'zibri';

export const someToken = new InjectionToken<string>('some-token');

export const providers: DiProvider<unknown>[] = [
// ...
{
token: 'some-token',
defineProvider({
token: someToken,
useFactory: () => '42'
}
})
// ...
]
```
Expand All @@ -58,14 +60,15 @@ And then inject them the same way before, with the constructor approach needing
```ts
// src/controllers/test.controller.ts
import { Controller, inject } from 'zibri';
import { someToken } from '../../providers.ts';

@Controller('/tests')
export class TestController {
constructor(
@Inject('some-token')
@Inject(someToken)
private readonly value: string
) {
const alternative: string = inject('some-token');
const alternative: string = inject(someToken);
}
// ...
}
Expand Down Expand Up @@ -96,10 +99,10 @@ import { myErrorHandler } from './my-error-handler.ts';

export const providers: DiProvider<unknown>[] = [
// ...
{
defineProvider({
token: ZIBRI_DI_TOKENS.GLOBAL_ERROR_HANDLER,
useFactory: () => myErrorHandler
}
})
// ...
]
```
Expand Down
21 changes: 9 additions & 12 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ You will however most likely use the provided `ScrapeMetricsCronJob` which colle
See [registering cron jobs](./cron.md#registering-the-cron-job).

## Exposing a basic dashboard
By default, your project contains a `metrics.hbs` file that is used to display the data of the metrics service.
By default, your project contains a `metrics.tsx` file that is used to display the data of the metrics service.

You can simply create a new controller and expose it there:

```ts
// src/controllers/metrics.controller.ts
import { Controller, Inject, ZIBRI_DI_TOKENS, Metric, Get, Response, HtmlResponse, MetricsSnapshot, GlobalRegistry, MetricsServiceInterface } from 'zibri';
import { Controller, Inject, PreactUtilities, ZIBRI_DI_TOKENS, Metric, Get, Response, HtmlResponse, MetricsSnapshot, GlobalRegistry, MetricsServiceInterface } from 'zibri';

import renderBasePageTemplate from '../templates/pages/base-page.hbs';
import renderMetricsTemplate from '../templates/pages/metrics.hbs';
import { MetricsPage } from '../templates/pages/metrics';

@Controller('/metrics')
export class MetricsController {
Expand All @@ -43,15 +42,13 @@ export class MetricsController {

@Response.html()
@Get('/dashboard')
dashboard(): HtmlResponse {
const content: string = renderMetricsTemplate({
name: GlobalRegistry.getAppData('name') ?? '-',
version: GlobalRegistry.getAppData('version') ?? '-'
});
const html: string = renderBasePageTemplate({ base: { title: 'Metrics Dashboard' }, content });
return HtmlResponse.fromString(html);
async dashboard(): Promise<HtmlResponse> {
const version: string = GlobalRegistry.getAppData('version') ?? '-';
return await PreactUtilities.renderResponse(MetricsPage, { version, primary: '#0e456f', secondary: '#00b4d8' });
}
}
```

Now you can navigate to `http://localhost:3000/metrics/dashboard` and see it in action.
Now you can navigate to `http://localhost:3000/metrics/dashboard` and see it in action.

There are also placeholders commented out in the `navbar.tsx` and the `home.tsx` files that link to this basic dashboard.
199 changes: 199 additions & 0 deletions docs/templating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Templating
The templating approach in Zibri currently differs based on whether you want to send an email or a html page.

# Emails
For emails, Zibri uses handlebars:

```ts
import renderBaseEmail from '../templates/emails/base-email.hbs';
import renderPasswordResetTemplate from '../templates/emails/password-reset.hbs';
// ...
const content: string = renderPasswordResetTemplate({
confirmPasswordResetUrl: 'http://localhost:4200/confirm-password-reset',
resetToken: 'test-token',
user: { name: 'James Smith' }
});
const html: string = renderBaseEmail({
content,
base: {
title: 'Password Reset',
baseUrl: 'http://localhost:3000'
}
});
// ...
```

You might be wondering how we can import a ts function from a file ending with `.hbs`.

That's due to our handlebars compiler that automatically creates ts definitions based on your templates. It is actually smart enough to detect any variables that you use in your templates, as well as infer their type. While being extremly helpful, this type safety comes with the downside that any variables you use are restricted in their typing for the compiler to infer their type. They need to be either:
- string
- string[]
- an object with its values being either string, string[] or another object

These are also hot reloaded when you change your templates and eg. introduce or remove a new variable.

> Caveat:<br>
> If you don't run `npm run start` then the compiler won't be able to generate the .ts files. So if you have any problems with eg. a property of your template not being recognized, check that first.

# Pages
For simple html pages Zibri uses [preact](https://preactjs.com/), but with some [heavy modifications](#additional-functionality).

This system is pretty great for a server side framework to render some basic pages. If you have more advanced use cases you should however you will probably be better of by creating a separate client application that consumes the Zibri API.

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

import { HomePage } from '../templates/pages/home';

@Controller('/')
export class PageController {

@Response.html()
@Get()
async index(): Promise<HtmlResponse> {
return await PreactUtilities.renderResponse(HomePage, { appName: GlobalRegistry.getAppData('name') ?? '' });
}
}
```

And here the HomePage component definition:

```tsx
import { PreactComponent } from 'zibri';

import { BasePage } from '../components/base-page';
import { Card } from '../components/card';
import { Heading } from '../components/heading';
import { Link } from '../components/link';

type Props = {
appName: string
};

export const HomePage: PreactComponent<Props> = ({ appName }) => {
return (
<BasePage title='' activeRoute='/' className="flex flex-col gap-4 py-8">
<Heading className="text-center">{appName}</Heading>
<div className="max-w-fit mx-auto grid grid-cols-2 gap-4">
<Card className="flex flex-col gap-2 max-w-80">
<Link href="/assets" icon="/assets/assets.svg">
Assets
</Link>
<p>
Lists all publicly registered assets.
</p>
</Card>
<Card className="flex flex-col gap-2 max-w-80">
<Link href="/explorer" icon="/assets/open-api/swagger.png">
OpenAPI Explorer
</Link>
<p>
The official OpenAPI/Swagger documentation.
</p>
</Card>
{/* <Card className="flex flex-col gap-2 max-w-80">
<Link href="/metrics/dashboard" icon="/assets/metrics.svg">
Metrics
</Link>
<p>
A basic metrics dashboard.
</p>
</Card> */}
</div>
</BasePage>
);
};
```

## Additional functionality
In addition to the features coming from preact out of the box, Zibri provides some more:

It tries to include js script tags inside the server generated code so that functionality can be restored on the frontend part.

Note that this is NOT hydration and only properties explicitly passed into a component can appear on the html/script sent to the client.
NO imports are resolved automatically.

This means that you can't simply leak server side secrets like api keys to the client just because you used `environment.apiUrl` somewhere and the `index.ts` where its imported from also contains some secrets that tsx compiles into the code. (A reoccuring problem with frameworks that mix the line between front- and backend)

But this also means that the functionality is a lot more restrictive than React, because every hook has to be custom provided. There are currently only two hooks available: `onClient` and `onServer`. Everything else that you might know (useState etc.) simply won't work.

Let's take the metrics page as an example (full content down below), because we have a lot of client functionality that gets restored.
The first thing you will probably notice is the strange import at the start of the file:

```tsx
import { Chart } from 'chart.js?client';
```

This is basically our way to tell the templating engine "chart.js needs to be available on the client side.".
What this results in is that a js file is automatically generated in `assets/public/vendor/chart.js.js` and then later referenced in the rendered html as a script tag.
This also has the added benefit that the version of chart.js on the client side is always the same as the one on the server side.

If you want to use a package on the client side, you also have to add an entry to the `tsx.d.ts`.

Let's go to the component body next:

```tsx
// ...
// the below can be safely executed on both server and client side.
Chart.defaults.color = 'whitesmoke';
Chart.defaults.borderColor = 'whitesmoke';
Chart.defaults.scale.grid.color = 'rgba(200, 200, 200, 0.3)';

let snaps: MetricsSnapshot[] = [];
let automaticReload: boolean = true;

// the onClient hook provides a way to mark logic that should only be called on the client.
// In this case window would throw an error on the server side, because it does not exist there.
onClient(() => {
window.addEventListener('load', () => {
void loadSnapshots();
setInterval(() => void loadSnapshots(), 1000);
});
});

async function loadSnapshots(): Promise<void> {
if (!automaticReload) {
return;
}
try {
const resp: Response = await fetch('/metrics');
snaps = await resp.json();
document.dispatchEvent(new CustomEvent('metrics:update', { detail: { snaps } }));
}
catch (error) {
console.error('failed to load metrics', error);
}
}
//...
```

Finally let's take a look at the template returned:

```tsx
<BasePage title='Metrics'
activeRoute='/metrics/dashboard'
scripts={['/assets/lib/chartjs-adapter-date-fns.js']}
className="flex flex-col gap-4 py-8"
>
<Heading className="text-center">Metrics</Heading>
<div className="w-full px-10 flex flex-col gap-5">
<div className="grid grid-cols-5 gap-5">
<div className="col-span-2 flex flex-col gap-5">
<MetricsStatus
automaticReloadChecked={automaticReload}
onReloadChange={() => automaticReload = !automaticReload} // The onReloadChange method is automatically reapplied, it just works on the client.
version={version}
className="flex-1"
>
</MetricsStatus>
<RequestDurationChart className="flex-1" secondary={secondary}></RequestDurationChart>
</div>
<RequestsPerSecondChart secondary={secondary} className="col-span-3"></RequestsPerSecondChart>
</div>
<div className="grid grid-cols-2 gap-5">
<ResourceUsageChart primary={primary} secondary={secondary}></ResourceUsageChart>
<NetworkChart primary={primary} secondary={secondary}></NetworkChart>
</div>
</div>
</BasePage>
```
Loading
Loading