@tstdl/base 0.93.139 → 0.93.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.js +2 -2
- package/audit/README.md +267 -0
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/lock/README.md +249 -0
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +1 -1
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/package.json +8 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/reflection/README.md +305 -0
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +300 -0
- package/task-queue/postgres/task-queue.d.ts +2 -1
- package/task-queue/postgres/task-queue.js +32 -2
- package/task-queue/task-context.js +1 -1
- package/task-queue/task-queue.d.ts +17 -0
- package/task-queue/task-queue.js +103 -45
- package/task-queue/tests/complex.test.js +4 -4
- package/task-queue/tests/dependencies.test.js +4 -2
- package/task-queue/tests/queue.test.js +111 -0
- package/task-queue/tests/worker.test.js +21 -13
- package/templates/README.md +287 -0
- package/testing/README.md +157 -0
- package/text/README.md +346 -0
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @tstdl/base/templates
|
|
2
|
+
|
|
3
|
+
A powerful, extensible, and type-safe templating engine for TypeScript applications. It provides a unified interface for defining, resolving, and rendering templates from various sources (memory, file system) using multiple engines (Handlebars, JSX, MJML).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [✨ Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
10
|
+
- [Setup](#setup)
|
|
11
|
+
- [In-Memory String Template](#in-memory-string-template)
|
|
12
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
13
|
+
- [File-Based Handlebars Templates](#file-based-handlebars-templates)
|
|
14
|
+
- [Responsive Emails with MJML & JSX](#responsive-emails-with-mjml--jsx)
|
|
15
|
+
- [Template Preprocessing](#template-preprocessing)
|
|
16
|
+
- [📚 API](#-api)
|
|
17
|
+
|
|
18
|
+
## ✨ Features
|
|
19
|
+
|
|
20
|
+
- **Modular Architecture**: Decouples template storage (Providers), content retrieval (Resolvers), and rendering logic (Renderers).
|
|
21
|
+
- **Multi-Engine Support**: Built-in support for:
|
|
22
|
+
- **Strings**: Simple text replacement or function execution.
|
|
23
|
+
- **Handlebars**: Logic-less templates with helpers and partials.
|
|
24
|
+
- **JSX**: Type-safe component-based rendering (via Preact).
|
|
25
|
+
- **MJML**: Responsive email framework.
|
|
26
|
+
- **Flexible Sources**: Load templates from the file system or define them programmatically in memory.
|
|
27
|
+
- **Type Safety**: Leverages TypeScript generics to ensure template contexts and options are strictly typed.
|
|
28
|
+
- **Dependency Injection**: Fully integrated with `@tstdl/base/injector` for easy configuration and extension.
|
|
29
|
+
|
|
30
|
+
## Core Concepts
|
|
31
|
+
|
|
32
|
+
The module relies on four main components working in harmony:
|
|
33
|
+
|
|
34
|
+
1. **Template**: A data model defining the structure of a template. It contains a `name` and a set of `fields` (e.g., `subject`, `body`). Each field specifies a `resolver` (how to get the content) and a `renderer` (how to process it).
|
|
35
|
+
2. **TemplateProvider**: Responsible for locating and loading the `Template` definition object.
|
|
36
|
+
- `MemoryTemplateProvider`: Stores templates in a Map.
|
|
37
|
+
- `FileTemplateProvider`: Loads template definitions from `.js` files.
|
|
38
|
+
3. **TemplateResolver**: Fetches the raw content for a specific field.
|
|
39
|
+
- `StringTemplateResolver`: Returns a string or executes a function.
|
|
40
|
+
- `FileTemplateResolver`: Reads content from a file on disk.
|
|
41
|
+
- `JsxTemplateResolver`: Returns a JSX component.
|
|
42
|
+
4. **TemplateRenderer**: Transforms the raw content and a data context into the final output string.
|
|
43
|
+
- `HandlebarsTemplateRenderer`, `JsxTemplateRenderer`, `MjmlTemplateRenderer`, etc.
|
|
44
|
+
|
|
45
|
+
## 🚀 Basic Usage
|
|
46
|
+
|
|
47
|
+
### Setup
|
|
48
|
+
|
|
49
|
+
To use the module, you must configure it during your application's bootstrap phase. This registers the necessary providers and renderers with the dependency injection system.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { configureTemplates } from '@tstdl/base/templates';
|
|
53
|
+
import { MemoryTemplateProvider } from '@tstdl/base/templates/providers';
|
|
54
|
+
import { StringTemplateRenderer } from '@tstdl/base/templates/renderers';
|
|
55
|
+
import { StringTemplateResolver } from '@tstdl/base/templates/resolvers';
|
|
56
|
+
|
|
57
|
+
// In your bootstrap.ts or app.ts
|
|
58
|
+
configureTemplates({
|
|
59
|
+
templateProvider: MemoryTemplateProvider,
|
|
60
|
+
templateResolvers: [StringTemplateResolver],
|
|
61
|
+
templateRenderers: [StringTemplateRenderer],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### In-Memory String Template
|
|
66
|
+
|
|
67
|
+
This example defines a template in code and renders it using a simple string function.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { inject } from '@tstdl/base/injector';
|
|
71
|
+
import { TemplateService, simpleTemplate } from '@tstdl/base/templates';
|
|
72
|
+
import { MemoryTemplateProvider } from '@tstdl/base/templates/providers';
|
|
73
|
+
import { stringTemplateField } from '@tstdl/base/templates/resolvers';
|
|
74
|
+
|
|
75
|
+
// 1. Define the context type for type safety
|
|
76
|
+
type WelcomeContext = { user: string };
|
|
77
|
+
|
|
78
|
+
// 2. Create the template definition
|
|
79
|
+
const welcomeTemplate = simpleTemplate<WelcomeContext>(
|
|
80
|
+
'welcome',
|
|
81
|
+
stringTemplateField({
|
|
82
|
+
renderer: 'string',
|
|
83
|
+
template: (context) => `Hello, ${context.user}!`,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
// 3. Register the template with the provider
|
|
89
|
+
const provider = inject(MemoryTemplateProvider);
|
|
90
|
+
provider.add('welcome', welcomeTemplate);
|
|
91
|
+
|
|
92
|
+
// 4. Render the template
|
|
93
|
+
const service = inject(TemplateService);
|
|
94
|
+
const result = await service.render('welcome', { user: 'Alice' });
|
|
95
|
+
|
|
96
|
+
console.log(result.fields.template); // Output: "Hello, Alice!"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 🔧 Advanced Topics
|
|
101
|
+
|
|
102
|
+
### File-Based Handlebars Templates
|
|
103
|
+
|
|
104
|
+
For larger applications, it is common to store template definitions and content in files.
|
|
105
|
+
|
|
106
|
+
**1. Configuration**
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { resolve } from 'node:path';
|
|
110
|
+
import { configureTemplates } from '@tstdl/base/templates';
|
|
111
|
+
import { configureFileTemplateProvider, FileTemplateProvider } from '@tstdl/base/templates/providers';
|
|
112
|
+
import { HandlebarsTemplateRenderer } from '@tstdl/base/templates/renderers';
|
|
113
|
+
import { configureFileTemplateResolver, FileTemplateResolver } from '@tstdl/base/templates/resolvers';
|
|
114
|
+
|
|
115
|
+
const templatesDir = resolve(process.cwd(), 'templates');
|
|
116
|
+
|
|
117
|
+
configureTemplates({
|
|
118
|
+
templateProvider: FileTemplateProvider,
|
|
119
|
+
templateResolvers: [FileTemplateResolver],
|
|
120
|
+
templateRenderers: [HandlebarsTemplateRenderer],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Set base paths for definitions and content files
|
|
124
|
+
configureFileTemplateProvider({ basePath: resolve(templatesDir, 'definitions') });
|
|
125
|
+
configureFileTemplateResolver({ basePath: resolve(templatesDir, 'content') });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**2. Template Definition (`templates/definitions/invoice.ts`)**
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Template } from '@tstdl/base/templates';
|
|
132
|
+
import { fileTemplateField } from '@tstdl/base/templates/resolvers';
|
|
133
|
+
|
|
134
|
+
export type InvoiceContext = { id: string; total: number };
|
|
135
|
+
|
|
136
|
+
// Define a template with two fields: subject and html body
|
|
137
|
+
const template: Template<{ subject: true; html: true }, any, InvoiceContext> = {
|
|
138
|
+
name: 'invoice',
|
|
139
|
+
fields: {
|
|
140
|
+
subject: fileTemplateField({
|
|
141
|
+
renderer: 'handlebars',
|
|
142
|
+
templateFile: 'invoice/subject.hbs', // Relative to resolver basePath
|
|
143
|
+
}),
|
|
144
|
+
html: fileTemplateField({
|
|
145
|
+
renderer: 'handlebars',
|
|
146
|
+
templateFile: 'invoice/body.hbs',
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default template;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**3. Content Files**
|
|
155
|
+
|
|
156
|
+
- `templates/content/invoice/subject.hbs`: `Invoice #{{id}}`
|
|
157
|
+
- `templates/content/invoice/body.hbs`: `<h1>Invoice #{{id}}</h1><p>Total: {{total}}</p>`
|
|
158
|
+
|
|
159
|
+
**4. Rendering**
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const service = inject(TemplateService);
|
|
163
|
+
const result = await service.render('invoice', { id: '1001', total: 59.99 });
|
|
164
|
+
|
|
165
|
+
console.log(result.fields.subject); // "Invoice #1001"
|
|
166
|
+
console.log(result.fields.html); // "<h1>Invoice #1001</h1><p>Total: 59.99</p>"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Responsive Emails with MJML & JSX
|
|
170
|
+
|
|
171
|
+
You can combine the power of JSX components with MJML to create type-safe, responsive emails.
|
|
172
|
+
|
|
173
|
+
**1. Configuration**
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { configureTemplates } from '@tstdl/base/templates';
|
|
177
|
+
import { MemoryTemplateProvider } from '@tstdl/base/templates/providers';
|
|
178
|
+
import { JsxTemplateRenderer, MjmlTemplateRenderer } from '@tstdl/base/templates/renderers';
|
|
179
|
+
import { JsxTemplateResolver } from '@tstdl/base/templates/resolvers';
|
|
180
|
+
|
|
181
|
+
configureTemplates({
|
|
182
|
+
templateProvider: MemoryTemplateProvider,
|
|
183
|
+
templateResolvers: [JsxTemplateResolver],
|
|
184
|
+
templateRenderers: [JsxTemplateRenderer, MjmlTemplateRenderer],
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**2. Component & Template**
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { inject } from '@tstdl/base/injector';
|
|
192
|
+
import { TemplateService } from '@tstdl/base/templates';
|
|
193
|
+
import { MemoryTemplateProvider } from '@tstdl/base/templates/providers';
|
|
194
|
+
import { simpleJsxTemplate } from '@tstdl/base/templates/resolvers';
|
|
195
|
+
|
|
196
|
+
type AlertContext = { message: string };
|
|
197
|
+
|
|
198
|
+
// Define the email layout using JSX and MJML tags
|
|
199
|
+
const AlertEmail = ({ message }: AlertContext) => (
|
|
200
|
+
<mjml>
|
|
201
|
+
<mj-body>
|
|
202
|
+
<mj-section>
|
|
203
|
+
<mj-column>
|
|
204
|
+
<mj-text color="#F45E43">{message}</mj-text>
|
|
205
|
+
</mj-column>
|
|
206
|
+
</mj-section>
|
|
207
|
+
</mj-body>
|
|
208
|
+
</mjml>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Create the template definition
|
|
212
|
+
const alertTemplate = simpleJsxTemplate('alert', AlertEmail);
|
|
213
|
+
|
|
214
|
+
// IMPORTANT: Tell the renderer to use 'mjml-jsx'.
|
|
215
|
+
// This means: Use 'jsx' renderer first, then pass the result to 'mjml' renderer.
|
|
216
|
+
alertTemplate.fields.template.renderer = 'mjml-jsx';
|
|
217
|
+
|
|
218
|
+
// Register and render
|
|
219
|
+
const provider = inject(MemoryTemplateProvider);
|
|
220
|
+
provider.add('alert', alertTemplate);
|
|
221
|
+
|
|
222
|
+
const service = inject(TemplateService);
|
|
223
|
+
const result = await service.render('alert', { message: 'System Down!' });
|
|
224
|
+
|
|
225
|
+
// result.fields.template contains the compiled HTML from MJML
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Template Preprocessing
|
|
229
|
+
|
|
230
|
+
The `MjmlTemplateRenderer` supports a special renderer string format: `mjml-[preprocessor]`.
|
|
231
|
+
|
|
232
|
+
If you specify `renderer: 'mjml-handlebars'`, the system will:
|
|
233
|
+
|
|
234
|
+
1. Resolve the content (e.g., from a file).
|
|
235
|
+
2. Render it using the `HandlebarsTemplateRenderer`.
|
|
236
|
+
3. Pass the resulting string (which should be valid MJML) to the `MjmlTemplateRenderer`.
|
|
237
|
+
4. Output the final HTML.
|
|
238
|
+
|
|
239
|
+
This allows you to use Handlebars or JSX logic to generate your MJML structure dynamically.
|
|
240
|
+
|
|
241
|
+
## 📚 API
|
|
242
|
+
|
|
243
|
+
### Configuration & Service
|
|
244
|
+
|
|
245
|
+
| Export | Description |
|
|
246
|
+
| :--------------------------- | :---------------------------------------------------------------- |
|
|
247
|
+
| `configureTemplates(config)` | Configures the module with providers, resolvers, and renderers. |
|
|
248
|
+
| `TemplateService` | The main service. Use `.render(key, context)` to generate output. |
|
|
249
|
+
| `TemplateProvider` | Abstract base class for template sources. |
|
|
250
|
+
| `TemplateResolver` | Abstract base class for content retrieval. |
|
|
251
|
+
| `TemplateRenderer` | Abstract base class for content rendering. |
|
|
252
|
+
|
|
253
|
+
### Providers
|
|
254
|
+
|
|
255
|
+
| Class | Description |
|
|
256
|
+
| :-------------------------------------- | :--------------------------------------------------------------------- |
|
|
257
|
+
| `MemoryTemplateProvider` | Stores template definitions in an in-memory Map. |
|
|
258
|
+
| `FileTemplateProvider` | Loads template definitions from `.js` files in a configured directory. |
|
|
259
|
+
| `configureFileTemplateProvider(config)` | Sets the base path for `FileTemplateProvider`. |
|
|
260
|
+
|
|
261
|
+
### Resolvers
|
|
262
|
+
|
|
263
|
+
| Class | Description |
|
|
264
|
+
| :-------------------------------------- | :--------------------------------------------------------------------- |
|
|
265
|
+
| `StringTemplateResolver` | Resolves content from a string or function in the template definition. |
|
|
266
|
+
| `FileTemplateResolver` | Reads content from a file path specified in the template definition. |
|
|
267
|
+
| `JsxTemplateResolver` | Resolves content from a JSX component in the template definition. |
|
|
268
|
+
| `configureFileTemplateResolver(config)` | Sets the base path for `FileTemplateResolver`. |
|
|
269
|
+
|
|
270
|
+
### Renderers
|
|
271
|
+
|
|
272
|
+
| Class | Description |
|
|
273
|
+
| :--------------------------- | :----------------------------------------------------------------------- |
|
|
274
|
+
| `StringTemplateRenderer` | Renders simple strings or executes string-returning functions. |
|
|
275
|
+
| `HandlebarsTemplateRenderer` | Renders Handlebars templates. Supports helpers and partials. |
|
|
276
|
+
| `JsxTemplateRenderer` | Renders JSX components to strings (via `preact-render-to-string`). |
|
|
277
|
+
| `MjmlTemplateRenderer` | Renders MJML strings to HTML. Supports preprocessing (e.g., `mjml-jsx`). |
|
|
278
|
+
|
|
279
|
+
### Helpers
|
|
280
|
+
|
|
281
|
+
| Function | Description |
|
|
282
|
+
| :----------------------------------- | :------------------------------------------------------------------ |
|
|
283
|
+
| `simpleTemplate(name, field)` | Creates a `Template` object with a single field named `template`. |
|
|
284
|
+
| `simpleJsxTemplate(name, component)` | Creates a `Template` object for a JSX component. |
|
|
285
|
+
| `fileTemplateField(options)` | Helper to create a field definition using `FileTemplateResolver`. |
|
|
286
|
+
| `stringTemplateField(options)` | Helper to create a field definition using `StringTemplateResolver`. |
|
|
287
|
+
| `jsxTemplateField(options)` | Helper to create a field definition using `JsxTemplateResolver`. |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Testing in tstdl
|
|
2
|
+
|
|
3
|
+
This project uses **[Vitest](https://vitest.dev/)** as its primary testing framework. It is configured to handle both unit and integration tests with support for dependency injection and database-backed scenarios.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Set up Infrastructure
|
|
8
|
+
|
|
9
|
+
Some tests require external services like a database. These must be started if the tests depend on them.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd base
|
|
13
|
+
docker-compose up -d
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 2. Run Tests
|
|
17
|
+
|
|
18
|
+
You can run all tests or filter by module/file.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Run all tests
|
|
22
|
+
npm test
|
|
23
|
+
|
|
24
|
+
# Run a specific test file
|
|
25
|
+
npx test source/injector/tests/basic.test.ts
|
|
26
|
+
|
|
27
|
+
# Run a specific folder of tests
|
|
28
|
+
npx test source/injector/tests/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 3. Coverage
|
|
32
|
+
|
|
33
|
+
To generate a code coverage report for a specific module:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm test --coverage --coverage.include="source/injector/**/*.ts" source/injector/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 4. Correctness
|
|
40
|
+
|
|
41
|
+
Regularily use TypesScript's type checker to ensure correctness.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx tsc --noEmit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Project Structure
|
|
48
|
+
|
|
49
|
+
- **Test Files**: Always located in a `<module>/tests/` subdirectory within their respective module.
|
|
50
|
+
- **Naming**: Files must end in `.test.ts`.
|
|
51
|
+
- **Infrastructure**: Shared testing utilities are located in `source/testing/`.
|
|
52
|
+
|
|
53
|
+
## Writing Unit Tests
|
|
54
|
+
|
|
55
|
+
Unit tests should be isolated and not require external services. Use Vitest's `describe`, `test`/`it`, and `expect`.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { describe, expect, test } from 'vitest';
|
|
59
|
+
import { myFunc } from '../my-module.js';
|
|
60
|
+
|
|
61
|
+
describe('MyModule', () => {
|
|
62
|
+
test('should do something', () => {
|
|
63
|
+
const result = myFunc();
|
|
64
|
+
expect(result).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Writing Integration Tests
|
|
70
|
+
|
|
71
|
+
Integration tests verify the interaction between multiple components or with external services (Database, S3, etc.). Use the `setupIntegrationTest` helper to initialize the environment.
|
|
72
|
+
|
|
73
|
+
### Setup Helper: `setupIntegrationTest`
|
|
74
|
+
|
|
75
|
+
The `setupIntegrationTest` utility (found in `source/testing/integration-setup.ts`) automates:
|
|
76
|
+
|
|
77
|
+
1. **Injector**: Creates a new `TestInjector`.
|
|
78
|
+
2. **Logging**: Configures pretty-print logging with configurable levels.
|
|
79
|
+
3. **Database**: Sets up the ORM and provides a `Database` instance.
|
|
80
|
+
4. **Schema Isolation**: Automatically creates and uses PostgreSQL schemas for isolation.
|
|
81
|
+
5. **Modules**: Optional configuration for `taskQueue`, `authentication`, `objectStorage`, `notification`, etc.
|
|
82
|
+
|
|
83
|
+
### Integration Test Example
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { beforeAll, describe, expect, it } from 'vitest';
|
|
87
|
+
import { setupIntegrationTest } from '#/testing/index.js';
|
|
88
|
+
import { MyService } from '../my-service.js';
|
|
89
|
+
|
|
90
|
+
describe('MyService Integration', () => {
|
|
91
|
+
let injector;
|
|
92
|
+
|
|
93
|
+
beforeAll(async () => {
|
|
94
|
+
// Setup with database and specific modules
|
|
95
|
+
({ injector } = await setupIntegrationTest({
|
|
96
|
+
modules: { taskQueue: true },
|
|
97
|
+
orm: { schema: 'test_my_service' },
|
|
98
|
+
}));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should process data through the database', async () => {
|
|
102
|
+
const service = injector.resolve(MyService);
|
|
103
|
+
const result = await service.process();
|
|
104
|
+
expect(result.success).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Best Practices & Guidelines
|
|
110
|
+
|
|
111
|
+
### 1. Isolation
|
|
112
|
+
|
|
113
|
+
- **Schemas**: Always use a unique PostgreSQL schema for your integration test suite via `orm: { schema: '...' }`.
|
|
114
|
+
- **Resource Naming**: When testing shared infrastructure (Task Queues, S3 Buckets, Rate Limiters), append a random suffix or timestamp to the resource name (e.g., `const queueName = \`test-queue-${crypto.randomUUID()}\``).
|
|
115
|
+
- **Cleanup**: Use `truncateTables` or `dropTables` in `beforeAll` or `beforeEach` to ensure a clean state.
|
|
116
|
+
- **Tenant Data**: Use `clearTenantData` if your tests are multi-tenant and use random tenant IDs to avoid collisions when concurrently running tests.
|
|
117
|
+
- **Schema**: Make sure you use the correct schema when clearing data with any of the helper functions. They must match the schema used by the module.
|
|
118
|
+
|
|
119
|
+
### 2. Dependency Injection & Mocking
|
|
120
|
+
|
|
121
|
+
- **Resolving**: Use `injector.resolve(Token)` or `injector.resolveAsync(Token)` to get instances.
|
|
122
|
+
- **Mocking Services**: For services managed by the `Injector`, prefer registering a mock value over using `vi.mock`.
|
|
123
|
+
```typescript
|
|
124
|
+
const mailServiceMock = { send: vi.fn() };
|
|
125
|
+
injector.register(MailService, { useValue: mailServiceMock });
|
|
126
|
+
```
|
|
127
|
+
- **Injection Context**: Any code that uses repositories or services relying on `inject()` outside of a class constructor/initializer **must** be wrapped in `runInInjectionContext(injector, async () => { ... })`.
|
|
128
|
+
- **Mock Reset**: Always call `vi.clearAllMocks()` in `beforeEach` if using spys or mocks to ensure test independence.
|
|
129
|
+
|
|
130
|
+
### 3. Integration Test Patterns
|
|
131
|
+
|
|
132
|
+
- **Schema Management**:
|
|
133
|
+
- **Preferred**: Use the `modules` option in `setupIntegrationTest`. This automatically runs the necessary migrations for standard modules (e.g., `taskQueue`, `authentication`, `notification`).
|
|
134
|
+
- **Fallback**: Use Manual DDL via `database.execute(sql\`...\`)`**only** for test-specific entities (like local`@Table`classes defined inside your test file) that are not part of a standard module. Always call`dropTables` before creating them to ensure a fresh start.
|
|
135
|
+
- **Background Workers**: When testing workers or loops, use a `CancellationToken` to stop them in `afterEach` or `afterAll`.
|
|
136
|
+
- **Timeouts**: Use the `timeout(ms)` helper from `#/utils/timing.js` for tests involving refills, retries, or background processing. Prefer `vi.useFakeTimers()` only when real-time passage is too slow for the test suite.
|
|
137
|
+
- **Logging**: Test should generally not output logs unless for active debugging. Use try-catch for expected errors and assert on the error message instead of relying on logs.
|
|
138
|
+
|
|
139
|
+
### 4. Performance
|
|
140
|
+
|
|
141
|
+
- Use `beforeAll` for heavy setup (like schema and table creation) and `beforeEach` for lightweight data resetting (truncation).
|
|
142
|
+
- Prefer integration tests over unit tests when reasonably possible to cover more code paths and reduce the need for extensive mocking and stubbing.
|
|
143
|
+
|
|
144
|
+
### 5. Coding Style
|
|
145
|
+
|
|
146
|
+
- **Explicit Types**: Use explicit return types for methods and functions.
|
|
147
|
+
- **Async/Await**: Always use `async/await` for asynchronous code to improve readability and error handling.
|
|
148
|
+
- **Mocking Types**: When mocking, use `Partial<Type>` or `Pick<Type, 'method1' | 'method2'>` to maintain type safety, where type is the original type being mocked.
|
|
149
|
+
- **Avoid `any`**: Strive to use specific types or `unknown` if required to maintain type safety.
|
|
150
|
+
- **Descriptive Names**: Use descriptive names for test files, cases and variables to improve readability.
|
|
151
|
+
|
|
152
|
+
### 6. Common Helpers (from `source/testing/`)
|
|
153
|
+
|
|
154
|
+
- `setupIntegrationTest(options)`: Main entry point for integration tests.
|
|
155
|
+
- `truncateTables(database, schema, tables)`: Clears data from specified tables.
|
|
156
|
+
- `dropTables(database, schema, tables)`: Drops specified tables.
|
|
157
|
+
- `clearTenantData(database, schema, tables, tenantId)`: Deletes data for a specific tenant.
|