@appweaver/cli 1.3.1 → 1.4.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.
package/skill/SKILL.md CHANGED
@@ -1,707 +1,753 @@
1
- ---
2
- name: appweaver
3
- description: >
4
- Use this skill whenever the user is building, debugging, scaffolding, or
5
- asking questions about Appweaver - a web development library. Triggers
6
- include: any mention of 'Appweaver', requests to create backend server logic,
7
- configurations, resources, models, routes, services and security policy,
8
- questions about the file conventions or config system. Use this skill
9
- if @appweaver npm package or Appweaver is detected anywhere in the project
10
- structure of Node.js (TypeScript) project.
11
- ---
12
-
13
- # Appweaver skill
14
-
15
- ## Purpose
16
-
17
- Appweaver is a library for building web applications with TypeScript and Node.js (or Bun). It provides a set of tools
18
- and conventions to simplify the development process, including file-based routing, reusable UI components, and
19
- centralized configuration. It is based mainly on Fastify for web server and Prisma for database ORM. The library
20
- provides a series of factory methods used for creating resource models, services, policies, and routes with predefined
21
- defaults. It provides a CLI tool for building the application, starting a server, generating schema and types, executing
22
- migrations, running seeders, testing, and more.
23
-
24
- ## Project structure
25
-
26
- The basic file structure of the Appweaver project:
27
-
28
- - `database/` - database migrations, seeders, generated prisma client, and client used by the application
29
- - `dist/` - the output directory for transpiled JavaScript files
30
- - `public/` - publicly exposed files if static file serving is enabled
31
- - `src/features/` - main application logic structured using vertical slice architecture (VSA)
32
- - `src/resources/` - application resources (models, services, policies, and routes)
33
- - `src/types/` - application types (generated and manually created)
34
- - `src/main.ts` - the main application entrypoint
35
- - `test/e2e/` - the end-to-end tests root directory
36
- - `test/unit/` - the unit tests root directory
37
- - `.env` - override the central configuration (optional)
38
- - `.env.{env}` - override the central configuration for a specific environment (optional)
39
- - `appweaver.json` - central library configuration file
40
- - `appweaver.{env}.json` - environment specific configuration files that override the central configuration
41
- - `Dockerfile` - the dockerfile used for building a docker image for deploying the application
42
-
43
- **IMPORTANT:** `{env}` is controlled by `NODE_ENV` environment variable set before any command is executed (can also be
44
- set in the `.env` file).
45
-
46
- ## Core patterns
47
-
48
- ### Scaffolding a new application
49
-
50
- Use `create-weaver-app` to scaffold a new project. It copies a default template, installs dependencies, and generates
51
- initial Prisma schema and models.
52
-
53
- ```sh
54
- create-weaver-app <name> [description] [options]
55
- ```
56
-
57
- **Options:**
58
-
59
- | Flag | Description | Default |
60
- |-------------------|----------------------------------------------------------------|--------------|
61
- | `-o, --outputDir` | Output directory (use ./ for current working directory) | project name |
62
- | `--database` | Database type: `sqlite`, `postgresql`, `mysql`, `sqlserver` | `sqlite` |
63
- | `--host` | Hostname or IP address where the application server will bind. | 0.0.0.0 |
64
- | `--port` | Port number where the application server will listen. | 5000 |
65
- | `--agent` | The AI agent for which to configure guidelines and skill files | `claude` |
66
- | `--bun` | Use Bun as application runtime. (default is node and npm) | false |
67
- | `--skipInstall` | Skip all dependencies installation. | false |
68
- | `--noDocker` | Skip Dockerfile, Dockerfile.bun and docker-compose.yml files | false |
69
- | `--noRedis` | Skip ioredis | false |
70
- | `--noQueue` | Skip bullmq | false |
71
- | `--noMailer` | Skip nodemailer | false |
72
- | `--noCron` | Skip cron | false |
73
-
74
- **Example — PostgreSQL project without queue:**
75
-
76
- ```sh
77
- create-weaver-app MyBlogAPI "My own CMS for blogging" --database postgresql --noQueue
78
- ```
79
-
80
- This creates a `./my-blog-api` directory, installs all dependencies, and runs the initial schema and type generation.
81
- The default test runner is `jest` with `swc` transpiler.
82
-
83
- **Example — Bun project with Sqlite:**
84
-
85
- ```sh
86
- create-weaver-app BunApp "Bun application with simple API" --bun --database sqlite
87
- ```
88
-
89
- This creates a `./bun-app` directory, installs all dependencies using bun package manager, and runs the initial schema
90
- and type generation. The default test runner is `bun`.
91
-
92
- After the application is scaffolded, the following commands need to be run to finish the application setup:
93
-
94
- ```sh
95
- npx weaver migration new init # use --no-install flag if npx tries to install package
96
- npm run seed
97
- ```
98
-
99
- Or, for bun runtime:
100
-
101
- ```sh
102
- bun weaver migration new init
103
- bun run seed
104
- ```
105
-
106
- **Install scripts:** npm 12+ blocks dependency install scripts by default. The scaffolded `package.json` ships an
107
- `allowScripts` field (Bun: `trustedDependencies`) covering the packages Appweaver needs to build. Without it,
108
- `npm install` skips the native builds and `weaver generate` fails. To approve a newly added dependency, run
109
- `npm install-scripts approve <pkg>`; it writes the entry to the root `package.json`. Note that a `package.json`
110
- `allowScripts` field makes npm ignore `.npmrc` `allow-scripts` entirely.
111
-
112
- ### Creating and starting the application server
113
-
114
- The main entrypoint to the application. This function creates an application object and initializes all resources and
115
- services.
116
-
117
- Default application bootstrap:
118
-
119
- ```ts
120
- // src/main.ts
121
- import { createApp } from '@appweaver/core';
122
- import { logger } from '@appweaver/common';
123
-
124
- createApp().catch((err) => logger.error(err));
125
- ```
126
-
127
- Manually starting an application:
128
-
129
- ```ts
130
- // src/main.ts
131
- import { createApp } from '@appweaver/core';
132
- import { logger } from '@appweaver/common';
133
-
134
- const app = createApp({ autoStart: false, scanPath: './dist/my/app/path' });
135
-
136
- // custom init logic...
137
-
138
- app.start().then((address) => {
139
- logger.info(address);
140
- });
141
- ```
142
-
143
- ### Creating resources
144
-
145
- Resources are the core building blocks for a web application. There are four resource types: **model**, **service**,
146
- **routes**, and **policy**. Created and exported resources are loaded automatically on application start. Except for a
147
- resource model, other resource types are optional and do not need to be created. If a service is created, then a model
148
- must be also created. If routes are created, then service must be created. Only policy is not required for other
149
- resources.
150
-
151
- Dependency chain: **model** → **service** → **routes** → **policy**
152
-
153
- **DOS:**
154
-
155
- - Use default configuration values whenever possible
156
- - Rely on library defaults for `omit`/`pick`, ad `input`/`output` settings
157
- - Use default `mimeType` and `namePattern` patterns in file configurations unless specifically requested
158
- - Prefer storing configuration in JSON file (`appweaver.json`) over environment (`.env`) file, but prefer it for secrets
159
- - Always create all four resource configs (model, service, routes, and policy) unless specified otherwise
160
-
161
- **DON'TS:**
162
-
163
- - Don't explicitly set default values in configuration unless specifically requested
164
- - Don't override `omit`/`pick` for `read`, `create` and `update` settings unnecessarily
165
- - Don't specify `input`/`output` configurations if defaults suffice
166
- - Don't modify file's `mimeType` and `namePattern` patterns unless specifically instructed
167
- - Don't customize index arrays without an explicit requirement
168
-
169
- #### Creating a resource model
170
-
171
- Resource model defines all aspects of the domain model: database table fields, relations, files, virtual fields, CRUD
172
- data transfer objects. The exported model is used to construct Prisma schema, generate TypeScript types for all model
173
- variations, define schema for CRUD routes, and input/output arguments to resource service methods.
174
-
175
- ```ts
176
- // src/resources/product/model.ts
177
- import { createModel } from '@appweaver/core';
178
-
179
- export default createModel({
180
- name: 'Product',
181
- scalars: {
182
- title: {
183
- type: 'string',
184
- minLength: 1,
185
- maxLength: 200
186
- },
187
- price: {
188
- type: 'float',
189
- minimum: 0
190
- },
191
- status: {
192
- type: 'enum',
193
- default: 'Draft',
194
- values: ['Draft', 'Active', 'Sold']
195
- },
196
- description: {
197
- type: 'string',
198
- required: false
199
- },
200
- lastViewedAt: {
201
- type: 'dateTime',
202
- defaultGenerator: 'now()'
203
- },
204
- enabled: {
205
- type: 'boolean',
206
- default: true
207
- }
208
- },
209
- relations: {
210
- category: {
211
- model: 'Category',
212
- type: 'oneToMany',
213
- mappedBy: 'products',
214
- owner: true,
215
- output: {
216
- type: 'always'
217
- }
218
- }
219
- },
220
- files: {
221
- photo: {
222
- mimeType: 'image/*',
223
- maxSize: '2 MB',
224
- image: { quality: 80, maxWidth: 1200 }
225
- }
226
- },
227
- create: {
228
- omit: ['status']
229
- },
230
- update: {
231
- pick: ['title', 'price', 'status', 'description']
232
- },
233
- index: ['title']
234
- });
235
- ```
236
-
237
- #### Creating a resource service
238
-
239
- Resource service defines the business logic layer for a resource: lifecycle hooks (before/after create, update, delete),
240
- custom data access behavior, and text search configuration. The exported service is automatically invoked by the CRUD
241
- route handlers to perform database operations for a bound model and trigger side effects.
242
-
243
- ```ts
244
- // src/resources/product/service.ts
245
- import { createService } from '@appweaver/core';
246
-
247
- export default createService({
248
- modelName: 'Product',
249
- afterCreate: (resource) => {
250
- console.log('Product created:', resource.id);
251
- },
252
- textSearch: {
253
- title: {
254
- contains: '{input}',
255
- mode: 'insensitive'
256
- }
257
- }
258
- });
259
- ```
260
-
261
- #### Creating the resource routes
262
-
263
- Resource routes define which CRUD endpoints are exposed for a resource and how they behave: the base URL path,
264
- per-operation role and permission requirements, caching settings, rate-limiting, and which operations to include or
265
- exclude. The exported routes are registered automatically on application start and derive their request/response schemas
266
- from the resource model.
267
-
268
- ```ts
269
- // src/resources/product/routes.ts
270
- import { createRoutes } from '@appweaver/core';
271
-
272
- export default createRoutes({
273
- modelName: 'Product',
274
- find: {
275
- cache: true,
276
- roles: ['Admin', 'User'],
277
- rateLimit: {
278
- max: 100
279
- }
280
- },
281
- query: {
282
- cacheTTL: 5000
283
- },
284
- create: {
285
- permissions: ['product:create']
286
- },
287
- delete: {
288
- exclude: true
289
- }
290
- });
291
- ```
292
-
293
- #### Creating a resource policy
294
-
295
- Resource policy defines row-level security for a resource: dynamic access checks against individual resource instances,
296
- read restrictions that filter which records are visible to the requester, and file access control. The service layer
297
- evaluates the exported policy on every CRUD operation to enforce fine-grained authorization beyond a static role or
298
- permission checks.
299
-
300
- ```ts
301
- // src/resources/product/policy.ts
302
- import { createPolicy } from '@appweaver/core';
303
-
304
- export default createPolicy({
305
- modelName: 'Product',
306
- checkAccess: (user, resource, action) => resource.status === 'Draft',
307
- readRestrictions: (user, resource, action) => {
308
- enabled: true;
309
- },
310
- files: {
311
- photo: {
312
- accessType: 'public'
313
- }
314
- }
315
- });
316
- ```
317
-
318
- #### Creating an authentication model and service
319
-
320
- Use `createAuthModel` and `createAuthService` instead of `createModel`/`createService` when the resource represents an
321
- authenticatable user. They cannot be used independently! If an auth model is created, then also auth service must exist.
322
-
323
- `createAuthModel` extends the config with: `email`, `passwordHash`, `verifiedEmail`, `twoFactorAuth`, `enabled`,
324
- `logoutAt` scalars; a virtual `password` field (write-only); a `roles` relation; and an optional `apiKeys` relation
325
- (when `SECURITY_API_KEY_ENABLED` is set).
326
-
327
- `createAuthService` extends the config with automatic password hashing on create/update, an optional
328
- `registrationData` callback to customize registration payload (for OAuth2 logins its `additionalData` argument includes
329
- `firstName`, `lastName`, `avatarUrl`, and — when `SECURITY_OAUTH2_FETCH_AVATAR_ENABLED` is set — a downloaded
330
- `avatarFile`), and an optional `checkOAuth2User` callback invoked before a user is registered or authenticated via
331
- OAuth2 (return nothing to proceed, or a string/`Error`/`HttpError` to abort the login with an error).
332
-
333
- ```ts
334
- // src/resources/user/model.ts
335
- import { createAuthModel } from '@appweaver/core';
336
-
337
- export default createAuthModel({
338
- name: 'User',
339
- scalars: {
340
- name: {
341
- type: 'string',
342
- maxLength: 100
343
- }
344
- },
345
- files: {
346
- avatar: {
347
- mimeType: 'image/(png|jpeg|gif)',
348
- maxSize: '2 MB',
349
- image: { quality: 80, maxHeight: 800, fit: 'inside' }
350
- }
351
- }
352
- });
353
- ```
354
-
355
- ```ts
356
- // src/resources/user/service.ts
357
- import { createAuthService } from '@appweaver/core';
358
-
359
- export default createAuthService({
360
- modelName: 'User',
361
- registrationData: (_, email, password) => ({ email, password, roles: [1, 2] })
362
- });
363
- ```
364
-
365
- #### Querying resources with filters
366
-
367
- The `filter` argument of the `query`, `aggregate`, and `export` service methods (and of the matching `POST /query`,
368
- `POST /aggregate`, `POST /export` routes) mirrors the WHERE part of a database query. It combines `_`-prefixed operators
369
- with plain value shorthands and nests through relations:
370
-
371
- - **Logical**: `_and`, `_or`, `_not`, `_nor` — take a filter object (each entry becomes one condition) or a list of
372
- filter objects.
373
- - **Comparison**: `_eq`, `_ne`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_nin`, `_between`, `_like`, `_ilike`, `_starts`,
374
- `_ends`, `_contains`, `_exists`, `_not`. Operators combined in one object must all match.
375
- - **List fields**: `_has`, `_hasSome`, `_hasEvery`, `_isEmpty`.
376
- - **Relations**: `_some`, `_every`, `_none` for list relations, `_exists` for any relation.
377
- - **Shorthands**: a bare value matches by equality, a list by inclusion, a two-value list on a numeric or date field as
378
- an inclusive range, and a bare value or list on a relation matches by id.
379
-
380
- ```ts
381
- import { injectService } from '@appweaver/core';
382
- import { UserQuery } from '@/types/generated';
383
-
384
- const filter: UserQuery = {
385
- _and: {
386
- firstName: { _eq: 'John', _exists: true },
387
- avatar: { _or: { title: { _eq: 'Avatar' }, description: { _like: '%avatar%' } } }
388
- },
389
- _or: [{ firstName: { _like: 'Jo%' } }, { lastName: 'Doe' }],
390
- roles: { _some: { name: { _contains: 'Admin' } } }
391
- };
392
-
393
- const users = await injectService('User').query(filter, 1, 50, '-createdAt,id');
394
- ```
395
-
396
- Filters are typed by `QueryFilter<T>` from `@appweaver/common`, and `weaver generate` emits a
397
- `<Model>Query = QueryFilter<Model>` alias per model. Over HTTP, they are validated against a generated per-model
398
- `<Model>QueryFilter` JSON schema, which strips unknown and hidden fields.
399
-
400
- ### Sorting
401
-
402
- The `sort` argument of `query` and `export`, and the `sort` property of the `POST /query` and `POST /export` bodies,
403
- accept either a comma-separated field list, where a `-` prefix sorts descending, or an object of `asc` and `desc` field
404
- directions. Both sort by a field of an included to-one relation and by the record count of a to-many relation:
405
-
406
- ```ts
407
- await injectService('Post').query({}, 1, 50, '-author.createdAt,tagsCount,id');
408
- await injectService('Post').query({}, 1, 50, {
409
- author: { createdAt: 'desc' },
410
- tagsCount: 'asc',
411
- id: 'asc'
412
- });
413
- ```
414
-
415
- A hidden, virtual, or array scalar field, a field of a to-many relation, or a relation the action does not include is
416
- rejected with a `400` error. Sort inputs are typed by `QuerySort<T>` from `@appweaver/common`, with a `<Model>Sort`
417
- alias emitted per model and validated over HTTP against a generated `<Model>QuerySort` JSON schema. The default is
418
- `-createdAt,id`.
419
-
420
- ### Aggregating
421
-
422
- The required `select` argument of `aggregate` (and of the `POST /aggregate` body) holds the operators to apply per
423
- field. Only the numeric fields (`count`, `sum`, `avg`, `min`, `max`, `first`, `last`), the date fields (all but `sum`
424
- and `avg`), and the numeric `id` and audit fields of the model can be aggregated:
425
-
426
- ```ts
427
- await injectService('Post').aggregate({}, {
428
- counter: { count: true, sum: true, avg: true, first: true, last: true },
429
- publishedAt: { min: true, max: true }
430
- }, 'createdAt', '2026-01-01T00:00:00.000Z', '2026-01-08T00:00:00.000Z');
431
- ```
432
-
433
- `first` and `last` take the value held by the earliest and the latest record of a period, ordered by the aggregated
434
- `dateField` (ties broken by `id`). The database cannot aggregate them, so each period requesting them costs up to two
435
- additional queries, skipped for the periods holding no record.
436
-
437
- Any other field (string, boolean, enum, JSON, array, hidden, virtual, or a relation), an operator its type does not
438
- support, an empty selection, or a `dateField` that is not a date field is rejected with a `400` error. Selections are
439
- typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per model, and validated
440
- over HTTP against a generated `<Model>AggregateSelect` JSON schema. The response stays untyped JSON, since its shape
441
- follows the selection.
442
-
443
- ### Registering a custom route
444
-
445
- Use `registerRoute` to register a custom [Fastify route](https://fastify.dev/docs/latest/Reference/Routes/) handler. The
446
- handler is a Fastify plugin function that defines one or more routes. An optional config object controls authentication,
447
- caching, and reCAPTCHA behavior. When a custom route's 2xx response schema references resource output models (`<Name>`,
448
- `<Name>Single` or `<Name>Multiple` — directly or nested inside custom schemas), virtual field values (e.g. `File.url`)
449
- are projected onto the response payload automatically before serialization.
450
-
451
- ```ts
452
- // src/plugins/custom-route.ts
453
- import { registerRoute, Router } from '@appweaver/core';
454
- import { Type } from '@sinclair/typebox';
455
-
456
- registerRoute(
457
- async function (router: Router) {
458
- router.get('/search-result', {
459
- schema: {
460
- summary: 'Sample search result response route',
461
- response: {
462
- 200: Type.Ref('SearchResult')
463
- }
464
- },
465
- handler: async () => {
466
- return { message: 'Hello, world!' };
467
- }
468
- });
469
- },
470
- { public: true, cacheTTL: 15000 }
471
- );
472
- ```
473
-
474
- ### Registering a custom model
475
-
476
- Use `registerModel` to register a custom [TypeBox](https://github.com/sinclairzx81/typebox) schema as a named model.
477
- Registered models are added to the schema registry and can be referenced by `$ref` in route schemas.
478
-
479
- ```ts
480
- // src/plugins/custom-model.ts
481
- import { registerModel } from '@appweaver/core';
482
- import { Type } from '@sinclair/typebox';
483
-
484
- registerModel(
485
- Type.Object(
486
- {
487
- id: Type.Number(),
488
- title: Type.String(),
489
- score: Type.Number({ minimum: 0, maximum: 1 })
490
- },
491
- { $id: 'SearchResult' }
492
- )
493
- );
494
- ```
495
-
496
- ### Registering plugin
497
-
498
- Use `registerPlugin` to register a custom [Fastify plugin](https://fastify.dev/docs/latest/Reference/Plugins/). Plugins
499
- are registered with `fastify-plugin` so their decorators and hooks are scoped to the entire server. You can declare
500
- optional dependencies on other named plugins.
501
-
502
- ```ts
503
- // src/plugins/audit-log.ts
504
- import { registerPlugin } from '@appweaver/core';
505
-
506
- registerPlugin('audit-log', async (server) => {
507
- server.addHook('onResponse', async (request, reply) => {
508
- console.log(`${request.method} ${request.url} → ${reply.statusCode}`);
509
- });
510
- });
511
- ```
512
-
513
- ### Dependency injection
514
-
515
- Use `define` to register a value or class in the app context, and `inject` to retrieve it. Class constructors are lazily
516
- instantiated as singletons on the first injection.
517
-
518
- ```ts
519
- import { Cache } from '@appweaver/common';
520
- import { define, inject } from '@appweaver/core';
521
-
522
- define(RedisCacheService, Cache); // register class under abstract token
523
- define('https://api.example.com', 'ApiBaseUrl'); // register plain value
524
-
525
- const cache = inject(Cache); // resolves singleton instance
526
- const url = inject<string>('ApiBaseUrl'); // resolves by string token
527
- ```
528
-
529
- Use `loadProvider` to dynamically load a class from a file path or npm package and register it under an abstract token.
530
- This is the standard pattern for wiring infrastructure providers in `main.ts`.
531
-
532
- ```ts
533
- import { loadProvider } from '@appweaver/core';
534
- import { Database, Cache } from '@appweaver/common';
535
-
536
- loadProvider(__dirname, config.DATABASE_PROVIDER, Database); // required provider
537
- loadProvider(__dirname, config.CACHE_PROVIDER, Cache);
538
- loadProvider(__dirname, config.MAILER_PROVIDER, Mailer, false); // optional (no error if provider cannot be loaded)
539
-
540
- const cache: Mailer | undefined = inject(Mailer, false); // optional injection
541
- ```
542
-
543
- ### Writing a seeder
544
-
545
- A seeder is a TypeScript file that must export at least one asynchronous function responsible for executing database
546
- seeding logic. Seeder files follow the same conventions as migration files: they can only be executed once, and their
547
- execution status is recorded in the database table `_seeders`. Seeders are executed in alphabetical order; therefore,
548
- the recommended naming convention is to prefix the filename with an ordinal number (e.g., `001-create-admin-user.ts`).
549
-
550
- During execution of seeder functions, the full application context is available, which means it is possible to inject
551
- any service or model previously defined in the main application logic or exported from other NPM packages.
552
-
553
- ```ts
554
- // database/seeders/001-create-admin-user.ts
555
-
556
- import { hashPassword } from '@appweaver/core';
557
- import { config, randomString } from '@appweaver/common';
558
- import { db } from '@db/client';
559
-
560
- export async function createAdminUser(): Promise<void> {
561
- await db.user.create({
562
- data: {
563
- firstName: 'Admin',
564
- lastName: 'Admin',
565
- email: 'admin@appweaver.co',
566
- phone: '01234435',
567
- roles: {
568
- connectOrCreate: [
569
- {
570
- where: { name: 'Admin' },
571
- create: {
572
- name: 'Admin',
573
- permissions: {
574
- connectOrCreate: [
575
- { where: { name: '*.read' }, create: { name: '*.read' } },
576
- { where: { name: '*.write' }, create: { name: '*.write' } }
577
- ]
578
- }
579
- }
580
- }
581
- ]
582
- }
583
- }
584
- });
585
- }
586
- ```
587
-
588
- ## Common tasks
589
-
590
- ### Build application
591
-
592
- ```sh
593
- weaver build
594
- weaver build --project tsconfig.build.json # path to tsconfig build file
595
- ```
596
-
597
- ### Start application
598
-
599
- ```sh
600
- weaver start # production
601
- weaver start --watch # development (watch mode)
602
- weaver start --project tsconfig.json # path to tsconfig file
603
- ```
604
-
605
- ### Generate types and schema
606
-
607
- ```sh
608
- weaver generate --types # TypeScript types only
609
- weaver generate --schema # Prisma schema only
610
- weaver generate --types --schema # both (same as with no option flags)
611
- ```
612
-
613
- ### Run database migrations
614
-
615
- ```sh
616
- weaver migrate # run pending migrations
617
- weaver migration new <name> # create a new migration
618
- weaver migration reset # reset database (prompts confirmation)
619
- weaver migration reset --force --yes # force reset, skip confirmation
620
- ```
621
-
622
- ### Seed the database
623
-
624
- ```sh
625
- weaver seed # run seeders
626
- weaver seed --buildProject # build project first, then run seeders
627
- weaver seed --continueOnError # continue if a seeder throws error
628
- weaver seed --fixWarnings # fix all warnings like invalid checksum or missing seeder
629
- weaver seed --project tsconfig.build.json # path to tsconfig build file
630
- ```
631
-
632
- ### Generate OpenAPI specification
633
-
634
- ```sh
635
- weaver openapi # generate schema to ./openapi.json
636
- weaver openapi --outputPath ./generated/openapi.json # generate schema to a custom path
637
- weaver openapi --format yaml # generate schema in yaml format
638
- ```
639
-
640
- ### Update Appweaver packages
641
-
642
- ```sh
643
- weaver update # update all @appweaver/* packages to latest
644
- weaver update @appweaver/core @appweaver/cli # update specific packages
645
- weaver update --targetVersion 1.2.3 # update to a specific version
646
- weaver update --noSkill # skip updating AI agent skill files (.claude, .agents, …)
647
- weaver update --force # force update despite peerDependency mismatches
648
- ```
649
-
650
- ### Run tests
651
-
652
- ```sh
653
- npm run test # unit tests with coverage
654
- npm run e2e # e2e tests
655
- ```
656
-
657
- Test files must use the **`.test.ts`** extension. Place unit tests in `test/unit/` and end-to-end tests in `test/e2e/`,
658
- naming each file after its module. Add or update tests whenever a feature is added or existing behaviour changes.
659
-
660
- The e2e setup and teardown are wired automatically, but **each e2e test file must register the per-file database reset
661
- itself**, after the hook that stops the application:
662
-
663
- ```ts
664
- import { resetTestData } from './support/reset';
665
-
666
- describe('My e2e test', () => {
667
- let app: Application;
668
-
669
- beforeAll(async () => {
670
- app = await createApp({ autoStartServer: false });
671
- });
672
-
673
- afterAll(async () => {
674
- await app.stop();
675
- });
676
-
677
- afterAll(resetTestData, 10_000);
678
- });
679
- ```
680
-
681
- ### Format code
682
-
683
- ```sh
684
- npm run format # prettier --write "./**/*.ts"
685
- ```
686
-
687
- ### Lint code
688
-
689
- ```sh
690
- npm run lint # eslint "./**/*.ts"
691
- ```
692
-
693
- ## References
694
-
695
- - Application CLI (weaver): [cli.md](references/cli.md)
696
- - Application configuration: [configuration.md](references/configuration.md)
697
- - Application resources: [resources.md](references/resources.md)
698
- - Dependency injection: [dependency-injection.md](references/dependency-injection.md)
699
- - Security details: [security.md](references/security.md)
700
- - Storage & File management: [storage.md](references/storage.md)
701
- - Database & Migrations: [database.md](references/database.md)
702
- - Events & Hooks: [events.md](references/events.md)
703
- - Cache management: [cache.md](references/cache.md)
704
- - Queue jobs: [queue.md](references/queue.md)
705
- - Scheduling jobs: [scheduler.md](references/scheduler.md)
706
- - Sending emails: [mailer.md](references/mailer.md)
707
- - Generating an HTTP client for using API: [client.md](references/client.md)
1
+ ---
2
+ name: appweaver
3
+ description: >
4
+ Use this skill whenever the user is building, debugging, scaffolding, or
5
+ asking questions about Appweaver - a web development library. Triggers
6
+ include: any mention of 'Appweaver', requests to create backend server logic,
7
+ configurations, resources, models, routes, services and security policy,
8
+ questions about the file conventions or config system. Use this skill
9
+ if @appweaver npm package or Appweaver is detected anywhere in the project
10
+ structure of Node.js (TypeScript) project.
11
+ ---
12
+
13
+ # Appweaver skill
14
+
15
+ ## Purpose
16
+
17
+ Appweaver is a library for building web applications with TypeScript and Node.js (or Bun). It provides a set of tools
18
+ and conventions to simplify the development process, including file-based routing, reusable UI components, and
19
+ centralized configuration. It is based mainly on Fastify for web server and Prisma for database ORM. The library
20
+ provides a series of factory methods used for creating resource models, services, policies, and routes with predefined
21
+ defaults. It provides a CLI tool for building the application, starting a server, generating schema and types, executing
22
+ migrations, running seeders, testing, and more.
23
+
24
+ ## Project structure
25
+
26
+ The basic file structure of the Appweaver project:
27
+
28
+ - `database/` - database migrations, seeders, generated prisma client, and client used by the application
29
+ - `dist/` - the output directory for transpiled JavaScript files
30
+ - `public/` - publicly exposed files if static file serving is enabled
31
+ - `src/features/` - main application logic structured using vertical slice architecture (VSA)
32
+ - `src/resources/` - application resources (models, services, policies, and routes)
33
+ - `src/types/` - application types (generated and manually created)
34
+ - `src/main.ts` - the main application entrypoint
35
+ - `test/e2e/` - the end-to-end tests root directory
36
+ - `test/unit/` - the unit tests root directory
37
+ - `.env` - override the central configuration (optional)
38
+ - `.env.{env}` - override the central configuration for a specific environment (optional)
39
+ - `appweaver.json` - central library configuration file
40
+ - `appweaver.{env}.json` - environment specific configuration files that override the central configuration
41
+ - `Dockerfile` - the dockerfile used for building a docker image for deploying the application
42
+
43
+ **IMPORTANT:** `{env}` is controlled by `NODE_ENV` environment variable set before any command is executed (can also be
44
+ set in the `.env` file).
45
+
46
+ ## Core patterns
47
+
48
+ ### Scaffolding a new application
49
+
50
+ Use `create-weaver-app` to scaffold a new project. It copies a default template, installs dependencies, and generates
51
+ initial Prisma schema and models.
52
+
53
+ ```sh
54
+ create-weaver-app <name> [description] [options]
55
+ ```
56
+
57
+ **Options:**
58
+
59
+ | Flag | Description | Default |
60
+ |-------------------|----------------------------------------------------------------|--------------|
61
+ | `-o, --outputDir` | Output directory (use ./ for current working directory) | project name |
62
+ | `--database` | Database type: `sqlite`, `postgresql`, `mysql`, `sqlserver` | `sqlite` |
63
+ | `--host` | Hostname or IP address where the application server will bind. | 0.0.0.0 |
64
+ | `--port` | Port number where the application server will listen. | 5000 |
65
+ | `--agent` | The AI agent for which to configure guidelines and skill files | `claude` |
66
+ | `--bun` | Use Bun as application runtime. (default is node and npm) | false |
67
+ | `--skipInstall` | Skip all dependencies installation. | false |
68
+ | `--noDocker` | Skip Dockerfile, Dockerfile.bun and docker-compose.yml files | false |
69
+ | `--noRedis` | Skip ioredis | false |
70
+ | `--noQueue` | Skip bullmq | false |
71
+ | `--noMailer` | Skip nodemailer | false |
72
+ | `--noCron` | Skip cron | false |
73
+
74
+ **Example — PostgreSQL project without queue:**
75
+
76
+ ```sh
77
+ create-weaver-app MyBlogAPI "My own CMS for blogging" --database postgresql --noQueue
78
+ ```
79
+
80
+ This creates a `./my-blog-api` directory, installs all dependencies, and runs the initial schema and type generation.
81
+ The default test runner is `jest` with `swc` transpiler.
82
+
83
+ **Example — Bun project with Sqlite:**
84
+
85
+ ```sh
86
+ create-weaver-app BunApp "Bun application with simple API" --bun --database sqlite
87
+ ```
88
+
89
+ This creates a `./bun-app` directory, installs all dependencies using bun package manager, and runs the initial schema
90
+ and type generation. The default test runner is `bun`.
91
+
92
+ After the application is scaffolded, the following commands need to be run to finish the application setup:
93
+
94
+ ```sh
95
+ npx weaver migration new init # use --no-install flag if npx tries to install package
96
+ npm run seed
97
+ ```
98
+
99
+ Or, for bun runtime:
100
+
101
+ ```sh
102
+ bun weaver migration new init
103
+ bun run seed
104
+ ```
105
+
106
+ **Install scripts:** npm 12+ blocks dependency install scripts by default. The scaffolded `package.json` ships an
107
+ `allowScripts` field (Bun: `trustedDependencies`) covering the packages Appweaver needs to build. Without it,
108
+ `npm install` skips the native builds and `weaver generate` fails. To approve a newly added dependency, run
109
+ `npm install-scripts approve <pkg>`; it writes the entry to the root `package.json`. Note that a `package.json`
110
+ `allowScripts` field makes npm ignore `.npmrc` `allow-scripts` entirely.
111
+
112
+ ### Creating and starting the application server
113
+
114
+ The main entrypoint to the application. This function creates an application object and initializes all resources and
115
+ services.
116
+
117
+ Default application bootstrap:
118
+
119
+ ```ts
120
+ // src/main.ts
121
+ import { createApp } from '@appweaver/core';
122
+ import { logger } from '@appweaver/common';
123
+
124
+ createApp().catch((err) => logger.error(err));
125
+ ```
126
+
127
+ Manually starting an application:
128
+
129
+ ```ts
130
+ // src/main.ts
131
+ import { createApp } from '@appweaver/core';
132
+ import { logger } from '@appweaver/common';
133
+
134
+ const app = createApp({ autoStart: false, scanPath: './dist/my/app/path' });
135
+
136
+ // custom init logic...
137
+
138
+ app.start().then((address) => {
139
+ logger.info(address);
140
+ });
141
+ ```
142
+
143
+ ### Creating resources
144
+
145
+ Resources are the core building blocks for a web application. There are four resource types: **model**, **service**,
146
+ **routes**, and **policy**. Created and exported resources are loaded automatically on application start. Except for a
147
+ resource model, other resource types are optional and do not need to be created. If a service is created, then a model
148
+ must be also created. If routes are created, then service must be created. Only policy is not required for other
149
+ resources.
150
+
151
+ Dependency chain: **model** → **service** → **routes** → **policy**
152
+
153
+ **DOS:**
154
+
155
+ - Use default configuration values whenever possible
156
+ - Rely on library defaults for `omit`/`pick`, ad `input`/`output` settings
157
+ - Use default `mimeType` and `namePattern` patterns in file configurations unless specifically requested
158
+ - Prefer storing configuration in JSON file (`appweaver.json`) over environment (`.env`) file, but prefer it for secrets
159
+ - Always create all four resource configs (model, service, routes, and policy) unless specified otherwise
160
+
161
+ **DON'TS:**
162
+
163
+ - Don't explicitly set default values in configuration unless specifically requested
164
+ - Don't override `omit`/`pick` for `read`, `create` and `update` settings unnecessarily
165
+ - Don't specify `input`/`output` configurations if defaults suffice
166
+ - Don't modify file's `mimeType` and `namePattern` patterns unless specifically instructed
167
+ - Don't customize index arrays without an explicit requirement
168
+
169
+ #### Creating a resource model
170
+
171
+ Resource model defines all aspects of the domain model: database table fields, relations, files, virtual fields, CRUD
172
+ data transfer objects. The exported model is used to construct Prisma schema, generate TypeScript types for all model
173
+ variations, define schema for CRUD routes, and input/output arguments to resource service methods.
174
+
175
+ ```ts
176
+ // src/resources/product/model.ts
177
+ import { createModel } from '@appweaver/core';
178
+
179
+ export default createModel({
180
+ name: 'Product',
181
+ scalars: {
182
+ title: {
183
+ type: 'string',
184
+ minLength: 1,
185
+ maxLength: 200
186
+ },
187
+ price: {
188
+ type: 'float',
189
+ minimum: 0
190
+ },
191
+ status: {
192
+ type: 'enum',
193
+ default: 'Draft',
194
+ values: ['Draft', 'Active', 'Sold']
195
+ },
196
+ description: {
197
+ type: 'string',
198
+ required: false
199
+ },
200
+ lastViewedAt: {
201
+ type: 'dateTime',
202
+ defaultGenerator: 'now()'
203
+ },
204
+ enabled: {
205
+ type: 'boolean',
206
+ default: true
207
+ }
208
+ },
209
+ relations: {
210
+ category: {
211
+ model: 'Category',
212
+ type: 'oneToMany',
213
+ mappedBy: 'products',
214
+ owner: true,
215
+ output: {
216
+ type: 'always'
217
+ }
218
+ }
219
+ },
220
+ files: {
221
+ photo: {
222
+ mimeType: 'image/*',
223
+ maxSize: '2 MB',
224
+ image: { quality: 80, maxWidth: 1200 }
225
+ }
226
+ },
227
+ create: {
228
+ omit: ['status']
229
+ },
230
+ update: {
231
+ pick: ['title', 'price', 'status', 'description']
232
+ },
233
+ index: ['title']
234
+ });
235
+ ```
236
+
237
+ A model has an auto-incrementing integer primary key unless the `id` block asks for a generated string one:
238
+
239
+ ```ts
240
+ export default createModel({
241
+ name: 'Comment',
242
+ id: {
243
+ type: 'string',
244
+ generator: 'cuid(2)' // or uuid(), uuid(7), cuid(), nanoid()
245
+ },
246
+ scalars: { body: { type: 'string' } }
247
+ });
248
+ ```
249
+
250
+ The choice flows through the Prisma column, the generated TypeScript type, the `:id` route path parameter, and the
251
+ relation inputs and foreign keys of every model pointing at it. Both ID types can be mixed across models.
252
+
253
+ Index entries are field names, nested in an array for a composite index. Prefix a name with `-` for a descending index
254
+ or `+` for an ascending one; without a prefix, the database default order is used:
255
+
256
+ ```ts
257
+ index: ['-createdAt', ['status', '-createdAt']]
258
+ ```
259
+
260
+ #### Creating a resource service
261
+
262
+ Resource service defines the business logic layer for a resource: lifecycle hooks (before/after create, update, delete),
263
+ custom data access behavior, and text search configuration. The exported service is automatically invoked by the CRUD
264
+ route handlers to perform database operations for a bound model and trigger side effects.
265
+
266
+ ```ts
267
+ // src/resources/product/service.ts
268
+ import { createService } from '@appweaver/core';
269
+
270
+ export default createService({
271
+ modelName: 'Product',
272
+ afterCreate: (resource) => {
273
+ console.log('Product created:', resource.id);
274
+ },
275
+ textSearch: {
276
+ title: {
277
+ contains: '{input}',
278
+ mode: 'insensitive'
279
+ }
280
+ }
281
+ });
282
+ ```
283
+
284
+ Any code can reach a resource service with `injectService`, typed by the `<Model>ResourceService` alias `weaver
285
+ generate` emits per model, so no service type has to be written by hand:
286
+
287
+ ```ts
288
+ import { injectService } from '@appweaver/core';
289
+ import { ProductResourceService } from '@/types/generated';
290
+
291
+ const products = injectService<ProductResourceService>('Product');
292
+ const product = await products.find(1);
293
+ ```
294
+
295
+ #### Creating the resource routes
296
+
297
+ Resource routes define which CRUD endpoints are exposed for a resource and how they behave: the base URL path,
298
+ per-operation role and permission requirements, caching settings, rate-limiting, and which operations to include or
299
+ exclude. The exported routes are registered automatically on application start and derive their request/response schemas
300
+ from the resource model.
301
+
302
+ ```ts
303
+ // src/resources/product/routes.ts
304
+ import { createRoutes } from '@appweaver/core';
305
+
306
+ export default createRoutes({
307
+ modelName: 'Product',
308
+ find: {
309
+ cache: true,
310
+ roles: ['Admin', 'User'],
311
+ rateLimit: {
312
+ max: 100
313
+ }
314
+ },
315
+ query: {
316
+ cacheTTL: 5000
317
+ },
318
+ create: {
319
+ permissions: ['product:create']
320
+ },
321
+ delete: {
322
+ exclude: true
323
+ }
324
+ });
325
+ ```
326
+
327
+ #### Creating a resource policy
328
+
329
+ Resource policy defines row-level security for a resource: dynamic access checks against individual resource instances,
330
+ read restrictions that filter which records are visible to the requester, and file access control. The service layer
331
+ evaluates the exported policy on every CRUD operation to enforce fine-grained authorization beyond a static role or
332
+ permission checks.
333
+
334
+ ```ts
335
+ // src/resources/product/policy.ts
336
+ import { createPolicy } from '@appweaver/core';
337
+
338
+ export default createPolicy({
339
+ modelName: 'Product',
340
+ checkAccess: (user, resource, action) => resource.status === 'Draft',
341
+ readRestrictions: (user, resource, action) => {
342
+ enabled: true;
343
+ },
344
+ files: {
345
+ photo: {
346
+ accessType: 'public'
347
+ }
348
+ }
349
+ });
350
+ ```
351
+
352
+ #### Creating an authentication model and service
353
+
354
+ Use `createAuthModel` and `createAuthService` instead of `createModel`/`createService` when the resource represents an
355
+ authenticatable user. They cannot be used independently! If an auth model is created, then also auth service must exist.
356
+
357
+ `createAuthModel` extends the config with: `email`, `passwordHash`, `verifiedEmail`, `twoFactorAuth`, `enabled`,
358
+ `logoutAt` scalars; a virtual `password` field (write-only); a `roles` relation; and an optional `apiKeys` relation
359
+ (when `SECURITY_API_KEY_ENABLED` is set).
360
+
361
+ `createAuthService` extends the config with automatic password hashing on create/update, an optional
362
+ `registrationData` callback to customize registration payload (for OAuth2 logins its `additionalData` argument includes
363
+ `firstName`, `lastName`, `avatarUrl`, and a downloaded `avatarFile` unless `SECURITY_OAUTH2_FETCH_AVATAR_ENABLED` is
364
+ turned off), an optional `registrationFiles` callback that maps the model's file fields to files stored right after
365
+ the user is created (the avatar included, since a file must be linked to an existing resource), and an optional
366
+ `checkOAuth2User` callback invoked before a user is registered or authenticated via OAuth2 (return nothing to proceed,
367
+ or a string/`Error`/`HttpError` to abort the login with an error).
368
+
369
+ ```ts
370
+ // src/resources/user/model.ts
371
+ import { createAuthModel } from '@appweaver/core';
372
+
373
+ export default createAuthModel({
374
+ name: 'User',
375
+ scalars: {
376
+ name: {
377
+ type: 'string',
378
+ maxLength: 100
379
+ }
380
+ },
381
+ files: {
382
+ avatar: {
383
+ mimeType: 'image/(png|jpeg|gif)',
384
+ maxSize: '2 MB',
385
+ image: { quality: 80, maxHeight: 800, fit: 'inside' }
386
+ }
387
+ }
388
+ });
389
+ ```
390
+
391
+ ```ts
392
+ // src/resources/user/service.ts
393
+ import { createAuthService } from '@appweaver/core';
394
+
395
+ export default createAuthService({
396
+ modelName: 'User',
397
+ registrationData: (_, email, password) => ({ email, password, roles: [1, 2] }),
398
+ registrationFiles: (_, data) => ({ avatar: data?.avatarFile })
399
+ });
400
+ ```
401
+
402
+ #### Querying resources with filters
403
+
404
+ The `filter` argument of the `query`, `aggregate`, and `export` service methods (and of the matching `POST /query`,
405
+ `POST /aggregate`, `POST /export` routes) mirrors the WHERE part of a database query. It combines `_`-prefixed operators
406
+ with plain value shorthands and nests through relations:
407
+
408
+ - **Logical**: `_and`, `_or`, `_not`, `_nor` — take a filter object (each entry becomes one condition) or a list of
409
+ filter objects.
410
+ - **Comparison**: `_eq`, `_ne`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_nin`, `_between`, `_like`, `_ilike`, `_starts`,
411
+ `_ends`, `_contains`, `_exists`, `_not`. Operators combined in one object must all match.
412
+ - **List fields**: `_has`, `_hasSome`, `_hasEvery`, `_isEmpty`.
413
+ - **Relations**: `_some`, `_every`, `_none` for list relations, `_exists` for any relation.
414
+ - **Shorthands**: a bare value matches by equality, a list by inclusion, a two-value list on a numeric or date field as
415
+ an inclusive range, and a bare value or list on a relation matches by id.
416
+
417
+ ```ts
418
+ import { injectService } from '@appweaver/core';
419
+ import { UserQuery } from '@/types/generated';
420
+
421
+ const filter: UserQuery = {
422
+ _and: {
423
+ firstName: { _eq: 'John', _exists: true },
424
+ avatar: { _or: { title: { _eq: 'Avatar' }, description: { _like: '%avatar%' } } }
425
+ },
426
+ _or: [{ firstName: { _like: 'Jo%' } }, { lastName: 'Doe' }],
427
+ roles: { _some: { name: { _contains: 'Admin' } } }
428
+ };
429
+
430
+ const users = await injectService('User').query(filter, 1, 50, '-createdAt');
431
+ ```
432
+
433
+ Filters are typed by `QueryFilter<T>` from `@appweaver/common`, and `weaver generate` emits a
434
+ `<Model>Query = QueryFilter<Model>` alias per model. Over HTTP, they are validated against a generated per-model
435
+ `<Model>QueryFilter` JSON schema, which strips unknown and hidden fields.
436
+
437
+ ### Sorting
438
+
439
+ The `sort` argument of `query` and `export`, and the `sort` property of the `POST /query` and `POST /export` bodies,
440
+ accept either a comma-separated field list, where a `-` prefix sorts descending, or an object of `asc` and `desc` field
441
+ directions. Both sort by a field of an included to-one relation and by the record count of a to-many relation:
442
+
443
+ ```ts
444
+ await injectService('Post').query({}, 1, 50, '-author.createdAt,tagsCount,id');
445
+ await injectService('Post').query({}, 1, 50, {
446
+ author: { createdAt: 'desc' },
447
+ tagsCount: 'asc',
448
+ id: 'asc'
449
+ });
450
+ ```
451
+
452
+ A hidden, virtual, or array scalar field, a field of a to-many relation, or a relation the action does not include is
453
+ rejected with a `400` error. Sort inputs are typed by `QuerySort<T>` from `@appweaver/common`, with a `<Model>Sort`
454
+ alias emitted per model and validated over HTTP against a generated `<Model>QuerySort` JSON schema. The default is
455
+ `-createdAt`, and every sort is terminated with the primary key so paging stays deterministic.
456
+
457
+ ### Aggregating
458
+
459
+ The required `select` argument of `aggregate` (and of the `POST /aggregate` body) holds the operators to apply per
460
+ field. Only the numeric fields (`count`, `sum`, `avg`, `min`, `max`, `first`, `last`), the date fields (all but `sum`
461
+ and `avg`), and the numeric `id` and audit fields of the model can be aggregated:
462
+
463
+ ```ts
464
+ await injectService('Post').aggregate({}, {
465
+ counter: { count: true, sum: true, avg: true, first: true, last: true },
466
+ publishedAt: { min: true, max: true }
467
+ }, 'createdAt', '2026-01-01T00:00:00.000Z', '2026-01-08T00:00:00.000Z');
468
+ ```
469
+
470
+ `first` and `last` take the value held by the earliest and the latest record of a period, ordered by the aggregated
471
+ `dateField` (ties broken by `id`). The database cannot aggregate them, so each period requesting them costs up to two
472
+ additional queries, skipped for the periods holding no record.
473
+
474
+ Any other field (string, boolean, enum, JSON, array, hidden, virtual, or a relation), an operator its type does not
475
+ support, an empty selection, or a `dateField` that is not a date field is rejected with a `400` error. Selections are
476
+ typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per model, and validated
477
+ over HTTP against a generated `<Model>AggregateSelect` JSON schema.
478
+
479
+ The response type is inferred from the selection, so a selection given as an object literal (or declared with
480
+ `satisfies <Model>Aggregate`) narrows it to the selected fields, while one annotated as `<Model>Aggregate` keeps every
481
+ aggregatable field of the model:
482
+
483
+ ```ts
484
+ const stats = await injectService<PostResourceService>('Post').aggregate({}, { counter: { sum: true } });
485
+ stats.total.counter?.sum; // typed
486
+ stats.total.publishedAt; // compile error, the field was not selected
487
+ ```
488
+
489
+ ### Registering a custom route
490
+
491
+ Use `registerRoute` to register a custom [Fastify route](https://fastify.dev/docs/latest/Reference/Routes/) handler. The
492
+ handler is a Fastify plugin function that defines one or more routes. An optional config object controls authentication,
493
+ caching, and reCAPTCHA behavior. When a custom route's 2xx response schema references resource output models (`<Name>`,
494
+ `<Name>Single` or `<Name>Multiple` — directly or nested inside custom schemas), virtual field values (e.g. `File.url`)
495
+ are projected onto the response payload automatically before serialization.
496
+
497
+ ```ts
498
+ // src/plugins/custom-route.ts
499
+ import { registerRoute, Router } from '@appweaver/core';
500
+ import { Type } from '@sinclair/typebox';
501
+
502
+ registerRoute(
503
+ async function (router: Router) {
504
+ router.get('/search-result', {
505
+ schema: {
506
+ summary: 'Sample search result response route',
507
+ response: {
508
+ 200: Type.Ref('SearchResult')
509
+ }
510
+ },
511
+ handler: async () => {
512
+ return { message: 'Hello, world!' };
513
+ }
514
+ });
515
+ },
516
+ { public: true, cacheTTL: 15000 }
517
+ );
518
+ ```
519
+
520
+ ### Registering a custom model
521
+
522
+ Use `registerModel` to register a custom [TypeBox](https://github.com/sinclairzx81/typebox) schema as a named model.
523
+ Registered models are added to the schema registry and can be referenced by `$ref` in route schemas.
524
+
525
+ ```ts
526
+ // src/plugins/custom-model.ts
527
+ import { registerModel } from '@appweaver/core';
528
+ import { Type } from '@sinclair/typebox';
529
+
530
+ registerModel(
531
+ Type.Object(
532
+ {
533
+ id: Type.Number(),
534
+ title: Type.String(),
535
+ score: Type.Number({ minimum: 0, maximum: 1 })
536
+ },
537
+ { $id: 'SearchResult' }
538
+ )
539
+ );
540
+ ```
541
+
542
+ ### Registering plugin
543
+
544
+ Use `registerPlugin` to register a custom [Fastify plugin](https://fastify.dev/docs/latest/Reference/Plugins/). Plugins
545
+ are registered with `fastify-plugin` so their decorators and hooks are scoped to the entire server. You can declare
546
+ optional dependencies on other named plugins.
547
+
548
+ ```ts
549
+ // src/plugins/audit-log.ts
550
+ import { registerPlugin } from '@appweaver/core';
551
+
552
+ registerPlugin('audit-log', async (server) => {
553
+ server.addHook('onResponse', async (request, reply) => {
554
+ console.log(`${request.method} ${request.url} → ${reply.statusCode}`);
555
+ });
556
+ });
557
+ ```
558
+
559
+ ### Dependency injection
560
+
561
+ Use `define` to register a value or class in the app context, and `inject` to retrieve it. Class constructors are lazily
562
+ instantiated as singletons on the first injection.
563
+
564
+ ```ts
565
+ import { Cache } from '@appweaver/common';
566
+ import { define, inject } from '@appweaver/core';
567
+
568
+ define(RedisCacheService, Cache); // register class under abstract token
569
+ define('https://api.example.com', 'ApiBaseUrl'); // register plain value
570
+
571
+ const cache = inject(Cache); // resolves singleton instance
572
+ const url = inject<string>('ApiBaseUrl'); // resolves by string token
573
+ ```
574
+
575
+ Use `loadProvider` to dynamically load a class from a file path or npm package and register it under an abstract token.
576
+ This is the standard pattern for wiring infrastructure providers in `main.ts`.
577
+
578
+ ```ts
579
+ import { loadProvider } from '@appweaver/core';
580
+ import { Database, Cache } from '@appweaver/common';
581
+
582
+ loadProvider(__dirname, config.DATABASE_PROVIDER, Database); // required provider
583
+ loadProvider(__dirname, config.CACHE_PROVIDER, Cache);
584
+ loadProvider(__dirname, config.MAILER_PROVIDER, Mailer, false); // optional (no error if provider cannot be loaded)
585
+
586
+ const cache: Mailer | undefined = inject(Mailer, false); // optional injection
587
+ ```
588
+
589
+ ### Writing a seeder
590
+
591
+ A seeder is a TypeScript file that must export at least one asynchronous function responsible for executing database
592
+ seeding logic. Seeder files follow the same conventions as migration files: they can only be executed once, and their
593
+ execution status is recorded in the database table `_seeders`. Seeders are executed in alphabetical order; therefore,
594
+ the recommended naming convention is to prefix the filename with an ordinal number (e.g., `001-create-admin-user.ts`).
595
+
596
+ During execution of seeder functions, the full application context is available, which means it is possible to inject
597
+ any service or model previously defined in the main application logic or exported from other NPM packages.
598
+
599
+ ```ts
600
+ // database/seeders/001-create-admin-user.ts
601
+
602
+ import { hashPassword } from '@appweaver/core';
603
+ import { config, randomString } from '@appweaver/common';
604
+ import { db } from '@db/client';
605
+
606
+ export async function createAdminUser(): Promise<void> {
607
+ await db.user.create({
608
+ data: {
609
+ firstName: 'Admin',
610
+ lastName: 'Admin',
611
+ email: 'admin@appweaver.co',
612
+ phone: '01234435',
613
+ roles: {
614
+ connectOrCreate: [
615
+ {
616
+ where: { name: 'Admin' },
617
+ create: {
618
+ name: 'Admin',
619
+ permissions: {
620
+ connectOrCreate: [
621
+ { where: { name: '*.read' }, create: { name: '*.read' } },
622
+ { where: { name: '*.write' }, create: { name: '*.write' } }
623
+ ]
624
+ }
625
+ }
626
+ }
627
+ ]
628
+ }
629
+ }
630
+ });
631
+ }
632
+ ```
633
+
634
+ ## Common tasks
635
+
636
+ ### Build application
637
+
638
+ ```sh
639
+ weaver build
640
+ weaver build --project tsconfig.build.json # path to tsconfig build file
641
+ ```
642
+
643
+ ### Start application
644
+
645
+ ```sh
646
+ weaver start # production
647
+ weaver start --watch # development (watch mode)
648
+ weaver start --project tsconfig.json # path to tsconfig file
649
+ ```
650
+
651
+ ### Generate types and schema
652
+
653
+ ```sh
654
+ weaver generate --types # TypeScript types only
655
+ weaver generate --schema # Prisma schema only
656
+ weaver generate --types --schema # both (same as with no option flags)
657
+ ```
658
+
659
+ ### Run database migrations
660
+
661
+ ```sh
662
+ weaver migrate # run pending migrations
663
+ weaver migration new <name> # create a new migration
664
+ weaver migration reset # reset database (prompts confirmation)
665
+ weaver migration reset --force --yes # force reset, skip confirmation
666
+ ```
667
+
668
+ ### Seed the database
669
+
670
+ ```sh
671
+ weaver seed # run seeders
672
+ weaver seed --buildProject # build project first, then run seeders
673
+ weaver seed --continueOnError # continue if a seeder throws error
674
+ weaver seed --fixWarnings # fix all warnings like invalid checksum or missing seeder
675
+ weaver seed --project tsconfig.build.json # path to tsconfig build file
676
+ ```
677
+
678
+ ### Generate OpenAPI specification
679
+
680
+ ```sh
681
+ weaver openapi # generate schema to ./openapi.json
682
+ weaver openapi --outputPath ./generated/openapi.json # generate schema to a custom path
683
+ weaver openapi --format yaml # generate schema in yaml format
684
+ ```
685
+
686
+ ### Update Appweaver packages
687
+
688
+ ```sh
689
+ weaver update # update all @appweaver/* packages to latest
690
+ weaver update @appweaver/core @appweaver/cli # update specific packages
691
+ weaver update --targetVersion 1.2.3 # update to a specific version
692
+ weaver update --noSkill # skip updating AI agent skill files (.claude, .agents, …)
693
+ weaver update --force # force update despite peerDependency mismatches
694
+ ```
695
+
696
+ ### Run tests
697
+
698
+ ```sh
699
+ npm run test # unit tests with coverage
700
+ npm run e2e # e2e tests
701
+ ```
702
+
703
+ Test files must use the **`.test.ts`** extension. Place unit tests in `test/unit/` and end-to-end tests in `test/e2e/`,
704
+ naming each file after its module. Add or update tests whenever a feature is added or existing behaviour changes.
705
+
706
+ The e2e setup and teardown are wired automatically, but **each e2e test file must register the per-file database reset
707
+ itself**, after the hook that stops the application:
708
+
709
+ ```ts
710
+ import { resetTestData } from './support/reset';
711
+
712
+ describe('My e2e test', () => {
713
+ let app: Application;
714
+
715
+ beforeAll(async () => {
716
+ app = await createApp({ autoStartServer: false });
717
+ });
718
+
719
+ afterAll(async () => {
720
+ await app.stop();
721
+ });
722
+
723
+ afterAll(resetTestData, 10_000);
724
+ });
725
+ ```
726
+
727
+ ### Format code
728
+
729
+ ```sh
730
+ npm run format # prettier --write "./**/*.ts"
731
+ ```
732
+
733
+ ### Lint code
734
+
735
+ ```sh
736
+ npm run lint # eslint "./**/*.ts"
737
+ ```
738
+
739
+ ## References
740
+
741
+ - Application CLI (weaver): [cli.md](references/cli.md)
742
+ - Application configuration: [configuration.md](references/configuration.md)
743
+ - Application resources: [resources.md](references/resources.md)
744
+ - Dependency injection: [dependency-injection.md](references/dependency-injection.md)
745
+ - Security details: [security.md](references/security.md)
746
+ - Storage & File management: [storage.md](references/storage.md)
747
+ - Database & Migrations: [database.md](references/database.md)
748
+ - Events & Hooks: [events.md](references/events.md)
749
+ - Cache management: [cache.md](references/cache.md)
750
+ - Queue jobs: [queue.md](references/queue.md)
751
+ - Scheduling jobs: [scheduler.md](references/scheduler.md)
752
+ - Sending emails: [mailer.md](references/mailer.md)
753
+ - Generating an HTTP client for using API: [client.md](references/client.md)