lakutata 3.0.0 → 3.1.0

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.
Files changed (117) hide show
  1. package/README.md +50 -30
  2. package/README.zh-CN.md +130 -0
  3. package/dist/cjs/exports/QueueComponent.d.ts +1 -0
  4. package/dist/cjs/exports/QueueComponent.js +20 -0
  5. package/dist/esm/exports/QueueComponent.js +1 -0
  6. package/dist/types/exports/QueueComponent.d.ts +1 -0
  7. package/doc/en/Application.md +107 -0
  8. package/doc/en/CLI.md +274 -0
  9. package/doc/en/Cache.md +379 -0
  10. package/doc/en/Command.md +110 -0
  11. package/doc/en/ContainerdIntegration.md +17 -16
  12. package/doc/en/Controllers.md +256 -0
  13. package/doc/en/DBus.md +291 -0
  14. package/doc/en/DTOValidation.md +72 -43
  15. package/doc/en/Database.md +188 -0
  16. package/doc/en/DatabaseDrivers.md +287 -0
  17. package/doc/en/Decorator.md +52 -40
  18. package/doc/en/DockerIntegration.md +7 -6
  19. package/doc/en/Entities.md +240 -0
  20. package/doc/en/Entrypoint.md +216 -0
  21. package/doc/en/Exceptions.md +174 -0
  22. package/doc/en/GRPC.md +251 -0
  23. package/doc/en/GettingStarted.md +165 -0
  24. package/doc/en/HTTP.md +507 -0
  25. package/doc/en/HelperFunctions.md +11 -11
  26. package/doc/en/I18n.md +119 -115
  27. package/doc/en/Injection.md +144 -0
  28. package/doc/en/Installers.md +280 -0
  29. package/doc/en/Logger.md +206 -0
  30. package/doc/en/MCP.md +293 -0
  31. package/doc/en/Middlewares.md +213 -0
  32. package/doc/en/Migrations.md +202 -0
  33. package/doc/en/MongoEmbedded.md +149 -0
  34. package/doc/en/Monitor.md +164 -0
  35. package/doc/en/Mutex.md +27 -25
  36. package/doc/en/NATS.md +246 -0
  37. package/doc/en/NativeAddons.md +212 -0
  38. package/doc/en/Objects.md +143 -0
  39. package/doc/en/Overview.md +62 -85
  40. package/doc/en/Packaging.md +193 -0
  41. package/doc/en/ProjectStructure.md +124 -0
  42. package/doc/en/Queue.md +269 -0
  43. package/doc/en/Schedules.md +217 -0
  44. package/doc/en/Security.md +10 -9
  45. package/doc/en/ServiceCalls.md +504 -0
  46. package/doc/en/Time.md +19 -20
  47. package/doc/en/UpgradingFrom1.md +435 -0
  48. package/doc/en/UpgradingFrom2.md +315 -0
  49. package/doc/en/Workspaces.md +189 -0
  50. package/doc/en/examples/BackgroundJobs.md +677 -0
  51. package/doc/en/examples/CommandLineTool.md +664 -0
  52. package/doc/en/examples/DesktopApp.md +809 -0
  53. package/doc/en/examples/DeviceService.md +607 -0
  54. package/doc/en/examples/MCPServer.md +653 -0
  55. package/doc/en/examples/README.md +16 -0
  56. package/doc/en/examples/RestAPI.md +490 -0
  57. package/doc/en/examples/ServiceCalls.md +829 -0
  58. package/doc/en/examples/WebApp.md +568 -0
  59. package/doc/zh/Application.md +75 -0
  60. package/doc/zh/CLI.md +250 -0
  61. package/doc/zh/Cache.md +328 -0
  62. package/doc/zh/Command.md +78 -0
  63. package/doc/zh/ContainerdIntegration.md +9 -9
  64. package/doc/zh/Controllers.md +228 -0
  65. package/doc/zh/DBus.md +257 -0
  66. package/doc/zh/DTOValidation.md +49 -19
  67. package/doc/zh/Database.md +175 -0
  68. package/doc/zh/DatabaseDrivers.md +261 -0
  69. package/doc/zh/Decorator.md +25 -18
  70. package/doc/zh/DockerIntegration.md +6 -6
  71. package/doc/zh/Entities.md +224 -0
  72. package/doc/zh/Entrypoint.md +193 -0
  73. package/doc/zh/Exceptions.md +158 -0
  74. package/doc/zh/GRPC.md +226 -0
  75. package/doc/zh/GettingStarted.md +148 -0
  76. package/doc/zh/HTTP.md +445 -0
  77. package/doc/zh/HelperFunctions.md +2 -2
  78. package/doc/zh/I18n.md +26 -26
  79. package/doc/zh/Injection.md +129 -0
  80. package/doc/zh/Installers.md +232 -0
  81. package/doc/zh/Logger.md +188 -0
  82. package/doc/zh/MCP.md +265 -0
  83. package/doc/zh/Middlewares.md +198 -0
  84. package/doc/zh/Migrations.md +178 -0
  85. package/doc/zh/MongoEmbedded.md +118 -0
  86. package/doc/zh/Monitor.md +145 -0
  87. package/doc/zh/Mutex.md +10 -10
  88. package/doc/zh/NATS.md +207 -0
  89. package/doc/zh/NativeAddons.md +180 -0
  90. package/doc/zh/Objects.md +119 -0
  91. package/doc/zh/Overview.md +46 -71
  92. package/doc/zh/Packaging.md +154 -0
  93. package/doc/zh/ProjectStructure.md +102 -0
  94. package/doc/zh/Queue.md +214 -0
  95. package/doc/zh/Schedules.md +193 -0
  96. package/doc/zh/Security.md +6 -6
  97. package/doc/zh/ServiceCalls.md +459 -0
  98. package/doc/zh/Time.md +2 -2
  99. package/doc/zh/UpgradingFrom1.md +347 -0
  100. package/doc/zh/UpgradingFrom2.md +230 -0
  101. package/doc/zh/Workspaces.md +150 -0
  102. package/doc/zh/examples/BackgroundJobs.md +618 -0
  103. package/doc/zh/examples/CommandLineTool.md +618 -0
  104. package/doc/zh/examples/DesktopApp.md +748 -0
  105. package/doc/zh/examples/DeviceService.md +546 -0
  106. package/doc/zh/examples/MCPServer.md +604 -0
  107. package/doc/zh/examples/README.md +14 -0
  108. package/doc/zh/examples/RestAPI.md +458 -0
  109. package/doc/zh/examples/ServiceCalls.md +765 -0
  110. package/doc/zh/examples/WebApp.md +520 -0
  111. package/package.json +46 -17
  112. package/doc/en/ActionPattern.md +0 -408
  113. package/doc/en/Core.md +0 -312
  114. package/doc/en/ORMIntegration.md +0 -341
  115. package/doc/zh/ActionPattern.md +0 -334
  116. package/doc/zh/Core.md +0 -256
  117. package/doc/zh/ORMIntegration.md +0 -319
@@ -0,0 +1,490 @@
1
+ # A REST API with a database
2
+
3
+ This example builds a service that keeps a list of tasks: an HTTP API to create tasks, list them a page at a time,
4
+ mark them done and delete them, stored in a SQLite database. It starts from the `service` template and shows the
5
+ everyday parts of a lakutata service: an entity and its migration, the DTOs that validate the input, an exception with
6
+ its HTTP status, a controller, and the tests.
7
+
8
+ | Method | Route | What it does |
9
+ |---|---|---|
10
+ | `GET` | `/tasks?done=false&limit=10&offset=0` | Lists the tasks, a page at a time, the done ones or the others |
11
+ | `GET` | `/tasks/:id` | Gives a task |
12
+ | `POST` | `/tasks` | Creates a task: `{"title": "Write the report"}` |
13
+ | `PATCH` | `/tasks/:id` | Changes its title or marks it done: `{"done": true}` |
14
+ | `DELETE` | `/tasks/:id` | Deletes it |
15
+
16
+ ## 1. Create the project
17
+
18
+ ```bash
19
+ npx @lakutata/cli create --name tasks --template service --yes
20
+ cd tasks
21
+ npm install better-sqlite3 -w app
22
+ rm packages/app/src/controllers/ExampleController.ts packages/app/src/dtos/GreetDTO.ts
23
+ echo '*.sqlite' >> .gitignore
24
+ ```
25
+
26
+ The project is a workspace whose service is `packages/app` ([ProjectStructure](../ProjectStructure.md)). The service
27
+ uses SQLite through the `better-sqlite3` driver, installed here; any other database of
28
+ [DatabaseDrivers](../DatabaseDrivers.md) works the same, with its own driver and options. The example controller of the
29
+ template is removed, and the database file is kept out of git.
30
+
31
+ ## 2. The entity
32
+
33
+ **`packages/app/src/entities/Task.ts`**
34
+
35
+ ```typescript
36
+ import {Column, CreateDateColumn, Entity, PrimaryGeneratedColumn} from 'lakutata/decorator/orm'
37
+
38
+ /**
39
+ * A task: what to do, and whether it is done
40
+ */
41
+ @Entity('tasks')
42
+ export class Task {
43
+ @PrimaryGeneratedColumn()
44
+ public id: number
45
+
46
+ @Column({type: 'varchar', length: 200})
47
+ public title: string
48
+
49
+ @Column({default: false})
50
+ public done: boolean
51
+
52
+ @CreateDateColumn()
53
+ public createdAt: Date
54
+ }
55
+ ```
56
+
57
+ An entity is a class whose decorators (from `lakutata/decorator/orm`) describe its table: `tasks`, with an id given by
58
+ the database, a title, whether it is done, and the date it was created on ([Entities](../Entities.md)).
59
+
60
+ ## 3. The input
61
+
62
+ **`packages/app/src/dtos/TaskDTO.ts`**
63
+
64
+ ```typescript
65
+ import {DTO} from 'lakutata'
66
+ import {Expect} from 'lakutata/decorator/dto'
67
+ import {PaginationSearchDTO} from 'lakutata/dtos'
68
+
69
+ /**
70
+ * A task by its id (the :id of the route: text, converted by strict(false))
71
+ */
72
+ export class TaskIdDTO extends DTO {
73
+ @Expect(DTO.Number().integer().positive().strict(false).required())
74
+ public id: number
75
+ }
76
+
77
+ /**
78
+ * A new task
79
+ */
80
+ export class CreateTaskDTO extends DTO {
81
+ @Expect(DTO.String().trim().strict(false).min(1).max(200).required())
82
+ public title: string
83
+ }
84
+
85
+ /**
86
+ * The changes of a task
87
+ */
88
+ export class UpdateTaskDTO extends TaskIdDTO {
89
+ @Expect(DTO.String().trim().strict(false).min(1).max(200).optional())
90
+ public title?: string
91
+
92
+ @Expect(DTO.Boolean().optional())
93
+ public done?: boolean
94
+ }
95
+
96
+ /**
97
+ * A page of the tasks (limit, offset), the done ones or the others
98
+ */
99
+ export class TaskSearchDTO extends PaginationSearchDTO {
100
+ @Expect(DTO.Boolean().strict(false).optional())
101
+ public done?: boolean
102
+ }
103
+ ```
104
+
105
+ Each action validates its input with a DTO before it runs, and refuses invalid input with a 400 error
106
+ ([DTOValidation](../DTOValidation.md)):
107
+
108
+ - the input of an HTTP action merges the route parameters, the body and the query string;
109
+ - the route parameters and the query string are text: `.strict(false)` converts `'1'` to the number 1 and `'false'` to
110
+ the boolean, which the schemas otherwise refuse;
111
+ - `.trim()` removes the spaces around the title only with `.strict(false)`: a strict schema refuses them instead;
112
+ - `TaskSearchDTO` extends `PaginationSearchDTO` (from `lakutata/dtos`), whose `limit` (10 by default) and `offset` (0)
113
+ select a page.
114
+
115
+ ## 4. The exception
116
+
117
+ **`packages/app/src/exceptions/TaskNotFoundException.ts`**
118
+
119
+ ```typescript
120
+ import {Exception} from 'lakutata'
121
+
122
+ /**
123
+ * The task of the request does not exist
124
+ */
125
+ export class TaskNotFoundException extends Exception {
126
+ public errno: string = 'E_TASK_NOT_FOUND'
127
+
128
+ public readonly statusCode: number = 404
129
+ }
130
+ ```
131
+
132
+ An exception has an `errno`, the code that clients test, and the HTTP status it is answered with
133
+ ([Exceptions](../Exceptions.md)). Its message is given when it is thrown.
134
+
135
+ ## 5. The controller
136
+
137
+ **`packages/app/src/controllers/TaskController.ts`**
138
+
139
+ ```typescript
140
+ import {Controller} from 'lakutata/com/entrypoint'
141
+ import {type Database} from 'lakutata/com/database'
142
+ import {DELETE, GET, PATCH, POST, Prefix} from 'lakutata/decorator/ctrl'
143
+ import {Inject} from 'lakutata/decorator/di'
144
+ import {type PaginationResultDTO, paginator} from 'lakutata/dtos'
145
+ import {type Repository} from 'lakutata/orm'
146
+ import {Task} from '../entities/Task.js'
147
+ import {CreateTaskDTO, TaskIdDTO, TaskSearchDTO, UpdateTaskDTO} from '../dtos/TaskDTO.js'
148
+ import {TaskNotFoundException} from '../exceptions/TaskNotFoundException.js'
149
+
150
+ @Prefix({route: '/tasks'})
151
+ export class TaskController extends Controller {
152
+
153
+ /**
154
+ * The database of the service (the component "database" of its options)
155
+ */
156
+ @Inject('database')
157
+ protected readonly database: Database
158
+
159
+ /**
160
+ * The tasks of the database
161
+ */
162
+ protected get tasks(): Repository<Task> {
163
+ return this.database.getRepository(Task)
164
+ }
165
+
166
+ /**
167
+ * A task, or a 404 error
168
+ * @param id
169
+ */
170
+ protected async find(id: number): Promise<Task> {
171
+ const task: Task | null = await this.tasks.findOneBy({id: id})
172
+ if (!task) throw new TaskNotFoundException(`The task ${id} does not exist`)
173
+ return task
174
+ }
175
+
176
+ /**
177
+ * GET /tasks?done=false&limit=10&offset=0
178
+ * @param input
179
+ */
180
+ @GET('/', TaskSearchDTO)
181
+ public async list(input: TaskSearchDTO): Promise<PaginationResultDTO<Task>> {
182
+ return await paginator(input, async (limit: number, offset: number) => {
183
+ const [items, total] = await this.tasks.findAndCount({
184
+ where: input.done === undefined ? {} : {done: input.done},
185
+ order: {id: 'ASC'},
186
+ take: limit,
187
+ skip: offset
188
+ })
189
+ return {items: items, total: total}
190
+ })
191
+ }
192
+
193
+ /**
194
+ * GET /tasks/1
195
+ * @param input
196
+ */
197
+ @GET('/:id', TaskIdDTO)
198
+ public async get(input: TaskIdDTO): Promise<Task> {
199
+ return await this.find(input.id)
200
+ }
201
+
202
+ /**
203
+ * POST /tasks {"title": "Write the report"}
204
+ * @param input
205
+ */
206
+ @POST('/', CreateTaskDTO)
207
+ public async create(input: CreateTaskDTO): Promise<Task> {
208
+ return await this.tasks.save(this.tasks.create({title: input.title}))
209
+ }
210
+
211
+ /**
212
+ * PATCH /tasks/1 {"done": true}
213
+ * @param input
214
+ */
215
+ @PATCH('/:id', UpdateTaskDTO)
216
+ public async update(input: UpdateTaskDTO): Promise<Task> {
217
+ const task: Task = await this.find(input.id)
218
+ if (input.title !== undefined) task.title = input.title
219
+ if (input.done !== undefined) task.done = input.done
220
+ return await this.tasks.save(task)
221
+ }
222
+
223
+ /**
224
+ * DELETE /tasks/1
225
+ * @param input
226
+ */
227
+ @DELETE('/:id', TaskIdDTO)
228
+ public async remove(input: TaskIdDTO): Promise<void> {
229
+ await this.tasks.remove(await this.find(input.id))
230
+ }
231
+ }
232
+ ```
233
+
234
+ - `@Prefix({route: '/tasks'})` puts the routes of the controller under `/tasks`; each action has its verb, its route
235
+ and its DTO ([HTTP](../HTTP.md)).
236
+ - `@Inject('database')` gives the controller the `database` component of the service: a TypeORM connection, whose
237
+ repositories read and write the entities ([Database](../Database.md)).
238
+ - `paginator()` (from `lakutata/dtos`) builds the page: the items, and `meta` with their count, the total, the limit
239
+ and the offset.
240
+ - An action returns its result; a thrown exception is the error of the request.
241
+
242
+ ## 6. The options of the service
243
+
244
+ **`packages/app/src/config/Config.ts`**
245
+
246
+ ```typescript
247
+ import path from 'node:path'
248
+ import type {ApplicationOptions} from 'lakutata'
249
+ import {buildDatabaseOptions} from 'lakutata/com/database'
250
+ import {buildEntrypoints} from 'lakutata/com/entrypoint'
251
+ import {expressHTTPEntrypoint, jsonEnvelope} from 'lakutata/adapter/express'
252
+ import compression from 'compression'
253
+ import {authenticate} from '../middlewares/Authenticate.js'
254
+ import {HealthController} from '../controllers/HealthController.js'
255
+ import {TaskController} from '../controllers/TaskController.js'
256
+ import {Task} from '../entities/Task.js'
257
+
258
+ /**
259
+ * The options of the service
260
+ * @param port the port of the HTTP server, the PORT environment variable or 8080 by default
261
+ * @param database the file of the SQLite database, the DATABASE environment variable or tasks.sqlite by default
262
+ * (":memory:" for a database in memory)
263
+ */
264
+ export function config(port: number = Number(process.env.PORT ?? 8080), database: string = process.env.DATABASE ?? 'tasks.sqlite'): ApplicationOptions {
265
+ return {
266
+ id: 'tasks',
267
+ name: 'tasks',
268
+ mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
269
+ components: {
270
+ database: buildDatabaseOptions({
271
+ type: 'better-sqlite3',
272
+ database: database,
273
+ entities: [Task],
274
+ //The migrations compiled with the service (build/migrations), run when it connects
275
+ migrations: [path.join(import.meta.dirname, '..', 'migrations', '*.js')],
276
+ migrationsRun: true
277
+ }),
278
+ entrypoint: buildEntrypoints({
279
+ controllers: [HealthController, TaskController],
280
+ http: expressHTTPEntrypoint({
281
+ port: port,
282
+ //The results and the errors are {errno, errMsg, data}; the errors keep their status (400, 404…)
283
+ respond: jsonEnvelope(),
284
+ body: {limit: '1mb'},
285
+ server: {keepAliveTimeout: 60000, headersTimeout: 65000},
286
+ middlewares: [authenticate],
287
+ before: (app): void => void app.use(compression())
288
+ })
289
+ })
290
+ },
291
+ //The database is connected (and migrated) before the HTTP server listens
292
+ bootstrap: ['database', 'entrypoint']
293
+ }
294
+ }
295
+ ```
296
+
297
+ This is the `Config.ts` of the template, with:
298
+
299
+ - the `database` component: the SQLite file (`tasks.sqlite` in the directory the service runs in, or the `DATABASE`
300
+ environment variable), its entities, and its migrations, run when it connects;
301
+ - the `TaskController` instead of the example controller;
302
+ - `jsonEnvelope()` without `errorStatus`: the results and the errors are `{errno, errMsg, data}`, and the errors keep
303
+ their status (400, 401, 404), which REST clients expect;
304
+ - a body of 1 MB at most (the template allows 100 MB), more than a task needs;
305
+ - `bootstrap: ['database', 'entrypoint']`: the database is connected and migrated before the HTTP server listens.
306
+
307
+ The template's middleware `Authenticate.ts` stays: when the `API_TOKEN` environment variable is set, the requests must
308
+ carry `Authorization: Bearer <API_TOKEN>`.
309
+
310
+ ## 7. The migration
311
+
312
+ The table is created by a migration, generated from the entities:
313
+
314
+ ```bash
315
+ npm run migration:generate
316
+ ```
317
+
318
+ ```
319
+ ✔ Migrations generated: packages/app/src/migrations/1792247417843-database.ts
320
+ ```
321
+
322
+ The command builds and starts the service, compares its entities with the database, and writes the migration (its
323
+ name holds the time it is generated):
324
+
325
+ ```typescript
326
+ import { MigrationInterface, QueryRunner } from "lakutata/orm";
327
+
328
+ export class Migrations1792247417843 implements MigrationInterface {
329
+ name = 'Migrations1792247417843'
330
+
331
+ public async up(queryRunner: QueryRunner): Promise<void> {
332
+ await queryRunner.query(`
333
+ CREATE TABLE "tasks" (
334
+ "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
335
+ "title" varchar(200) NOT NULL,
336
+ "done" boolean NOT NULL DEFAULT (0),
337
+ "createdAt" datetime NOT NULL DEFAULT (datetime('now'))
338
+ )
339
+ `);
340
+ }
341
+
342
+ public async down(queryRunner: QueryRunner): Promise<void> {
343
+ await queryRunner.query(`
344
+ DROP TABLE "tasks"
345
+ `);
346
+ }
347
+
348
+ }
349
+ ```
350
+
351
+ It is compiled with the service, and run when the service connects to a database that has not run it. After a change
352
+ of the entities, generate the next one the same way ([Migrations](../Migrations.md)).
353
+
354
+ ## 8. Run it
355
+
356
+ ```bash
357
+ npm run dev
358
+ ```
359
+
360
+ In another terminal:
361
+
362
+ ```bash
363
+ curl -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d '{"title": "Write the report"}'
364
+ # {"errno":null,"errMsg":null,"data":{"title":"Write the report","id":1,"done":false,"createdAt":"2026-10-17T14:33:07.000Z"}}
365
+
366
+ curl -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d '{"title": "Book the room"}'
367
+ curl -X PATCH localhost:8080/tasks/1 -H 'Content-Type: application/json' -d '{"done": true}'
368
+
369
+ curl 'localhost:8080/tasks?done=false&limit=5'
370
+ # {"errno":null,"errMsg":null,"data":{"items":[{"id":2,"title":"Book the room","done":false,"createdAt":"2026-10-17T14:33:07.000Z"}],"meta":{"count":1,"total":1,"limit":5,"offset":0}}}
371
+
372
+ curl -i localhost:8080/tasks/9
373
+ # HTTP/1.1 404 Not Found
374
+ # {"errno":"E_TASK_NOT_FOUND","errMsg":"The task 9 does not exist","data":null}
375
+
376
+ curl -i -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d '{"title": ""}'
377
+ # HTTP/1.1 400 Bad Request
378
+ # {"errno":"E_INVALID_VALUE","errMsg":"\"title\" is not allowed to be empty","data":null}
379
+ ```
380
+
381
+ ## 9. Test it
382
+
383
+ The test of the template is replaced by a test of the tasks. It starts the service with a database in memory, migrated
384
+ when it starts, and calls its routes:
385
+
386
+ **`packages/app/src/tests/App.spec.ts`**
387
+
388
+ ```typescript
389
+ import {after, before, describe, it} from 'node:test'
390
+ import assert from 'node:assert'
391
+ import {Application} from 'lakutata'
392
+ import {config} from '../config/Config.js'
393
+
394
+ const PORT: number = Number(process.env.TEST_PORT ?? 18081)
395
+
396
+ /**
397
+ * A request to the service: its status and its {errno, errMsg, data}
398
+ * @param method
399
+ * @param route
400
+ * @param body
401
+ */
402
+ async function request(method: string, route: string, body?: unknown): Promise<{ status: number, errno: string | null, data: any }> {
403
+ const response: Response = await fetch(`http://127.0.0.1:${PORT}${route}`, {
404
+ method: method,
405
+ headers: {'Authorization': 'Bearer test-token', 'Content-Type': 'application/json'},
406
+ body: body === undefined ? undefined : JSON.stringify(body)
407
+ })
408
+ const envelope = await response.json() as { errno: string | null, data: any }
409
+ return {status: response.status, errno: envelope.errno, data: envelope.data}
410
+ }
411
+
412
+ describe('tasks', (): void => {
413
+
414
+ let app: Application
415
+
416
+ before(async (): Promise<void> => {
417
+ process.env.API_TOKEN = 'test-token'
418
+ //A database in memory, migrated when the service starts
419
+ app = await new Promise<Application>((resolve: (app: Application) => void): void => {
420
+ Application.run(config(PORT, ':memory:')).onLaunched((launched: Application): void => resolve(launched))
421
+ })
422
+ })
423
+
424
+ after(async (): Promise<void> => {
425
+ await app?.dispose()
426
+ })
427
+
428
+ it('answers the health check without authentication', async (): Promise<void> => {
429
+ assert.deepStrictEqual(await (await fetch(`http://127.0.0.1:${PORT}/health`)).json(), {errno: null, errMsg: null, data: {status: 'ok'}})
430
+ })
431
+
432
+ it('refuses the requests without authentication', async (): Promise<void> => {
433
+ const response: Response = await fetch(`http://127.0.0.1:${PORT}/tasks`)
434
+ assert.strictEqual(response.status, 401)
435
+ })
436
+
437
+ it('creates, lists, updates and deletes the tasks', async (): Promise<void> => {
438
+ const created = await request('POST', '/tasks', {title: 'Write the report'})
439
+ assert.strictEqual(created.status, 200)
440
+ assert.strictEqual(created.data.title, 'Write the report')
441
+ assert.strictEqual(created.data.done, false)
442
+ await request('POST', '/tasks', {title: 'Book the room'})
443
+ const updated = await request('PATCH', `/tasks/${created.data.id}`, {done: true})
444
+ assert.strictEqual(updated.data.done, true)
445
+ const open = await request('GET', '/tasks?done=false&limit=5')
446
+ assert.deepStrictEqual(open.data.items.map((task: { title: string }): string => task.title), ['Book the room'])
447
+ assert.deepStrictEqual(open.data.meta, {count: 1, total: 1, limit: 5, offset: 0})
448
+ assert.strictEqual((await request('DELETE', `/tasks/${created.data.id}`)).status, 200)
449
+ const missing = await request('GET', `/tasks/${created.data.id}`)
450
+ assert.strictEqual(missing.status, 404)
451
+ assert.strictEqual(missing.errno, 'E_TASK_NOT_FOUND')
452
+ })
453
+
454
+ it('refuses an invalid task', async (): Promise<void> => {
455
+ const invalid = await request('POST', '/tasks', {title: ''})
456
+ assert.strictEqual(invalid.status, 400)
457
+ assert.strictEqual(invalid.errno, 'E_INVALID_VALUE')
458
+ })
459
+ })
460
+ ```
461
+
462
+ ```bash
463
+ npm test
464
+ ```
465
+
466
+ ```
467
+ app │ ✔ answers the health check without authentication
468
+ app │ ✔ refuses the requests without authentication
469
+ app │ ✔ creates, lists, updates and deletes the tasks
470
+ app │ ✔ refuses an invalid task
471
+ app │ ℹ pass 4
472
+ ✔ test ran in app
473
+ ```
474
+
475
+ ## 10. In production
476
+
477
+ - `npm start` runs the built service (`NODE_ENV=production` sets its mode), and `npm run docker:build` makes its image
478
+ (the template's `Dockerfile`). Give `DATABASE` a path on a volume, so that the tasks outlive the container.
479
+ - The `installer` feature of the template (`lakutata create --with installer`) makes installers that run the service at
480
+ the start of the system ([Installers](../Installers.md)).
481
+ - Another database is another driver and other options of `buildDatabaseOptions()`; generate the migrations against
482
+ it.
483
+
484
+ <!--
485
+ check:
486
+ create: tasks service
487
+ remove: packages/app/src/controllers/ExampleController.ts packages/app/src/dtos/GreetDTO.ts
488
+ run: npm run migration:generate
489
+ run: npm test
490
+ -->