@appweaver/cli 1.4.0 → 1.5.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,753 +1,764 @@
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)
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
+ Foreign key columns are indexed automatically, unless an explicit index already leads with them. `unique` takes the
261
+ same shape as `index` for composite unique constraints, i.e. `unique: [['provider', 'providerAccountId']]`.
262
+
263
+ Set `softDelete: true` to keep deleted records in the database, marked by the `deletedAt` and `deletedById` columns,
264
+ instead of removing them. Soft deleted records are hidden from every read, so the API behaves exactly as after a real
265
+ delete, and a record can only be restored manually in the database. The relations cascading on delete are soft deleted
266
+ with the record, so every model a soft deleted model cascades into must enable `softDelete` too, or the application
267
+ fails to start. Stored files are removed on a regular delete but kept on a soft delete by default, configurable per
268
+ file field with `onResourceDeleted` and `onResourceSoftDeleted`. A kept file stays in the storage, e.g. for audit, but
269
+ is never served again.
270
+
271
+ #### Creating a resource service
272
+
273
+ Resource service defines the business logic layer for a resource: lifecycle hooks (before/after create, update, delete),
274
+ custom data access behavior, and text search configuration. The exported service is automatically invoked by the CRUD
275
+ route handlers to perform database operations for a bound model and trigger side effects.
276
+
277
+ ```ts
278
+ // src/resources/product/service.ts
279
+ import { createService } from '@appweaver/core';
280
+
281
+ export default createService({
282
+ modelName: 'Product',
283
+ afterCreate: (resource) => {
284
+ console.log('Product created:', resource.id);
285
+ },
286
+ textSearch: {
287
+ title: {
288
+ contains: '{input}',
289
+ mode: 'insensitive'
290
+ }
291
+ }
292
+ });
293
+ ```
294
+
295
+ Any code can reach a resource service with `injectService`, typed by the `<Model>ResourceService` alias `weaver
296
+ generate` emits per model, so no service type has to be written by hand:
297
+
298
+ ```ts
299
+ import { injectService } from '@appweaver/core';
300
+ import { ProductResourceService } from '@/types/generated';
301
+
302
+ const products = injectService<ProductResourceService>('Product');
303
+ const product = await products.find(1);
304
+ ```
305
+
306
+ #### Creating the resource routes
307
+
308
+ Resource routes define which CRUD endpoints are exposed for a resource and how they behave: the base URL path,
309
+ per-operation role and permission requirements, caching settings, rate-limiting, and which operations to include or
310
+ exclude. The exported routes are registered automatically on application start and derive their request/response schemas
311
+ from the resource model.
312
+
313
+ ```ts
314
+ // src/resources/product/routes.ts
315
+ import { createRoutes } from '@appweaver/core';
316
+
317
+ export default createRoutes({
318
+ modelName: 'Product',
319
+ find: {
320
+ cache: true,
321
+ roles: ['Admin', 'User'],
322
+ rateLimit: {
323
+ max: 100
324
+ }
325
+ },
326
+ query: {
327
+ cacheTTL: 5000
328
+ },
329
+ create: {
330
+ permissions: ['product:create']
331
+ },
332
+ delete: {
333
+ exclude: true
334
+ }
335
+ });
336
+ ```
337
+
338
+ #### Creating a resource policy
339
+
340
+ Resource policy defines row-level security for a resource: dynamic access checks against individual resource instances,
341
+ read restrictions that filter which records are visible to the requester, and file access control. The service layer
342
+ evaluates the exported policy on every CRUD operation to enforce fine-grained authorization beyond a static role or
343
+ permission checks.
344
+
345
+ ```ts
346
+ // src/resources/product/policy.ts
347
+ import { createPolicy } from '@appweaver/core';
348
+
349
+ export default createPolicy({
350
+ modelName: 'Product',
351
+ checkAccess: (user, resource, action) => resource.status === 'Draft',
352
+ readRestrictions: (user, resource, action) => {
353
+ enabled: true;
354
+ },
355
+ files: {
356
+ photo: {
357
+ accessType: 'public'
358
+ }
359
+ }
360
+ });
361
+ ```
362
+
363
+ #### Creating an authentication model and service
364
+
365
+ Use `createAuthModel` and `createAuthService` instead of `createModel`/`createService` when the resource represents an
366
+ authenticatable user. They cannot be used independently! If an auth model is created, then also auth service must exist.
367
+
368
+ `createAuthModel` extends the config with: `email`, `passwordHash`, `verifiedEmail`, `twoFactorAuth`, `enabled`,
369
+ `logoutAt` scalars; a virtual `password` field (write-only); a `roles` relation; and an optional `apiKeys` relation
370
+ (when `SECURITY_API_KEY_ENABLED` is set).
371
+
372
+ `createAuthService` extends the config with automatic password hashing on create/update, an optional
373
+ `registrationData` callback to customize registration payload (for OAuth2 logins its `additionalData` argument includes
374
+ `firstName`, `lastName`, `avatarUrl`, and a downloaded `avatarFile` unless `SECURITY_OAUTH2_FETCH_AVATAR_ENABLED` is
375
+ turned off), an optional `registrationFiles` callback that maps the model's file fields to files stored right after
376
+ the user is created (the avatar included, since a file must be linked to an existing resource), and an optional
377
+ `checkOAuth2User` callback invoked before a user is registered or authenticated via OAuth2 (return nothing to proceed,
378
+ or a string/`Error`/`HttpError` to abort the login with an error).
379
+
380
+ ```ts
381
+ // src/resources/user/model.ts
382
+ import { createAuthModel } from '@appweaver/core';
383
+
384
+ export default createAuthModel({
385
+ name: 'User',
386
+ scalars: {
387
+ name: {
388
+ type: 'string',
389
+ maxLength: 100
390
+ }
391
+ },
392
+ files: {
393
+ avatar: {
394
+ mimeType: 'image/(png|jpeg|gif)',
395
+ maxSize: '2 MB',
396
+ image: { quality: 80, maxHeight: 800, fit: 'inside' }
397
+ }
398
+ }
399
+ });
400
+ ```
401
+
402
+ ```ts
403
+ // src/resources/user/service.ts
404
+ import { createAuthService } from '@appweaver/core';
405
+
406
+ export default createAuthService({
407
+ modelName: 'User',
408
+ registrationData: (_, email, password) => ({ email, password, roles: [1, 2] }),
409
+ registrationFiles: (_, data) => ({ avatar: data?.avatarFile })
410
+ });
411
+ ```
412
+
413
+ #### Querying resources with filters
414
+
415
+ The `filter` argument of the `query`, `aggregate`, and `export` service methods (and of the matching `POST /query`,
416
+ `POST /aggregate`, `POST /export` routes) mirrors the WHERE part of a database query. It combines `_`-prefixed operators
417
+ with plain value shorthands and nests through relations:
418
+
419
+ - **Logical**: `_and`, `_or`, `_not`, `_nor` — take a filter object (each entry becomes one condition) or a list of
420
+ filter objects.
421
+ - **Comparison**: `_eq`, `_ne`, `_gt`, `_gte`, `_lt`, `_lte`, `_in`, `_nin`, `_between`, `_like`, `_ilike`, `_starts`,
422
+ `_ends`, `_contains`, `_exists`, `_not`. Operators combined in one object must all match.
423
+ - **List fields**: `_has`, `_hasSome`, `_hasEvery`, `_isEmpty`.
424
+ - **Relations**: `_some`, `_every`, `_none` for list relations, `_exists` for any relation.
425
+ - **Shorthands**: a bare value matches by equality, a list by inclusion, a two-value list on a numeric or date field as
426
+ an inclusive range, and a bare value or list on a relation matches by id.
427
+
428
+ ```ts
429
+ import { injectService } from '@appweaver/core';
430
+ import { UserQuery } from '@/types/generated';
431
+
432
+ const filter: UserQuery = {
433
+ _and: {
434
+ firstName: { _eq: 'John', _exists: true },
435
+ avatar: { _or: { title: { _eq: 'Avatar' }, description: { _like: '%avatar%' } } }
436
+ },
437
+ _or: [{ firstName: { _like: 'Jo%' } }, { lastName: 'Doe' }],
438
+ roles: { _some: { name: { _contains: 'Admin' } } }
439
+ };
440
+
441
+ const users = await injectService('User').query(filter, 1, 50, '-createdAt');
442
+ ```
443
+
444
+ Filters are typed by `QueryFilter<T>` from `@appweaver/common`, and `weaver generate` emits a
445
+ `<Model>Query = QueryFilter<Model>` alias per model. Over HTTP, they are validated against a generated per-model
446
+ `<Model>QueryFilter` JSON schema, which strips unknown and hidden fields.
447
+
448
+ ### Sorting
449
+
450
+ The `sort` argument of `query` and `export`, and the `sort` property of the `POST /query` and `POST /export` bodies,
451
+ accept either a comma-separated field list, where a `-` prefix sorts descending, or an object of `asc` and `desc` field
452
+ directions. Both sort by a field of an included to-one relation and by the record count of a to-many relation:
453
+
454
+ ```ts
455
+ await injectService('Post').query({}, 1, 50, '-author.createdAt,tagsCount,id');
456
+ await injectService('Post').query({}, 1, 50, {
457
+ author: { createdAt: 'desc' },
458
+ tagsCount: 'asc',
459
+ id: 'asc'
460
+ });
461
+ ```
462
+
463
+ A hidden, virtual, or array scalar field, a field of a to-many relation, or a relation the action does not include is
464
+ rejected with a `400` error. Sort inputs are typed by `QuerySort<T>` from `@appweaver/common`, with a `<Model>Sort`
465
+ alias emitted per model and validated over HTTP against a generated `<Model>QuerySort` JSON schema. The default is
466
+ `-createdAt`, and every sort is terminated with the primary key so paging stays deterministic.
467
+
468
+ ### Aggregating
469
+
470
+ The required `select` argument of `aggregate` (and of the `POST /aggregate` body) holds the operators to apply per
471
+ field. Only the numeric fields (`count`, `sum`, `avg`, `min`, `max`, `first`, `last`), the date fields (all but `sum`
472
+ and `avg`), and the numeric `id` and audit fields of the model can be aggregated:
473
+
474
+ ```ts
475
+ await injectService('Post').aggregate({}, {
476
+ counter: { count: true, sum: true, avg: true, first: true, last: true },
477
+ publishedAt: { min: true, max: true }
478
+ }, 'createdAt', '2026-01-01T00:00:00.000Z', '2026-01-08T00:00:00.000Z');
479
+ ```
480
+
481
+ `first` and `last` take the value held by the earliest and the latest record of a period, ordered by the aggregated
482
+ `dateField` (ties broken by `id`). The database cannot aggregate them, so each period requesting them costs up to two
483
+ additional queries, skipped for the periods holding no record.
484
+
485
+ Any other field (string, boolean, enum, JSON, array, hidden, virtual, or a relation), an operator its type does not
486
+ support, an empty selection, or a `dateField` that is not a date field is rejected with a `400` error. Selections are
487
+ typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per model, and validated
488
+ over HTTP against a generated `<Model>AggregateSelect` JSON schema.
489
+
490
+ The response type is inferred from the selection, so a selection given as an object literal (or declared with
491
+ `satisfies <Model>Aggregate`) narrows it to the selected fields, while one annotated as `<Model>Aggregate` keeps every
492
+ aggregatable field of the model:
493
+
494
+ ```ts
495
+ const stats = await injectService<PostResourceService>('Post').aggregate({}, { counter: { sum: true } });
496
+ stats.total.counter?.sum; // typed
497
+ stats.total.publishedAt; // compile error, the field was not selected
498
+ ```
499
+
500
+ ### Registering a custom route
501
+
502
+ Use `registerRoute` to register a custom [Fastify route](https://fastify.dev/docs/latest/Reference/Routes/) handler. The
503
+ handler is a Fastify plugin function that defines one or more routes. An optional config object controls authentication,
504
+ caching, and reCAPTCHA behavior. When a custom route's 2xx response schema references resource output models (`<Name>`,
505
+ `<Name>Single` or `<Name>Multiple` — directly or nested inside custom schemas), virtual field values (e.g. `File.url`)
506
+ are projected onto the response payload automatically before serialization.
507
+
508
+ ```ts
509
+ // src/plugins/custom-route.ts
510
+ import { registerRoute, Router } from '@appweaver/core';
511
+ import { Type } from '@sinclair/typebox';
512
+
513
+ registerRoute(
514
+ async function (router: Router) {
515
+ router.get('/search-result', {
516
+ schema: {
517
+ summary: 'Sample search result response route',
518
+ response: {
519
+ 200: Type.Ref('SearchResult')
520
+ }
521
+ },
522
+ handler: async () => {
523
+ return { message: 'Hello, world!' };
524
+ }
525
+ });
526
+ },
527
+ { public: true, cacheTTL: 15000 }
528
+ );
529
+ ```
530
+
531
+ ### Registering a custom model
532
+
533
+ Use `registerModel` to register a custom [TypeBox](https://github.com/sinclairzx81/typebox) schema as a named model.
534
+ Registered models are added to the schema registry and can be referenced by `$ref` in route schemas.
535
+
536
+ ```ts
537
+ // src/plugins/custom-model.ts
538
+ import { registerModel } from '@appweaver/core';
539
+ import { Type } from '@sinclair/typebox';
540
+
541
+ registerModel(
542
+ Type.Object(
543
+ {
544
+ id: Type.Number(),
545
+ title: Type.String(),
546
+ score: Type.Number({ minimum: 0, maximum: 1 })
547
+ },
548
+ { $id: 'SearchResult' }
549
+ )
550
+ );
551
+ ```
552
+
553
+ ### Registering plugin
554
+
555
+ Use `registerPlugin` to register a custom [Fastify plugin](https://fastify.dev/docs/latest/Reference/Plugins/). Plugins
556
+ are registered with `fastify-plugin` so their decorators and hooks are scoped to the entire server. You can declare
557
+ optional dependencies on other named plugins.
558
+
559
+ ```ts
560
+ // src/plugins/audit-log.ts
561
+ import { registerPlugin } from '@appweaver/core';
562
+
563
+ registerPlugin('audit-log', async (server) => {
564
+ server.addHook('onResponse', async (request, reply) => {
565
+ console.log(`${request.method} ${request.url} → ${reply.statusCode}`);
566
+ });
567
+ });
568
+ ```
569
+
570
+ ### Dependency injection
571
+
572
+ Use `define` to register a value or class in the app context, and `inject` to retrieve it. Class constructors are lazily
573
+ instantiated as singletons on the first injection.
574
+
575
+ ```ts
576
+ import { Cache } from '@appweaver/common';
577
+ import { define, inject } from '@appweaver/core';
578
+
579
+ define(RedisCacheService, Cache); // register class under abstract token
580
+ define('https://api.example.com', 'ApiBaseUrl'); // register plain value
581
+
582
+ const cache = inject(Cache); // resolves singleton instance
583
+ const url = inject<string>('ApiBaseUrl'); // resolves by string token
584
+ ```
585
+
586
+ Use `loadProvider` to dynamically load a class from a file path or npm package and register it under an abstract token.
587
+ This is the standard pattern for wiring infrastructure providers in `main.ts`.
588
+
589
+ ```ts
590
+ import { loadProvider } from '@appweaver/core';
591
+ import { Database, Cache } from '@appweaver/common';
592
+
593
+ loadProvider(__dirname, config.DATABASE_PROVIDER, Database); // required provider
594
+ loadProvider(__dirname, config.CACHE_PROVIDER, Cache);
595
+ loadProvider(__dirname, config.MAILER_PROVIDER, Mailer, false); // optional (no error if provider cannot be loaded)
596
+
597
+ const cache: Mailer | undefined = inject(Mailer, false); // optional injection
598
+ ```
599
+
600
+ ### Writing a seeder
601
+
602
+ A seeder is a TypeScript file that must export at least one asynchronous function responsible for executing database
603
+ seeding logic. Seeder files follow the same conventions as migration files: they can only be executed once, and their
604
+ execution status is recorded in the database table `_seeders`. Seeders are executed in alphabetical order; therefore,
605
+ the recommended naming convention is to prefix the filename with an ordinal number (e.g., `001-create-admin-user.ts`).
606
+
607
+ During execution of seeder functions, the full application context is available, which means it is possible to inject
608
+ any service or model previously defined in the main application logic or exported from other NPM packages.
609
+
610
+ ```ts
611
+ // database/seeders/001-create-admin-user.ts
612
+
613
+ import { hashPassword } from '@appweaver/core';
614
+ import { config, randomString } from '@appweaver/common';
615
+ import { db } from '@db/client';
616
+
617
+ export async function createAdminUser(): Promise<void> {
618
+ await db.user.create({
619
+ data: {
620
+ firstName: 'Admin',
621
+ lastName: 'Admin',
622
+ email: 'admin@appweaver.co',
623
+ phone: '01234435',
624
+ roles: {
625
+ connectOrCreate: [
626
+ {
627
+ where: { name: 'Admin' },
628
+ create: {
629
+ name: 'Admin',
630
+ permissions: {
631
+ connectOrCreate: [
632
+ { where: { name: '*.read' }, create: { name: '*.read' } },
633
+ { where: { name: '*.write' }, create: { name: '*.write' } }
634
+ ]
635
+ }
636
+ }
637
+ }
638
+ ]
639
+ }
640
+ }
641
+ });
642
+ }
643
+ ```
644
+
645
+ ## Common tasks
646
+
647
+ ### Build application
648
+
649
+ ```sh
650
+ weaver build
651
+ weaver build --project tsconfig.build.json # path to tsconfig build file
652
+ ```
653
+
654
+ ### Start application
655
+
656
+ ```sh
657
+ weaver start # production
658
+ weaver start --watch # development (watch mode)
659
+ weaver start --project tsconfig.json # path to tsconfig file
660
+ ```
661
+
662
+ ### Generate types and schema
663
+
664
+ ```sh
665
+ weaver generate --types # TypeScript types only
666
+ weaver generate --schema # Prisma schema only
667
+ weaver generate --types --schema # both (same as with no option flags)
668
+ ```
669
+
670
+ ### Run database migrations
671
+
672
+ ```sh
673
+ weaver migrate # run pending migrations
674
+ weaver migration new <name> # create a new migration
675
+ weaver migration reset # reset database (prompts confirmation)
676
+ weaver migration reset --force --yes # force reset, skip confirmation
677
+ ```
678
+
679
+ ### Seed the database
680
+
681
+ ```sh
682
+ weaver seed # run seeders
683
+ weaver seed --buildProject # build project first, then run seeders
684
+ weaver seed --continueOnError # continue if a seeder throws error
685
+ weaver seed --fixWarnings # fix all warnings like invalid checksum or missing seeder
686
+ weaver seed --project tsconfig.build.json # path to tsconfig build file
687
+ ```
688
+
689
+ ### Generate OpenAPI specification
690
+
691
+ ```sh
692
+ weaver openapi # generate schema to ./openapi.json
693
+ weaver openapi --outputPath ./generated/openapi.json # generate schema to a custom path
694
+ weaver openapi --format yaml # generate schema in yaml format
695
+ ```
696
+
697
+ ### Update Appweaver packages
698
+
699
+ ```sh
700
+ weaver update # update all @appweaver/* packages to latest
701
+ weaver update @appweaver/core @appweaver/cli # update specific packages
702
+ weaver update --targetVersion 1.2.3 # update to a specific version
703
+ weaver update --noSkill # skip updating AI agent skill files (.claude, .agents, …)
704
+ weaver update --force # force update despite peerDependency mismatches
705
+ ```
706
+
707
+ ### Run tests
708
+
709
+ ```sh
710
+ npm run test # unit tests with coverage
711
+ npm run e2e # e2e tests
712
+ ```
713
+
714
+ Test files must use the **`.test.ts`** extension. Place unit tests in `test/unit/` and end-to-end tests in `test/e2e/`,
715
+ naming each file after its module. Add or update tests whenever a feature is added or existing behaviour changes.
716
+
717
+ The e2e setup and teardown are wired automatically, but **each e2e test file must register the per-file database reset
718
+ itself**, after the hook that stops the application:
719
+
720
+ ```ts
721
+ import { resetTestData } from './support/reset';
722
+
723
+ describe('My e2e test', () => {
724
+ let app: Application;
725
+
726
+ beforeAll(async () => {
727
+ app = await createApp({ autoStartServer: false });
728
+ });
729
+
730
+ afterAll(async () => {
731
+ await app.stop();
732
+ });
733
+
734
+ afterAll(resetTestData, 10_000);
735
+ });
736
+ ```
737
+
738
+ ### Format code
739
+
740
+ ```sh
741
+ npm run format # prettier --write "./**/*.ts"
742
+ ```
743
+
744
+ ### Lint code
745
+
746
+ ```sh
747
+ npm run lint # eslint "./**/*.ts"
748
+ ```
749
+
750
+ ## References
751
+
752
+ - Application CLI (weaver): [cli.md](references/cli.md)
753
+ - Application configuration: [configuration.md](references/configuration.md)
754
+ - Application resources: [resources.md](references/resources.md)
755
+ - Dependency injection: [dependency-injection.md](references/dependency-injection.md)
756
+ - Security details: [security.md](references/security.md)
757
+ - Storage & File management: [storage.md](references/storage.md)
758
+ - Database & Migrations: [database.md](references/database.md)
759
+ - Events & Hooks: [events.md](references/events.md)
760
+ - Cache management: [cache.md](references/cache.md)
761
+ - Queue jobs: [queue.md](references/queue.md)
762
+ - Scheduling jobs: [scheduler.md](references/scheduler.md)
763
+ - Sending emails: [mailer.md](references/mailer.md)
764
+ - Generating an HTTP client for using API: [client.md](references/client.md)