@appweaver/create-weaver-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -0
  3. package/create-weaver-app.d.ts +2 -0
  4. package/create-weaver-app.js +266 -0
  5. package/package.json +37 -0
  6. package/skill/GUIDELINES.md +298 -0
  7. package/skill/SKILL.md +593 -0
  8. package/skill/references/cache.md +207 -0
  9. package/skill/references/cli.md +213 -0
  10. package/skill/references/client.md +507 -0
  11. package/skill/references/configuration.md +402 -0
  12. package/skill/references/database.md +134 -0
  13. package/skill/references/dependency-injection.md +214 -0
  14. package/skill/references/events.md +152 -0
  15. package/skill/references/mailer.md +235 -0
  16. package/skill/references/queue.md +196 -0
  17. package/skill/references/resources.md +961 -0
  18. package/skill/references/scheduler.md +184 -0
  19. package/skill/references/security.md +694 -0
  20. package/skill/references/storage.md +251 -0
  21. package/templates/default/.dockerignore +5 -0
  22. package/templates/default/.env.tpl +1 -0
  23. package/templates/default/.prettierignore +3 -0
  24. package/templates/default/.prettierrc +7 -0
  25. package/templates/default/Dockerfile +56 -0
  26. package/templates/default/Dockerfile.bun +56 -0
  27. package/templates/default/README.md.tpl +7 -0
  28. package/templates/default/appweaver.dev.json.tpl +9 -0
  29. package/templates/default/appweaver.json.bun.tpl +17 -0
  30. package/templates/default/appweaver.json.tpl +16 -0
  31. package/templates/default/appweaver.test.json.tpl +30 -0
  32. package/templates/default/bunfig.toml.bun +8 -0
  33. package/templates/default/database/client.ts.tpl +7 -0
  34. package/templates/default/database/schema.prisma +13 -0
  35. package/templates/default/database/seeders/001-create-admin-user.ts.tpl +39 -0
  36. package/templates/default/eslint.config.mjs +55 -0
  37. package/templates/default/eslint.config.mjs.bun +53 -0
  38. package/templates/default/jest.config.json.node +23 -0
  39. package/templates/default/package.json.bun.tpl +39 -0
  40. package/templates/default/package.json.tpl +44 -0
  41. package/templates/default/prisma.config.ts.tpl +14 -0
  42. package/templates/default/public/favicon.ico +0 -0
  43. package/templates/default/public/robots.txt +2 -0
  44. package/templates/default/src/features/index.ts.tpl +0 -0
  45. package/templates/default/src/main.ts.tpl +7 -0
  46. package/templates/default/src/resources/user/model.ts.tpl +28 -0
  47. package/templates/default/src/resources/user/policy.ts.tpl +3 -0
  48. package/templates/default/src/resources/user/routes.ts.tpl +3 -0
  49. package/templates/default/src/resources/user/service.ts.tpl +16 -0
  50. package/templates/default/src/types/generated.ts.tpl +1 -0
  51. package/templates/default/src/types/index.ts.tpl +1 -0
  52. package/templates/default/start.sh +26 -0
  53. package/templates/default/start.sh.bun +26 -0
  54. package/templates/default/swc.config.json.node +13 -0
  55. package/templates/default/test/e2e/jest.e2e-config.json.node +22 -0
  56. package/templates/default/test/e2e/main.test.ts.tpl +24 -0
  57. package/templates/default/test/e2e/support/each.ts.tpl +13 -0
  58. package/templates/default/test/e2e/support/preload.ts.bun +13 -0
  59. package/templates/default/test/e2e/support/setup.ts.tpl +13 -0
  60. package/templates/default/test/e2e/support/teardown.ts.tpl +13 -0
  61. package/templates/default/test/unit/sample.test.ts.tpl +5 -0
  62. package/templates/default/tsconfig.build.json +10 -0
  63. package/templates/default/tsconfig.json +27 -0
  64. package/templates/default/tsconfig.json.bun +28 -0
@@ -0,0 +1,961 @@
1
+ # Resources
2
+
3
+ Resources are the core building blocks of an Appweaver application. There are four resource types that form a
4
+ dependency chain: **model** → **service** → **routes** → **policy**. Each resource type is created using a
5
+ corresponding factory function and autoloaded from `src/resources/*/` on application start. Source directory and
6
+ resources pattern could be changed with `APP_SOURCE_PATH` and `RESOURCE_{MODEL,SERVICE,...}_PATTERN` config variables.
7
+
8
+ - A **model** is always required.
9
+ - A **service** requires a model.
10
+ - The **Routes** require a service.
11
+ - A **policy** is optional and independent of the chain.
12
+
13
+ ---
14
+
15
+ ## createModel
16
+
17
+ Creates a resource model definition. The model defines database fields, relations, files, virtual fields, DTOs for
18
+ CRUD operations, and index configuration. It is used to generate Prisma schema, TypeScript types, and route
19
+ request/response schemas.
20
+
21
+ ```ts
22
+ import { createModel } from '@appweaver/core';
23
+
24
+ export default createModel({
25
+ name: 'Product',
26
+ // ... configuration
27
+ });
28
+ ```
29
+
30
+ ### Configuration
31
+
32
+ ```ts
33
+ function createModel(config: ResourceModelConfig, override ?: Partial<ResourceModelConfig>) {
34
+ }
35
+ ```
36
+
37
+ | Property | Type | Required | Default | Description |
38
+ |------------------|--------------------------------|----------|-----------------------|---------------------------------------------------------------------|
39
+ | `name` | string | yes | - | Model name (PascalCase). Used as database table name and type name. |
40
+ | `tableName` | string | no | (model name) | Custom database table name override. |
41
+ | `generateTypes` | boolean | no | `true` | Generate TypeScript types for this model. |
42
+ | `generateSchema` | boolean | no | `true` | Generate Prisma schema for this model. |
43
+ | `id` | IdField | no | Autoincrement integer | ID field configuration. |
44
+ | `audit` | AuditFields | no | All included | Audit timestamps and creator tracking fields. |
45
+ | `scalars` | Record\<string, ScalarField> | no | - | Scalar fields (database columns). |
46
+ | `relations` | Record\<string, RelationField> | no | - | Relations to other models. |
47
+ | `files` | Record\<string, FileField> | no | - | File upload fields. |
48
+ | `virtual` | Record\<string, VirtualField> | no | - | Computed/virtual fields not stored in database. |
49
+ | `read` | OperationConfig | no | - | Pick/omit fields for the read DTO. |
50
+ | `create` | OperationConfig | no | - | Pick/omit fields for the create DTO. |
51
+ | `update` | OperationConfig | no | - | Pick/omit fields for the update DTO. |
52
+ | `export` | Record\<string, ExportField> | no | - | CSV export field configuration. |
53
+ | `index` | string[] \| string[][] | no | - | Database index definitions. |
54
+
55
+ ### ID field
56
+
57
+ ```ts
58
+ const config = {
59
+ // Integer ID with autoincrement (default)
60
+ id: {
61
+ type: 'int',
62
+ generator: 'autoincrement()'
63
+ },
64
+
65
+ // String ID with generator
66
+ id: {
67
+ type: 'string',
68
+ generator: 'uuid()'
69
+ }
70
+ };
71
+ ```
72
+
73
+ | Property | Type | Default | Description |
74
+ |-------------|-------------------------------------------------------------------------------|---------------------|-------------------------------------------------------------------------------|
75
+ | `type` | `'string'` \| `'int'` \| `'bigInt'` | `'int'` | ID field data type. |
76
+ | `generator` | `'uuid()'` \| `'uuid(7)'` \| `'cuid()'` \| `'cuid(2)'` \| `'autoincrement()'` | `'autoincrement()'` | Value generator. String types use UUID/CUID, integer types use autoincrement. |
77
+
78
+ ### Audit fields
79
+
80
+ It is recommended to always use all audit fields for all resource models, unless specified otherwise. In the usual
81
+ scenario audit should be left out (including all fields by default).
82
+
83
+ ```ts
84
+ const config = {
85
+ // By default all audit fields are included
86
+ audit: {
87
+ createdAt: true,
88
+ updatedAt: true,
89
+ createdById: true
90
+ }
91
+ };
92
+ ```
93
+
94
+ | Property | Type | Default | Description |
95
+ |---------------|---------|---------|-------------------------------------------------|
96
+ | `createdAt` | boolean | `true` | Add `createdAt` timestamp field. |
97
+ | `updatedAt` | boolean | `true` | Add `updatedAt` timestamp field. |
98
+ | `createdById` | boolean | `true` | Add `createdById` foreign key to the auth user. |
99
+
100
+ ### Scalar field types
101
+
102
+ All scalar fields share these common properties:
103
+
104
+ | Property | Type | Default | Description |
105
+ |---------------------|-----------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------|
106
+ | `required` | boolean | `true` | Whether the field is required. |
107
+ | `unique` | boolean | `false` | Add a unique constraint. |
108
+ | `hidden` | boolean | `false` | Hide from API output (e.g. password hashes). |
109
+ | `default` | varies | - | Default static value. |
110
+ | `defaultGenerator` | string | - | Default is generated by function (e.g. uuid(), cuid(), autoincrement(), now(), ...). |
111
+ | `defaultExpression` | string | - | Default is generated by database expression in supported database syntax (e.g. concat('token_', gen_random_uuid()))::TEXT). |
112
+ | `array` | boolean | `false` | Store as array (supported on string, int, float). |
113
+ | `example` | string \| number \| boolean | - | Example value for OpenAPI (Swagger) schema documentation. |
114
+
115
+ #### String
116
+
117
+ ```ts
118
+ const config = {
119
+ title: {
120
+ type: 'string',
121
+ minLength: 1,
122
+ maxLength: 200,
123
+ default: 'No title'
124
+ },
125
+ email: {
126
+ type: 'string',
127
+ format: 'email'
128
+ },
129
+ slug: {
130
+ type: 'string',
131
+ pattern: '^[a-z0-9-]+$'
132
+ },
133
+ code: {
134
+ type: 'string',
135
+ defaultGenerator: 'uuid()'
136
+ }
137
+ };
138
+ ```
139
+
140
+ | Property | Type | Description |
141
+ |-------------|---------------------------------------------------------------------------------------|--------------------------------------|
142
+ | `type` | `'string'` | String field type. |
143
+ | `minLength` | number | Minimum string length. |
144
+ | `maxLength` | number | Maximum string length. |
145
+ | `format` | `'email'` \| `'hostname'` \| `'ipv4'` \| `'ipv6'` \| `'uri'` \| `'uuid'` \| `'regex'` | Built-in format validation. |
146
+ | `pattern` | string | Custom regex pattern for validation. |
147
+
148
+ String defaults can also be ID generators: `'uuid()'`, `'uuid(7)'`, `'cuid()'`, `'cuid(2)'`.
149
+
150
+ #### Number (int, bigInt, float)
151
+
152
+ ```ts
153
+ const config = {
154
+ price: {
155
+ type: 'float',
156
+ minimum: 0
157
+ },
158
+ quantity: {
159
+ type: 'int',
160
+ minimum: 0,
161
+ maximum: 10000
162
+ }
163
+ };
164
+ ```
165
+
166
+ | Property | Type | Description |
167
+ |-----------|------------------------------------|--------------------|
168
+ | `type` | `'int'` \| `'bigInt'` \| `'float'` | Number field type. |
169
+ | `minimum` | number | Minimum value. |
170
+ | `maximum` | number | Maximum value. |
171
+
172
+ Integer defaults can be `'autoincrement()'`.
173
+
174
+ #### Boolean
175
+
176
+ ```ts
177
+ const config = {
178
+ enabled: {
179
+ type: 'boolean',
180
+ default: true
181
+ }
182
+ };
183
+ ```
184
+
185
+ | Property | Type | Description |
186
+ |----------|-------------|---------------------|
187
+ | `type` | `'boolean'` | Boolean field type. |
188
+
189
+ #### DateTime
190
+
191
+ ```ts
192
+ const config = {
193
+ publishedAt: {
194
+ type: 'dateTime',
195
+ defaultGenerator: 'now()'
196
+ },
197
+ eventDate: {
198
+ type: 'dateTime',
199
+ format: 'date'
200
+ }
201
+ };
202
+ ```
203
+
204
+ | Property | Type | Description |
205
+ |----------|---------------------------------------|----------------------|
206
+ | `type` | `'dateTime'` | DateTime field type. |
207
+ | `format` | `'date-time'` \| `'time'` \| `'date'` | DateTime format. |
208
+
209
+ Default can be `'now()'` for current timestamp.
210
+
211
+ #### JSON
212
+
213
+ ```ts
214
+ const config = {
215
+ metadata: {
216
+ type: 'json',
217
+ default: {}
218
+ }
219
+ };
220
+ ```
221
+
222
+ | Property | Type | Description |
223
+ |----------|----------|------------------------------------------------------|
224
+ | `type` | `'json'` | JSON field type. Stores arbitrary objects or arrays. |
225
+
226
+ #### Enum
227
+
228
+ ```ts
229
+ const config = {
230
+ status: {
231
+ type: 'enum',
232
+ values: ['Draft', 'Active', 'Sold'],
233
+ default: 'Draft'
234
+ }
235
+ };
236
+ ```
237
+
238
+ | Property | Type | Description |
239
+ |----------|----------|---------------------------------|
240
+ | `type` | `'enum'` | Enum field type. |
241
+ | `values` | string[] | Allowed enum values (required). |
242
+
243
+ ### Relations
244
+
245
+ ```ts
246
+ // src/resources/product/model.ts
247
+ const config = {
248
+ relations: {
249
+ category: {
250
+ model: 'Category',
251
+ mappedBy: 'products',
252
+ owner: true,
253
+ output: {
254
+ type: 'always'
255
+ }
256
+ },
257
+ reviews: {
258
+ model: 'Review',
259
+ mappedBy: 'product',
260
+ array: true,
261
+ output: {
262
+ type: 'single',
263
+ count: true
264
+ }
265
+ }
266
+ }
267
+ };
268
+ ```
269
+
270
+ ```ts
271
+ // src/resources/category/model.ts
272
+ const config = {
273
+ relations: {
274
+ products: {
275
+ model: 'Product',
276
+ mappedBy: 'category',
277
+ array: true,
278
+ output: {
279
+ type: 'single'
280
+ }
281
+ }
282
+ }
283
+ };
284
+ ```
285
+
286
+ ```ts
287
+ // src/resources/review/model.ts
288
+ const config = {
289
+ relations: {
290
+ product: {
291
+ model: 'Product',
292
+ mappedBy: 'reviews',
293
+ owner: true,
294
+ input: {
295
+ type: 'none'
296
+ }
297
+ }
298
+ }
299
+ };
300
+ ```
301
+
302
+ | Property | Type | Default | Description |
303
+ |---------------------|-------------------|--------------|--------------------------------------------------------------------------|
304
+ | `model` | string | **required** | Target model name. |
305
+ | `owner` | boolean | `false` | This side owns the foreign key (only one side should be owner). |
306
+ | `mappedBy` | string | - | Name of the inverse relation on the target model. |
307
+ | `array` | boolean | `false` | One-to-many value or many-to-many relation. |
308
+ | `unique` | boolean | `false` | One-to-one relation (unique foreign key). |
309
+ | `required` | boolean | `true` | Whether the relation is required (nullable foreign key if not required). |
310
+ | `minItems` | number | - | Minimum items for array relations. |
311
+ | `createIfNotExists` | boolean | `false` | Auto-create related record if it doesn't exist. |
312
+ | `orphanRemoval` | boolean | `false` | Delete orphaned records when parent is deleted. |
313
+ | `onDelete` | ReferentialAction | - | Foreign key action on delete. |
314
+ | `onUpdate` | ReferentialAction | - | Foreign key action on update. |
315
+ | `input` | RelationInput | - | Input DTO configuration. |
316
+ | `output` | RelationOutput | - | Output DTO configuration. |
317
+
318
+ **ReferentialAction values**: `'cascade'`, `'restrict'`, `'noAction'`, `'setNull'`, `'setDefault'`
319
+
320
+ #### Relationship types
321
+
322
+ The combination of `owner`, `array`, and `unique` properties determines the relationship type:
323
+
324
+ **One-to-One**: One side has `owner: true` and `unique: true`, the other side has `owner: false` (default) and
325
+ `unique: false` (default).
326
+
327
+ ```ts
328
+ // User model
329
+ const config = {
330
+ relations: {
331
+ profile: {
332
+ model: 'Profile',
333
+ mappedBy: 'user',
334
+ owner: true,
335
+ unique: true,
336
+ required: false // otherwise the Profile DTO must be sent when creating the user resource
337
+ }
338
+ }
339
+ };
340
+ ```
341
+
342
+ ```ts
343
+ // Profile model
344
+ const config = {
345
+ relations: {
346
+ user: {
347
+ model: 'User',
348
+ mappedBy: 'profile'
349
+ }
350
+ }
351
+ };
352
+ ```
353
+
354
+ **One-to-Many**: The "one" side has `owner: false` (default) and `array: true`, the "many" side has `owner: true` and
355
+ `array: false` (default).
356
+
357
+ ```ts
358
+ // Category model (one)
359
+ const config = {
360
+ relations: {
361
+ products: {
362
+ model: 'Product',
363
+ mappedBy: 'category',
364
+ array: true
365
+ }
366
+ }
367
+ };
368
+ ```
369
+
370
+ ```ts
371
+ // Product model (many)
372
+ const config = {
373
+ relations: {
374
+ category: {
375
+ model: 'Category',
376
+ mappedBy: 'products',
377
+ owner: true
378
+ }
379
+ }
380
+ };
381
+ ```
382
+
383
+ **Many-to-Many**: Both sides have `array: true`. Only one side should have `owner: true` (determines which creates the
384
+ join table).
385
+
386
+ ```ts
387
+ // Post model
388
+ const config = {
389
+ relations: {
390
+ tags: {
391
+ model: 'Tag',
392
+ mappedBy: 'posts',
393
+ owner: true,
394
+ array: true
395
+ }
396
+ }
397
+ };
398
+ ```
399
+
400
+ ```ts
401
+ // Tag model
402
+ const config = {
403
+ relations: {
404
+ posts: {
405
+ model: 'Post',
406
+ mappedBy: 'tags',
407
+ array: true
408
+ }
409
+ }
410
+ };
411
+ ```
412
+
413
+ #### Relation input
414
+
415
+ | Property | Type | Description |
416
+ |-------------|-------------------------------------------------|---------------------------------------------------|
417
+ | `type` | `'all'` \| `'create'` \| `'update'` \| `'none'` | When the relation field is available as input. |
418
+ | `uniqueKey` | string | Use a unique key field instead of ID for linking. |
419
+ | `fullModel` | boolean | Accept full nested model object as input. |
420
+
421
+ #### Relation output
422
+
423
+ | Property | Type | Description |
424
+ |-----------|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
425
+ | `type` | `'always'` \| `'single'` \| `'multiple'` \| `'none'` | When to include the relation in output. `always` = all reads, `single` = single record reads, `multiple` = list reads, `none` = never. |
426
+ | `include` | Record\<string, RelationOutput> | Nested relation output configuration. |
427
+ | `count` | boolean | Include a count of related records. |
428
+
429
+ ### File fields
430
+
431
+ ```ts
432
+ const config = {
433
+ files: {
434
+ photo: {
435
+ mimeType: 'image/*',
436
+ namePattern: 'photos/{userId}-{name}-{hash}.{extension}',
437
+ maxSize: '2 MB'
438
+ },
439
+ documents: {
440
+ mimeType: 'application/pdf',
441
+ array: true,
442
+ maxCount: 5
443
+ }
444
+ }
445
+ };
446
+ ```
447
+
448
+ | Property | Type | Description |
449
+ |---------------|--------------------|-------------------------------------------------------------------------|
450
+ | `mimeType` | string \| RegExp | Allowed MIME types (glob patterns like `'image/*'` supported). |
451
+ | `namePattern` | string \| function | File naming pattern or function (available variables are listed below). |
452
+ | `array` | boolean | Allow multiple files. |
453
+ | `maxSize` | number \| string | Maximum file size (e.g. `'2 MB'`, `5242880`). |
454
+ | `maxCount` | number | Maximum number of files (for array fields). |
455
+ | `output` | RelationOutput | When to include file info in output. |
456
+
457
+ #### Available namePattern variables
458
+
459
+ Default pattern is: `{name}-{hash}.{extension}`.
460
+
461
+ | Variable | Type | Description |
462
+ |-----------------|--------|---------------------------------------------|
463
+ | `name` | string | Original filename without extension. |
464
+ | `extension` | string | Original file extension. |
465
+ | `resourceField` | string | Field name the file is assigned to. |
466
+ | `resourceName` | string | Resource model name. |
467
+ | `resourceId` | string | Resource ID. |
468
+ | `userId` | string | Authenticated user ID. |
469
+ | `userEmail` | string | Authenticated user email. |
470
+ | `year` | number | Current UTC year. |
471
+ | `month` | number | Current UTC month (1-12). |
472
+ | `day` | number | Current UTC day of month. |
473
+ | `weekDay` | number | Current UTC day of week (0-6, Sunday is 0). |
474
+ | `yearWeek` | number | ISO week number. |
475
+ | `yearDay` | number | Day of year (1-366). |
476
+ | `hours` | number | Current UTC hours. |
477
+ | `minutes` | number | Current UTC minutes. |
478
+ | `seconds` | number | Current UTC seconds. |
479
+ | `milliseconds` | number | Current UTC milliseconds. |
480
+ | `timestamp` | number | Unix timestamp in milliseconds. |
481
+ | `date` | string | Current date in ISO 8601 format. |
482
+ | `uuid` | string | Generated random UUID. |
483
+ | `hash` | string | Generated random hash (32 bytes). |
484
+
485
+ ### Virtual fields
486
+
487
+ Virtual fields are computed values not stored in the database. They can appear in input DTOs (to receive data) and/or
488
+ output DTOs (to return computed values).
489
+
490
+ ```ts
491
+ const config = {
492
+ virtual: {
493
+ displayName: {
494
+ type: 'string',
495
+ output: {
496
+ type: 'always',
497
+ value: (resource) => `${resource.firstName} ${resource.lastName}`
498
+ }
499
+ },
500
+ inviteCode: {
501
+ type: 'string',
502
+ input: {
503
+ type: 'create'
504
+ }
505
+ }
506
+ }
507
+ };
508
+ ```
509
+
510
+ | Property | Type | Description |
511
+ |------------------|------------------------------------------------------|------------------------------------------------------------|
512
+ | *(scalar props)* | - | All scalar field properties (type, minLength, etc.) apply. |
513
+ | `input.type` | `'all'` \| `'create'` \| `'update'` \| `'none'` | When the virtual field accepts input. |
514
+ | `input.value` | primitive \| function | Default value or transformer for input. |
515
+ | `output.type` | `'always'` \| `'single'` \| `'multiple'` \| `'none'` | When the virtual field appears in output. |
516
+ | `output.value` | primitive \| function | Computed value or transformer for output. |
517
+
518
+ ### Operation config (read, create, update)
519
+
520
+ Control which fields appear in each DTO. Use `pick` for an allowlist or `omit` for a deny-list.
521
+
522
+ ```ts
523
+ const config = {
524
+ create: {
525
+ omit: ['status'] // All fields except status
526
+ },
527
+ update: {
528
+ pick: ['title', 'price'] // Only title and price
529
+ }
530
+ };
531
+ ```
532
+
533
+ | Property | Type | Description |
534
+ |----------|----------|------------------------------------------------|
535
+ | `omit` | string[] | Fields to exclude from the DTO. |
536
+ | `pick` | string[] | Fields to include in the DTO (overrides omit). |
537
+
538
+ ### Export config
539
+
540
+ Configure CSV export behavior per field:
541
+
542
+ ```ts
543
+ const config = {
544
+ export: {
545
+ price: {
546
+ headerName: 'Product Price',
547
+ mapValue: 'price'
548
+ },
549
+ passwordHash: {
550
+ exclude: true
551
+ },
552
+ status: {
553
+ mapValue: (val) => val.toUpperCase()
554
+ },
555
+ author: {
556
+ firstName: {
557
+ headerName: 'Given Name'
558
+ },
559
+ lastName: {
560
+ headerName: 'Family Name'
561
+ }
562
+ }
563
+ }
564
+ };
565
+ ```
566
+
567
+ | Property | Type | Description |
568
+ |--------------|--------------------|------------------------------------|
569
+ | `headerName` | string | Custom CSV column header name. |
570
+ | `exclude` | boolean | Exclude this field from exports. |
571
+ | `mapValue` | string \| function | Transform the value during export. |
572
+
573
+ ### Index config
574
+
575
+ Define database indexes as a flat array (single-field indexes) or nested arrays (composite indexes):
576
+
577
+ ```ts
578
+ index: ['title'] // Single-field index on title
579
+ index: [['status', 'categoryId']] // Composite index on status + categoryId
580
+ index: ['email', ['status', 'createdAt']] // Both single and composite
581
+ ```
582
+
583
+ ### Generated models
584
+
585
+ `createModel` produces the following TypeBox schema models used internally by routes and services:
586
+
587
+ | Model | Purpose |
588
+ |-------------------|------------------------------------|
589
+ | `readModel` | Full model with all visible fields |
590
+ | `createModel` | Request body for create operations |
591
+ | `updateModel` | Request body for update operations |
592
+ | `relationsModel` | Relations-only subset |
593
+ | `virtualModel` | Virtual fields-only subset |
594
+ | `filesModel` | File fields-only subset |
595
+ | `readOneModel` | Response for single-item reads |
596
+ | `readManyModel` | Response for list reads |
597
+ | `createOneModel` | Request for create endpoint |
598
+ | `updateOneModel` | Request for update endpoint |
599
+ | `fileUploadModel` | Request for file upload endpoint |
600
+ | `fileDeleteModel` | Request for file delete endpoint |
601
+
602
+ ---
603
+
604
+ ## createService
605
+
606
+ Creates a resource service with lifecycle hooks and business logic. The service handles all database operations for a
607
+ model and triggers hooks on each CRUD operation before/after.
608
+
609
+ ```ts
610
+ import { createService } from '@appweaver/core';
611
+
612
+ export default createService({
613
+ modelName: 'Product',
614
+ afterCreate: (resource) => {
615
+ logger.info(`Product created: ${resource.id}`);
616
+ },
617
+ textSearch: {
618
+ title: { contains: '{input}', mode: 'insensitive' }
619
+ }
620
+ });
621
+ ```
622
+
623
+ ### Configuration
624
+
625
+ ```ts
626
+ function createService(config: ResourceServiceConfig, override ?: Partial<ResourceServiceConfig>) {
627
+ }
628
+ ```
629
+
630
+ | Property | Type | Description |
631
+ |-------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
632
+ | `modelName` | string | Model name to bind this service to (required). |
633
+ | `beforeFind` | `(id) => void` | Hook called before finding a single resource. |
634
+ | `beforeQuery` | `(filter, page, size, sort) => void` | Hook called before querying resources. |
635
+ | `beforeAggregate` | `(filter, select, dateField, from?, to?, step?, safeIncrement?) => void` | Hook called before aggregation. |
636
+ | `beforeCreate` | `(data) => void` | Hook called before creating a resource. Mutate `data` to modify input. |
637
+ | `beforeUpdate` | `(id, data) => void` | Hook called before updating a resource. |
638
+ | `beforeDelete` | `(id) => void` | Hook called before deleting a resource. |
639
+ | `afterFind` | `(resource) => void` | Hook called after finding a resource. |
640
+ | `afterQuery` | `(response) => void` | Hook called after querying resources. |
641
+ | `afterAggregate` | `(response) => void` | Hook called after aggregation. |
642
+ | `afterCreate` | `(resource) => void` | Hook called after creating a resource. |
643
+ | `afterUpdate` | `(resource) => void` | Hook called after updating a resource. |
644
+ | `afterDelete` | `(resource) => void` | Hook called after deleting a resource. |
645
+ | `textSearch` | object \| function | Prisma filter object or function `(input: string) => filter` for text search. Use `'{input}'` as placeholder in filter objects. |
646
+
647
+ All hooks can be synchronous or return a `Promise`.
648
+
649
+ ### Service methods
650
+
651
+ The created service exposes the following methods:
652
+
653
+ | Method | Signature | Description |
654
+ |-------------|---------------------------------------------------------------------------------------------------|----------------------------------------------------------|
655
+ | `find` | `(id) => Promise<ReadOne>` | Find a single resource by ID. |
656
+ | `query` | `(filter?, page?, size?, sort?) => Promise<QueryResponse>` | Query resources with filtering, pagination, and sorting. |
657
+ | `aggregate` | `(filter?, select?, dateField?, from?, to?, step?, safeIncrement?) => Promise<AggregateResponse>` | Aggregate resources with time-series grouping. |
658
+ | `create` | `(data) => Promise<ReadOne>` | Create a new resource. |
659
+ | `update` | `(id, data) => Promise<ReadOne>` | Update an existing resource. |
660
+ | `delete` | `(id) => Promise<ReadOne>` | Delete a resource. |
661
+
662
+ ### Query response
663
+
664
+ ```ts
665
+ const config = {
666
+ resultCount: 123, // Items in this page
667
+ totalCount: 123, // Total items matching filter
668
+ items: [] // Page data
669
+ };
670
+ ```
671
+
672
+ ### Aggregate response
673
+
674
+ ```ts
675
+ const resp = {
676
+ total: AggregateValue, // Overall aggregation
677
+ items: Arrray<AggregateResult> // Per-period results
678
+ };
679
+
680
+ // Each AggregateResult:
681
+ const result = {
682
+ date: 'Date',
683
+ result: {
684
+ [field]:
685
+ {
686
+ count: 123,
687
+ min: 123,
688
+ max: 123,
689
+ avg: 123,
690
+ sum: 123,
691
+ first: 123,
692
+ last: 123
693
+ }
694
+ }
695
+ };
696
+ ```
697
+
698
+ ### Text search example
699
+
700
+ Object form with placeholder:
701
+
702
+ ```ts
703
+ const config = {
704
+ textSearch: {
705
+ title: {
706
+ contains: '{input}', mode:
707
+ 'insensitive'
708
+ }
709
+ }
710
+ };
711
+ ```
712
+
713
+ Function form for complex queries:
714
+
715
+ ```ts
716
+ const config = {
717
+ textSearch: (input) => ({
718
+ OR: [
719
+ { title: { contains: input, mode: 'insensitive' } },
720
+ { description: { contains: input, mode: 'insensitive' } }
721
+ ]
722
+ })
723
+ };
724
+ ```
725
+
726
+ ---
727
+
728
+ ## createRoutes
729
+
730
+ Creates CRUD route definitions for a resource. Routes are automatically registered with Fastify and derive their
731
+ request/response schemas from the resource model.
732
+
733
+ ```ts
734
+ import { createRoutes } from '@appweaver/core';
735
+
736
+ export default createRoutes({
737
+ modelName: 'Product',
738
+ path: '/products',
739
+ find: { roles: ['Admin', 'User'], rateLimit: { max: 100 } },
740
+ query: { cache: true, cacheTTL: 5000 },
741
+ create: { permissions: ['product:create'] },
742
+ delete: { exclude: true }
743
+ });
744
+ ```
745
+
746
+ ### Configuration
747
+
748
+ ```ts
749
+ function createRoutes(config: ResourceRoutesConfig, override ?: Partial<ResourceRoutesConfig>) {
750
+ }
751
+ ```
752
+
753
+ | Property | Type | Description |
754
+ |--------------|-----------------|----------------------------------------------------------|
755
+ | `modelName` | string | Model name to bind routes to (required). |
756
+ | `path` | string | Custom base URL path (default: derived from model name). |
757
+ | `find` | ReadRouteConfig | `GET /:id` - Find single resource by ID. |
758
+ | `query` | ReadRouteConfig | `POST /query` - Query resources with filters. |
759
+ | `aggregate` | ReadRouteConfig | `POST /aggregate` - Aggregate resources. |
760
+ | `create` | RouteConfig | `POST /` - Create a new resource. |
761
+ | `update` | RouteConfig | `PUT /:id` - Update a resource. |
762
+ | `delete` | RouteConfig | `DELETE /:id` - Delete a resource. |
763
+ | `export` | RouteConfig | `POST /export` - Export resources to CSV. |
764
+ | `fileUpload` | RouteConfig | `POST /:id/files` - Upload files to a resource. |
765
+ | `fileDelete` | RouteConfig | `POST /:id/delete-files` - Delete files from a resource. |
766
+
767
+ ### Route config (all operations)
768
+
769
+ | Property | Type | Default | Description |
770
+ |-------------------|--------------------------|---------|---------------------------------------------------------------|
771
+ | `exclude` | boolean | `false` | Exclude this operation entirely. |
772
+ | `public` | boolean | `false` | No authentication required. |
773
+ | `roles` | string[] | - | Required roles (OR logic by default). |
774
+ | `permissions` | string[] | - | Required permissions (OR logic by default). |
775
+ | `auth` | AuthType[] | - | Allowed authentication types: `'jwt'`, `'apiKey'`, `'basic'`. |
776
+ | `rateLimit` | RateLimitConfig \| false | - | Per-operation rate limiting. `false` disables. |
777
+ | `recaptcha` | boolean | `false` | Require reCAPTCHA verification. |
778
+ | `recaptchaAction` | string | - | Expected reCAPTCHA action name for score validation. |
779
+
780
+ ### Read route config (find, query, aggregate)
781
+
782
+ Extends RouteConfig with caching options:
783
+
784
+ | Property | Type | Default | Description |
785
+ |-------------------------|--------------------|---------|----------------------------------------------------------------|
786
+ | `cache` | boolean | `false` | Enable response caching. |
787
+ | `cacheKey` | string \| function | - | Custom cache key. Function signature: `(req, user) => string`. |
788
+ | `cacheTTL` | number | - | Cache TTL in milliseconds (overrides global default). |
789
+ | `cacheSkipInvalidation` | boolean | `false` | Skip automatic cache invalidation on writes. |
790
+
791
+ ### Rate limit config
792
+
793
+ ```ts
794
+ const config = {
795
+ rateLimit: {
796
+ max: 100,
797
+ timeWindow: 60000,
798
+ allowList: ['127.0.0.1'],
799
+ keyGenerator: (req) => req.ip
800
+ }
801
+ };
802
+ ```
803
+
804
+ | Property | Type | Description |
805
+ |----------------|------------------------------|---------------------------------------------------------------------|
806
+ | `max` | number \| function | Maximum requests per time window. Function: `(req, key) => number`. |
807
+ | `timeWindow` | number \| string \| function | Window duration in ms. Function: `(req, key) => number`. |
808
+ | `allowList` | string[] \| function | IPs exempt from limiting. Function: `(req, key) => boolean`. |
809
+ | `keyGenerator` | function | Custom key generator. Signature: `(req) => string \| number`. |
810
+
811
+ ---
812
+
813
+ ## createPolicy
814
+
815
+ Creates row-level security policies for a resource. The service layer evaluates the policy on every CRUD operation to
816
+ enforce fine-grained authorization beyond static role/permission checks.
817
+
818
+ ```ts
819
+ import { createPolicy } from '@appweaver/core';
820
+
821
+ export default createPolicy({
822
+ modelName: 'Product',
823
+ checkAccess: (action, resource) => resource.status === 'Draft',
824
+ readRestrictions: (action, resource) => ({
825
+ enabled: true
826
+ }),
827
+ files: {
828
+ photo: { accessType: 'public' }
829
+ }
830
+ });
831
+ ```
832
+
833
+ ### Configuration
834
+
835
+ ```ts
836
+ function createPolicy(config: ResourcePolicyConfig, override ?: Partial<ResourcePolicyConfig>) {
837
+ }
838
+ ```
839
+
840
+ | Property | Type | Description |
841
+ |---------------------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------|
842
+ | `modelName` | string | Model name to bind this policy to (required). |
843
+ | `checkAccess` | `(action, resource) => boolean` | Dynamic access check against a resource instance. Return `true` to allow, `false` to deny. |
844
+ | `readRestrictions` | `(action, resource) => filter` | Returns a Prisma filter object applied to all read queries (find, query, aggregate). Restricts which records are visible. |
845
+ | `writeRestrictions` | `(action, resource) => data` | Returns data to merge or validate on create/update operations. |
846
+ | `files` | Record\<string, FilePolicy> | Per-file field access policy. |
847
+
848
+ **Action types**: `'find'`, `'query'`, `'aggregate'`, `'create'`, `'update'`, `'delete'`
849
+
850
+ ### File policy
851
+
852
+ | Property | Type | Default | Description |
853
+ |--------------|--------------------------------------------|---------------|--------------------------------------------------------------------------------------------------|
854
+ | `accessType` | `'public'` \| `'protected'` \| `'private'` | `'protected'` | File access level. `public` = anyone, `protected` = authenticated users, `private` = owner only. |
855
+ | `canAccess` | `(user, resource, file) => boolean` | - | Custom access check for reading files. |
856
+ | `canCreate` | `(user, resource, file) => boolean` | - | Custom access check for uploading files. |
857
+ | `canDelete` | `(user, resource, file) => boolean` | - | Custom access check for deleting files. |
858
+
859
+ ---
860
+
861
+ ## registerRoute
862
+
863
+ Registers a custom Fastify route handler outside the resource system. Use this for endpoints that don't map to a
864
+ standard CRUD resource.
865
+
866
+ ```ts
867
+ import { registerRoute, Router } from '@appweaver/core';
868
+ import { Type } from '@sinclair/typebox';
869
+
870
+ registerRoute(
871
+ async function (router: Router) {
872
+ router.get('/search-result', {
873
+ schema: {
874
+ summary: 'Sample search result response route',
875
+ response: { 200: Type.Ref('SearchResult') }
876
+ },
877
+ handler: async () => {
878
+ return { message: 'Hello, world!' };
879
+ }
880
+ });
881
+ },
882
+ { public: true, cacheTTL: 15000 }
883
+ );
884
+ ```
885
+
886
+ ### Config options
887
+
888
+ | Property | Type | Description |
889
+ |-------------------------|--------------------------|---------------------------------------------|
890
+ | `exclude` | boolean | Skip registration of this route. |
891
+ | `public` | boolean | No authentication required. |
892
+ | `roles` | string[] | Required roles. |
893
+ | `permissions` | string[] | Required permissions. |
894
+ | `auth` | AuthType[] | Allowed authentication types. |
895
+ | `rateLimit` | RateLimitConfig \| false | Rate limiting configuration. |
896
+ | `recaptcha` | boolean | Require reCAPTCHA verification. |
897
+ | `recaptchaAction` | string | Expected reCAPTCHA action. |
898
+ | `cache` | boolean | Enable response caching. |
899
+ | `cacheKey` | string \| function | Custom cache key. |
900
+ | `cacheTTL` | number | Cache TTL in milliseconds. |
901
+ | `cacheSkipInvalidation` | boolean | Skip automatic cache invalidation. |
902
+ | `cacheModelName` | string | Model name for cache invalidation tracking. |
903
+ | `cacheRelations` | string[] | Related model names for cache invalidation. |
904
+
905
+ ---
906
+
907
+ ## registerModel
908
+
909
+ Registers a custom TypeBox schema as a named model in the schema registry. Registered models can be referenced using
910
+ `Type.Ref('ModelName')` in route schemas.
911
+
912
+ ```ts
913
+ import { registerModel } from '@appweaver/core';
914
+ import { Nullable } from '@appweaver/common';
915
+ import { Type } from '@sinclair/typebox';
916
+
917
+ registerModel(
918
+ Type.Object(
919
+ {
920
+ id: Type.Integer(),
921
+ title: Type.String({ example: 'My Title' }),
922
+ description: Nullable(Type.String({ maxLength: 512 })),
923
+ score: Type.Number({ minimum: 0, maximum: 1 })
924
+ },
925
+ { $id: 'SearchResult' } // The prefered way for naming the model
926
+ ),
927
+ 'SearchResult' // Model name can be overriden as a second optional argument
928
+ );
929
+ ```
930
+
931
+ | Parameter | Type | Description |
932
+ |-----------|---------|--------------------------------------------------------------|
933
+ | `schema` | TObject | TypeBox object schema definition. |
934
+ | `name` | string? | Override schema name identifier for `Type.Ref()` references. |
935
+
936
+ ---
937
+
938
+ ## registerPlugin
939
+
940
+ Registers a custom Fastify plugin. Plugins are wrapped with `fastify-plugin` so their decorators and hooks are scoped
941
+ to the entire server instance.
942
+
943
+ ```ts
944
+ import { registerPlugin } from '@appweaver/core';
945
+
946
+ registerPlugin(
947
+ 'audit-log',
948
+ async (server) => {
949
+ server.addHook('onResponse', async (request, reply) => {
950
+ logger.info(`${request.method} ${request.url} -> ${reply.statusCode}`);
951
+ });
952
+ },
953
+ ['other-plugin'] // optional dependencies
954
+ );
955
+ ```
956
+
957
+ | Parameter | Type | Description |
958
+ |----------------|-------------------------------------|-------------------------------------------------------|
959
+ | `name` | string | Plugin name (used for dependency resolution). |
960
+ | `plugin` | `(server) => void \| Promise<void>` | Fastify plugin function. |
961
+ | `dependencies` | string[] | Optional list of plugin names this plugin depends on. |