@appweaver/cli 1.4.1 → 1.5.1
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/generate/generate-schema.js +69 -8
- package/package.json +1 -1
- package/skill/SKILL.md +764 -753
- package/skill/references/cli.md +34 -20
- package/skill/references/resources.md +120 -11
- package/skill/references/security.md +3 -2
- package/skill/references/storage.md +29 -6
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
});
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
registerRoute(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
weaver
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
weaver
|
|
674
|
-
weaver
|
|
675
|
-
weaver
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
weaver
|
|
683
|
-
weaver
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
weaver
|
|
693
|
-
weaver
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
-
|
|
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
|
+
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)
|