@appweaver/create-weaver-app 1.4.0 → 1.4.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.
|
@@ -1,1424 +1,1478 @@
|
|
|
1
|
-
# Resources
|
|
2
|
-
|
|
3
|
-
Resources are the core building blocks of an Appweaver application. There are four resource types that form a dependency
|
|
4
|
-
chain: **model** → **service** → **routes** → **policy**. Each resource type is created using a corresponding factory
|
|
5
|
-
function and autoloaded from `src/resources/*/` on application start. Source directory and resources pattern could be
|
|
6
|
-
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 CRUD
|
|
18
|
-
operations, and index configuration. It is used to generate Prisma schema, TypeScript types, and route request/response
|
|
19
|
-
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 (`-field` desc, `+field` asc). |
|
|
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)'` \| `'nanoid()'` \| `'autoincrement()'` | `'autoincrement()'` (`'uuid()'` for `string`) | Value generator. String types use UUID/CUID/Nano ID. |
|
|
77
|
-
|
|
78
|
-
Declaring only a string generator (i.e. `{ generator: 'cuid()' }`) infers the `'string'` type.
|
|
79
|
-
|
|
80
|
-
#### String IDs
|
|
81
|
-
|
|
82
|
-
String IDs are generated on creation like auto-incrementing integers, so no value is sent. Both ID types can be mixed
|
|
83
|
-
across models in the same project, and the choice flows through everywhere the primary key appears:
|
|
84
|
-
|
|
85
|
-
| Where | Integer ID | String ID |
|
|
86
|
-
|--------------------------------|--------------------------|-----------------------------------------|
|
|
87
|
-
| Prisma column | `id Int @id` | `id String @id` |
|
|
88
|
-
| Generated TypeScript type | `id: number` | `id: string` |
|
|
89
|
-
| Route path parameter | `GET /posts/{id}` number | `GET /comments/{id}` string |
|
|
90
|
-
| Foreign key on a related model | `authorId Int` | `pinnedCommentId String` |
|
|
91
|
-
| Relation input | `{ author: 12 }` | `{ pinnedComment: 'k4pcxi0t5vs8rl65' }` |
|
|
92
|
-
| `createdById` audit column | `Int?` | `String?` (follows the auth model) |
|
|
93
|
-
| Service methods | `find(12)` | `find('k4pcxi0t5vs8rl65')` |
|
|
94
|
-
|
|
95
|
-
#### Generated column types
|
|
96
|
-
|
|
97
|
-
Generated string columns are sized after the value they hold, on the primary key, the foreign keys referencing it, and
|
|
98
|
-
any string scalar with a `defaultGenerator`. SQLite keeps the plain column.
|
|
99
|
-
|
|
100
|
-
| Generator | PostgreSQL | MySQL | SQL Server |
|
|
101
|
-
|---------------------|-------------------|-------------------|------------------------|
|
|
102
|
-
| `uuid()`, `uuid(7)` | `@db.Uuid` | `@db.Char(36)` | `@db.UniqueIdentifier` |
|
|
103
|
-
| `cuid()` | `@db.VarChar(25)` | `@db.VarChar(25)` | `@db.VarChar(25)` |
|
|
104
|
-
| `cuid(2)` | `@db.VarChar(24)` | `@db.VarChar(24)` | `@db.VarChar(24)` |
|
|
105
|
-
| `nanoid()` | `@db.VarChar(21)` | `@db.VarChar(21)` | `@db.VarChar(21)` |
|
|
106
|
-
|
|
107
|
-
The generator width wins over an explicit `maxLength`, which only bounds what the API accepts.
|
|
108
|
-
|
|
109
|
-
Service and hook signatures take `ResourceId` (`number | string`), so they work with either ID type:
|
|
110
|
-
|
|
111
|
-
```ts
|
|
112
|
-
import { ResourceId } from '@appweaver/common';
|
|
113
|
-
|
|
114
|
-
export default createService({
|
|
115
|
-
modelName: 'Comment',
|
|
116
|
-
beforeFind: (id: ResourceId) => console.log('Finding comment', id)
|
|
117
|
-
});
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
The `resourceId` column of the built-in `File` model stores the owning record ID as text, so files attach to resources
|
|
121
|
-
with either ID type.
|
|
122
|
-
|
|
123
|
-
> Changing the ID type of the existing model rewrites its primary key column and every foreign key pointing at it. Run
|
|
124
|
-
> `weaver generate` then `weaver migration new <name>`, and treat it as destructive on a populated database.
|
|
125
|
-
|
|
126
|
-
### Audit fields
|
|
127
|
-
|
|
128
|
-
It is recommended to always use all audit fields for all resource models, unless specified otherwise. In the usual
|
|
129
|
-
scenario audit should be left out (including all fields by default).
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const config = {
|
|
133
|
-
// By default all audit fields are included
|
|
134
|
-
audit: {
|
|
135
|
-
createdAt: true,
|
|
136
|
-
updatedAt: true,
|
|
137
|
-
createdById: true
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
| Property | Type | Default | Description |
|
|
143
|
-
|---------------|---------|---------|-------------------------------------------------|
|
|
144
|
-
| `createdAt` | boolean | `true` | Add `createdAt` timestamp field. |
|
|
145
|
-
| `updatedAt` | boolean | `true` | Add `updatedAt` timestamp field. |
|
|
146
|
-
| `createdById` | boolean | `true` | Add `createdById` foreign key to the auth user. |
|
|
147
|
-
|
|
148
|
-
### Scalar field types
|
|
149
|
-
|
|
150
|
-
All scalar fields share these common properties:
|
|
151
|
-
|
|
152
|
-
| Property | Type | Default | Description |
|
|
153
|
-
|---------------------|-----------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------|
|
|
154
|
-
| `required` | boolean | `true` | Whether the field is required. |
|
|
155
|
-
| `unique` | boolean | `false` | Add a unique constraint. |
|
|
156
|
-
| `hidden` | boolean | `false` | Hide from API output (e.g. password hashes). |
|
|
157
|
-
| `default` | varies | - | Default static value. |
|
|
158
|
-
| `defaultGenerator` | string | - | Default is generated by function (e.g. uuid(), cuid(), autoincrement(), now(), ...). |
|
|
159
|
-
| `defaultExpression` | string | - | Default is generated by database expression in supported database syntax (e.g. concat('token_', gen_random_uuid()))::TEXT). |
|
|
160
|
-
| `array` | boolean | `false` | Store as array (supported on string, int, float). |
|
|
161
|
-
| `example` | string \| number \| boolean | - | Example value for OpenAPI (Swagger) schema documentation. |
|
|
162
|
-
|
|
163
|
-
A `default` must satisfy the constraints declared on its own field (`minimum`, `maximum`, `minLength`, `maxLength`,
|
|
164
|
-
`pattern`, enum `values`) and match its type. The application **refuses to start** otherwise, naming every offending
|
|
165
|
-
field.
|
|
166
|
-
|
|
167
|
-
#### String
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
const config = {
|
|
171
|
-
title: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
minLength: 1,
|
|
174
|
-
maxLength: 200,
|
|
175
|
-
default: 'No title'
|
|
176
|
-
},
|
|
177
|
-
email: {
|
|
178
|
-
type: 'string',
|
|
179
|
-
format: 'email'
|
|
180
|
-
},
|
|
181
|
-
slug: {
|
|
182
|
-
type: 'string',
|
|
183
|
-
pattern: '^[a-z0-9-]+$'
|
|
184
|
-
},
|
|
185
|
-
code: {
|
|
186
|
-
type: 'string',
|
|
187
|
-
defaultGenerator: 'uuid()'
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
| Property | Type | Description |
|
|
193
|
-
|-------------|---------------------------------------------------------------------------------------|--------------------------------------|
|
|
194
|
-
| `type` | `'string'` | String field type. |
|
|
195
|
-
| `minLength` | number | Minimum string length. |
|
|
196
|
-
| `maxLength` | number | Maximum string length. |
|
|
197
|
-
| `format` | `'email'` \| `'hostname'` \| `'ipv4'` \| `'ipv6'` \| `'uri'` \| `'uuid'` \| `'regex'` | Built-in format validation. |
|
|
198
|
-
| `pattern` | string | Custom regex pattern for validation. |
|
|
199
|
-
|
|
200
|
-
String defaults can also be ID generators: `'uuid()'`, `'uuid(7)'`, `'cuid()'`, `'cuid(2)'`, `'nanoid()'`, which also
|
|
201
|
-
size the column (see [Generated column types](#generated-column-types)).
|
|
202
|
-
|
|
203
|
-
#### Number (int, bigInt, float)
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
const config = {
|
|
207
|
-
price: {
|
|
208
|
-
type: 'float',
|
|
209
|
-
minimum: 0
|
|
210
|
-
},
|
|
211
|
-
quantity: {
|
|
212
|
-
type: 'int',
|
|
213
|
-
minimum: 0,
|
|
214
|
-
maximum: 10000
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
| Property | Type | Description |
|
|
220
|
-
|-----------|------------------------------------|--------------------|
|
|
221
|
-
| `type` | `'int'` \| `'bigInt'` \| `'float'` | Number field type. |
|
|
222
|
-
| `minimum` | number | Minimum value. |
|
|
223
|
-
| `maximum` | number | Maximum value. |
|
|
224
|
-
|
|
225
|
-
Integer defaults can be `'autoincrement()'`.
|
|
226
|
-
|
|
227
|
-
#### Boolean
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
const config = {
|
|
231
|
-
enabled: {
|
|
232
|
-
type: 'boolean',
|
|
233
|
-
default: true
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
| Property | Type | Description |
|
|
239
|
-
|----------|-------------|---------------------|
|
|
240
|
-
| `type` | `'boolean'` | Boolean field type. |
|
|
241
|
-
|
|
242
|
-
#### DateTime
|
|
243
|
-
|
|
244
|
-
```ts
|
|
245
|
-
const config = {
|
|
246
|
-
publishedAt: {
|
|
247
|
-
type: 'dateTime',
|
|
248
|
-
defaultGenerator: 'now()'
|
|
249
|
-
},
|
|
250
|
-
eventDate: {
|
|
251
|
-
type: 'dateTime',
|
|
252
|
-
format: 'date'
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
| Property | Type | Description |
|
|
258
|
-
|----------|---------------------------------------|----------------------|
|
|
259
|
-
| `type` | `'dateTime'` | DateTime field type. |
|
|
260
|
-
| `format` | `'date-time'` \| `'time'` \| `'date'` | DateTime format. |
|
|
261
|
-
|
|
262
|
-
Default can be `'now()'` for current timestamp.
|
|
263
|
-
|
|
264
|
-
#### JSON
|
|
265
|
-
|
|
266
|
-
```ts
|
|
267
|
-
const config = {
|
|
268
|
-
metadata: {
|
|
269
|
-
type: 'json',
|
|
270
|
-
default: {}
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
| Property | Type | Description |
|
|
276
|
-
|----------|----------|------------------------------------------------------|
|
|
277
|
-
| `type` | `'json'` | JSON field type. Stores arbitrary objects or arrays. |
|
|
278
|
-
|
|
279
|
-
#### Enum
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
const config = {
|
|
283
|
-
status: {
|
|
284
|
-
type: 'enum',
|
|
285
|
-
values: ['Draft', 'Active', 'Sold'],
|
|
286
|
-
default: 'Draft'
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
| Property | Type | Description |
|
|
292
|
-
|----------|----------|---------------------------------|
|
|
293
|
-
| `type` | `'enum'` | Enum field type. |
|
|
294
|
-
| `values` | string[] | Allowed enum values (required). |
|
|
295
|
-
|
|
296
|
-
### Relations
|
|
297
|
-
|
|
298
|
-
```ts
|
|
299
|
-
// src/resources/product/model.ts
|
|
300
|
-
const config = {
|
|
301
|
-
relations: {
|
|
302
|
-
category: {
|
|
303
|
-
model: 'Category',
|
|
304
|
-
type: 'oneToMany',
|
|
305
|
-
mappedBy: 'products',
|
|
306
|
-
owner: true,
|
|
307
|
-
output: {
|
|
308
|
-
type: 'always'
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
reviews: {
|
|
312
|
-
model: 'Review',
|
|
313
|
-
type: 'oneToMany',
|
|
314
|
-
mappedBy: 'product',
|
|
315
|
-
output: {
|
|
316
|
-
type: 'single',
|
|
317
|
-
count: true
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
```ts
|
|
325
|
-
// src/resources/category/model.ts
|
|
326
|
-
const config = {
|
|
327
|
-
relations: {
|
|
328
|
-
products: {
|
|
329
|
-
model: 'Product',
|
|
330
|
-
type: 'oneToMany',
|
|
331
|
-
mappedBy: 'category',
|
|
332
|
-
output: {
|
|
333
|
-
type: 'single'
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
```ts
|
|
341
|
-
// src/resources/review/model.ts
|
|
342
|
-
const config = {
|
|
343
|
-
relations: {
|
|
344
|
-
product: {
|
|
345
|
-
model: 'Product',
|
|
346
|
-
type: 'oneToMany',
|
|
347
|
-
mappedBy: 'reviews',
|
|
348
|
-
owner: true,
|
|
349
|
-
input: {
|
|
350
|
-
type: 'none'
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
| Property | Type | Default | Description |
|
|
358
|
-
|-----------------|-------------------------------------------------|--------------|--------------------------------------------------------------------------|
|
|
359
|
-
| `model` | string | **required** | Target model name. |
|
|
360
|
-
| `type` | `'oneToOne'` \| `'oneToMany'` \| `'manyToMany'` | **required** | Relation cardinality between the two models. |
|
|
361
|
-
| `owner` | boolean | `false` | This side owns the foreign key column (only one side should be owner). |
|
|
362
|
-
| `mappedBy` | string | - | Name of the inverse relation on the target model. |
|
|
363
|
-
| `required` | boolean | `true` | Whether the relation is required (nullable foreign key if not required). |
|
|
364
|
-
| `minItems` | number | - | Minimum items for list relations. |
|
|
365
|
-
| `orphanRemoval` | boolean | `false` | Delete orphaned records when parent is deleted. |
|
|
366
|
-
| `onDelete` | ReferentialAction | - | Foreign key action on delete. |
|
|
367
|
-
| `onUpdate` | ReferentialAction | - | Foreign key action on update. |
|
|
368
|
-
| `input` | RelationInput | - | Input DTO configuration. |
|
|
369
|
-
| `output` | RelationOutput | - | Output DTO configuration. |
|
|
370
|
-
|
|
371
|
-
**ReferentialAction values**: `'cascade'`, `'restrict'`, `'noAction'`, `'setNull'`, `'setDefault'`
|
|
372
|
-
|
|
373
|
-
Without an explicit `onDelete`, a **required** owning relation falls back to `restrict`, so deleting the referenced
|
|
374
|
-
record fails with a foreign key violation while any child row still exists. Set `onDelete: 'cascade'` on relations whose
|
|
375
|
-
rows are owned by the parent and meaningless without it. Optional owning relations (`required: false`) fall back to
|
|
376
|
-
`setNull`, which already lets the referenced record be deleted.
|
|
377
|
-
|
|
378
|
-
#### Relationship types
|
|
379
|
-
|
|
380
|
-
The `type` property declares the relation cardinality explicitly, and `owner` marks the side that holds the foreign key
|
|
381
|
-
column in the generated table:
|
|
382
|
-
|
|
383
|
-
**One-to-One** (`type: 'oneToOne'`): Both sides reference a single record. The side with `owner: true` holds a unique
|
|
384
|
-
foreign key; the inverse side is always optional.
|
|
385
|
-
|
|
386
|
-
```ts
|
|
387
|
-
// User model
|
|
388
|
-
const config = {
|
|
389
|
-
relations: {
|
|
390
|
-
profile: {
|
|
391
|
-
model: 'Profile',
|
|
392
|
-
type: 'oneToOne',
|
|
393
|
-
mappedBy: 'user',
|
|
394
|
-
owner: true,
|
|
395
|
-
required: false // otherwise the Profile DTO must be sent when creating the user resource
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
```ts
|
|
402
|
-
// Profile model
|
|
403
|
-
const config = {
|
|
404
|
-
relations: {
|
|
405
|
-
user: {
|
|
406
|
-
model: 'User',
|
|
407
|
-
type: 'oneToOne',
|
|
408
|
-
mappedBy: 'profile'
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
**One-to-Many** (`type: 'oneToMany'`): The "many" side (which holds the foreign key) has `owner: true` and references a
|
|
415
|
-
single record; the "one" side has no `owner` and holds a list of related records.
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
// Category model (one, list side)
|
|
419
|
-
const config = {
|
|
420
|
-
relations: {
|
|
421
|
-
products: {
|
|
422
|
-
model: 'Product',
|
|
423
|
-
type: 'oneToMany',
|
|
424
|
-
mappedBy: 'category'
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
// Product model (many, foreign key side)
|
|
432
|
-
const config = {
|
|
433
|
-
relations: {
|
|
434
|
-
category: {
|
|
435
|
-
model: 'Category',
|
|
436
|
-
type: 'oneToMany',
|
|
437
|
-
mappedBy: 'products',
|
|
438
|
-
owner: true
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
**Many-to-Many** (`type: 'manyToMany'`): Both sides hold lists of related records, joined through an implicit join
|
|
445
|
-
table. The `owner` property has no effect on this relation type.
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
// Post model
|
|
449
|
-
const config = {
|
|
450
|
-
relations: {
|
|
451
|
-
tags: {
|
|
452
|
-
model: 'Tag',
|
|
453
|
-
type: 'manyToMany',
|
|
454
|
-
mappedBy: 'posts'
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
```ts
|
|
461
|
-
// Tag model
|
|
462
|
-
const config = {
|
|
463
|
-
relations: {
|
|
464
|
-
posts: {
|
|
465
|
-
model: 'Post',
|
|
466
|
-
type: 'manyToMany',
|
|
467
|
-
mappedBy: 'tags'
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
#### Relation pair validation
|
|
474
|
-
|
|
475
|
-
`weaver generate` validates every bidirectional relation pair linked through `mappedBy` and fails schema generation with
|
|
476
|
-
a descriptive error when the two sides are inconsistent:
|
|
477
|
-
|
|
478
|
-
- Both sides must declare the same relation `type`.
|
|
479
|
-
- The mapped relation must reference the declaring model back via its `model` property.
|
|
480
|
-
- For `oneToOne` and `oneToMany` relations, exactly one side must declare `owner: true` (neither or both is an error).
|
|
481
|
-
|
|
482
|
-
A relation whose `mappedBy` field does not exist on the target model is treated as single-sided and skipped by the
|
|
483
|
-
validation; an inverse field is generated automatically in the Prisma schema.
|
|
484
|
-
|
|
485
|
-
#### Relation input
|
|
486
|
-
|
|
487
|
-
| Property | Type | Description |
|
|
488
|
-
|---------------|-------------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
|
489
|
-
| `type` | `'all'` \| `'create'` \| `'update'` \| `'none'` | When the relation field is available as input. |
|
|
490
|
-
| `allowCreate` | boolean | Allow creating related records inline (input objects without an `id`). |
|
|
491
|
-
| `allowUpdate` | boolean | Allow updating related records inline on parent update requests (input objects with a required `id`). |
|
|
492
|
-
| `uniqueKey` | string | Unique field matching existing records, turning an inline create into a connect-or-create. |
|
|
493
|
-
|
|
494
|
-
Both flags are off by default: a relation only connects existing records unless `allowCreate` / `allowUpdate` is set.
|
|
495
|
-
|
|
496
|
-
By default, a relation input only connects existing records. It accepts an id value, an `{ id }` object, or an array of
|
|
497
|
-
either for list relations. The `allowCreate` and `allowUpdate` flags also accept the related model's own data:
|
|
498
|
-
|
|
499
|
-
- **`allowCreate: true`** — input objects **without** an `id` create the related record inline. The accepted fields are
|
|
500
|
-
the related model's create data, without its own relations and files (`<Model>RelationCreate`).
|
|
501
|
-
- **`allowUpdate: true`** — input objects **with** an `id` and further fields update the related record inline
|
|
502
|
-
(`<Model>RelationUpdate`). Objects carrying only an `id` are connected instead. This applies to parent **update**
|
|
503
|
-
requests only. On parent **create** requests every object with an `id` is connected, since the database updates
|
|
504
|
-
relations only within an update action.
|
|
505
|
-
|
|
506
|
-
Relations that accept inline writes document their request shape as `<Model>RelationInput`. It holds the id and the
|
|
507
|
-
fields of both shapes above, all optional. The shape stays permissive on purpose, since the server strips the properties
|
|
508
|
-
that the matched schema does not declare. The service applies the restrictions instead. Fields excluded by the related
|
|
509
|
-
model's `create` or `update` config are dropped. A missing required create field fails with a `400` error naming the
|
|
510
|
-
field.
|
|
511
|
-
|
|
512
|
-
Connect, create, and update inputs can be mixed within one list relation request:
|
|
513
|
-
|
|
514
|
-
```ts
|
|
515
|
-
// PUT /api/users/1
|
|
516
|
-
{
|
|
517
|
-
posts: [
|
|
518
|
-
5, // connect post 5 by id
|
|
519
|
-
{ id: 7, title: 'Renamed' }, // update post 7 inline
|
|
520
|
-
{ title: 'Fresh post', slug: 'new' } // create a new post inline
|
|
521
|
-
]
|
|
522
|
-
}
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
Records without an `id` require `allowCreate: true`. Otherwise, the request fails with a `400` error and the related
|
|
526
|
-
record has to be created through its own endpoint first. With `allowCreate` set, a `uniqueKey` matches an existing
|
|
527
|
-
record by that field before creating a new one, so the inline create becomes a connect-or-create. Without
|
|
528
|
-
`allowCreate` the `uniqueKey` has no effect. Plain connect and inline update always match related records by `id`.
|
|
529
|
-
|
|
530
|
-
#### Relation output
|
|
531
|
-
|
|
532
|
-
| Property
|
|
533
|
-
|
|
534
|
-
| `type`
|
|
535
|
-
| `include`
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
```ts
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
```
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
|
828
|
-
|
|
829
|
-
| `
|
|
830
|
-
| `
|
|
831
|
-
| `
|
|
832
|
-
| `
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
|
877
|
-
|
|
878
|
-
| `
|
|
879
|
-
| `
|
|
880
|
-
| `
|
|
881
|
-
| `
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
|
895
|
-
|
|
896
|
-
| `
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
The
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
```
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
```
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
|
1271
|
-
|
|
1272
|
-
| `
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
|
1304
|
-
|
|
1305
|
-
| `
|
|
1306
|
-
| `
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
|
1358
|
-
|
|
1359
|
-
| `
|
|
1360
|
-
| `
|
|
1361
|
-
| `
|
|
1362
|
-
| `
|
|
1363
|
-
| `
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
`
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
Resources are the core building blocks of an Appweaver application. There are four resource types that form a dependency
|
|
4
|
+
chain: **model** → **service** → **routes** → **policy**. Each resource type is created using a corresponding factory
|
|
5
|
+
function and autoloaded from `src/resources/*/` on application start. Source directory and resources pattern could be
|
|
6
|
+
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 CRUD
|
|
18
|
+
operations, and index configuration. It is used to generate Prisma schema, TypeScript types, and route request/response
|
|
19
|
+
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 (`-field` desc, `+field` asc). |
|
|
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)'` \| `'nanoid()'` \| `'autoincrement()'` | `'autoincrement()'` (`'uuid()'` for `string`) | Value generator. String types use UUID/CUID/Nano ID. |
|
|
77
|
+
|
|
78
|
+
Declaring only a string generator (i.e. `{ generator: 'cuid()' }`) infers the `'string'` type.
|
|
79
|
+
|
|
80
|
+
#### String IDs
|
|
81
|
+
|
|
82
|
+
String IDs are generated on creation like auto-incrementing integers, so no value is sent. Both ID types can be mixed
|
|
83
|
+
across models in the same project, and the choice flows through everywhere the primary key appears:
|
|
84
|
+
|
|
85
|
+
| Where | Integer ID | String ID |
|
|
86
|
+
|--------------------------------|--------------------------|-----------------------------------------|
|
|
87
|
+
| Prisma column | `id Int @id` | `id String @id` |
|
|
88
|
+
| Generated TypeScript type | `id: number` | `id: string` |
|
|
89
|
+
| Route path parameter | `GET /posts/{id}` number | `GET /comments/{id}` string |
|
|
90
|
+
| Foreign key on a related model | `authorId Int` | `pinnedCommentId String` |
|
|
91
|
+
| Relation input | `{ author: 12 }` | `{ pinnedComment: 'k4pcxi0t5vs8rl65' }` |
|
|
92
|
+
| `createdById` audit column | `Int?` | `String?` (follows the auth model) |
|
|
93
|
+
| Service methods | `find(12)` | `find('k4pcxi0t5vs8rl65')` |
|
|
94
|
+
|
|
95
|
+
#### Generated column types
|
|
96
|
+
|
|
97
|
+
Generated string columns are sized after the value they hold, on the primary key, the foreign keys referencing it, and
|
|
98
|
+
any string scalar with a `defaultGenerator`. SQLite keeps the plain column.
|
|
99
|
+
|
|
100
|
+
| Generator | PostgreSQL | MySQL | SQL Server |
|
|
101
|
+
|---------------------|-------------------|-------------------|------------------------|
|
|
102
|
+
| `uuid()`, `uuid(7)` | `@db.Uuid` | `@db.Char(36)` | `@db.UniqueIdentifier` |
|
|
103
|
+
| `cuid()` | `@db.VarChar(25)` | `@db.VarChar(25)` | `@db.VarChar(25)` |
|
|
104
|
+
| `cuid(2)` | `@db.VarChar(24)` | `@db.VarChar(24)` | `@db.VarChar(24)` |
|
|
105
|
+
| `nanoid()` | `@db.VarChar(21)` | `@db.VarChar(21)` | `@db.VarChar(21)` |
|
|
106
|
+
|
|
107
|
+
The generator width wins over an explicit `maxLength`, which only bounds what the API accepts.
|
|
108
|
+
|
|
109
|
+
Service and hook signatures take `ResourceId` (`number | string`), so they work with either ID type:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { ResourceId } from '@appweaver/common';
|
|
113
|
+
|
|
114
|
+
export default createService({
|
|
115
|
+
modelName: 'Comment',
|
|
116
|
+
beforeFind: (id: ResourceId) => console.log('Finding comment', id)
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The `resourceId` column of the built-in `File` model stores the owning record ID as text, so files attach to resources
|
|
121
|
+
with either ID type.
|
|
122
|
+
|
|
123
|
+
> Changing the ID type of the existing model rewrites its primary key column and every foreign key pointing at it. Run
|
|
124
|
+
> `weaver generate` then `weaver migration new <name>`, and treat it as destructive on a populated database.
|
|
125
|
+
|
|
126
|
+
### Audit fields
|
|
127
|
+
|
|
128
|
+
It is recommended to always use all audit fields for all resource models, unless specified otherwise. In the usual
|
|
129
|
+
scenario audit should be left out (including all fields by default).
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const config = {
|
|
133
|
+
// By default all audit fields are included
|
|
134
|
+
audit: {
|
|
135
|
+
createdAt: true,
|
|
136
|
+
updatedAt: true,
|
|
137
|
+
createdById: true
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Property | Type | Default | Description |
|
|
143
|
+
|---------------|---------|---------|-------------------------------------------------|
|
|
144
|
+
| `createdAt` | boolean | `true` | Add `createdAt` timestamp field. |
|
|
145
|
+
| `updatedAt` | boolean | `true` | Add `updatedAt` timestamp field. |
|
|
146
|
+
| `createdById` | boolean | `true` | Add `createdById` foreign key to the auth user. |
|
|
147
|
+
|
|
148
|
+
### Scalar field types
|
|
149
|
+
|
|
150
|
+
All scalar fields share these common properties:
|
|
151
|
+
|
|
152
|
+
| Property | Type | Default | Description |
|
|
153
|
+
|---------------------|-----------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------|
|
|
154
|
+
| `required` | boolean | `true` | Whether the field is required. |
|
|
155
|
+
| `unique` | boolean | `false` | Add a unique constraint. |
|
|
156
|
+
| `hidden` | boolean | `false` | Hide from API output (e.g. password hashes). |
|
|
157
|
+
| `default` | varies | - | Default static value. |
|
|
158
|
+
| `defaultGenerator` | string | - | Default is generated by function (e.g. uuid(), cuid(), autoincrement(), now(), ...). |
|
|
159
|
+
| `defaultExpression` | string | - | Default is generated by database expression in supported database syntax (e.g. concat('token_', gen_random_uuid()))::TEXT). |
|
|
160
|
+
| `array` | boolean | `false` | Store as array (supported on string, int, float). |
|
|
161
|
+
| `example` | string \| number \| boolean | - | Example value for OpenAPI (Swagger) schema documentation. |
|
|
162
|
+
|
|
163
|
+
A `default` must satisfy the constraints declared on its own field (`minimum`, `maximum`, `minLength`, `maxLength`,
|
|
164
|
+
`pattern`, enum `values`) and match its type. The application **refuses to start** otherwise, naming every offending
|
|
165
|
+
field.
|
|
166
|
+
|
|
167
|
+
#### String
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const config = {
|
|
171
|
+
title: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
minLength: 1,
|
|
174
|
+
maxLength: 200,
|
|
175
|
+
default: 'No title'
|
|
176
|
+
},
|
|
177
|
+
email: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
format: 'email'
|
|
180
|
+
},
|
|
181
|
+
slug: {
|
|
182
|
+
type: 'string',
|
|
183
|
+
pattern: '^[a-z0-9-]+$'
|
|
184
|
+
},
|
|
185
|
+
code: {
|
|
186
|
+
type: 'string',
|
|
187
|
+
defaultGenerator: 'uuid()'
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
| Property | Type | Description |
|
|
193
|
+
|-------------|---------------------------------------------------------------------------------------|--------------------------------------|
|
|
194
|
+
| `type` | `'string'` | String field type. |
|
|
195
|
+
| `minLength` | number | Minimum string length. |
|
|
196
|
+
| `maxLength` | number | Maximum string length. |
|
|
197
|
+
| `format` | `'email'` \| `'hostname'` \| `'ipv4'` \| `'ipv6'` \| `'uri'` \| `'uuid'` \| `'regex'` | Built-in format validation. |
|
|
198
|
+
| `pattern` | string | Custom regex pattern for validation. |
|
|
199
|
+
|
|
200
|
+
String defaults can also be ID generators: `'uuid()'`, `'uuid(7)'`, `'cuid()'`, `'cuid(2)'`, `'nanoid()'`, which also
|
|
201
|
+
size the column (see [Generated column types](#generated-column-types)).
|
|
202
|
+
|
|
203
|
+
#### Number (int, bigInt, float)
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const config = {
|
|
207
|
+
price: {
|
|
208
|
+
type: 'float',
|
|
209
|
+
minimum: 0
|
|
210
|
+
},
|
|
211
|
+
quantity: {
|
|
212
|
+
type: 'int',
|
|
213
|
+
minimum: 0,
|
|
214
|
+
maximum: 10000
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Property | Type | Description |
|
|
220
|
+
|-----------|------------------------------------|--------------------|
|
|
221
|
+
| `type` | `'int'` \| `'bigInt'` \| `'float'` | Number field type. |
|
|
222
|
+
| `minimum` | number | Minimum value. |
|
|
223
|
+
| `maximum` | number | Maximum value. |
|
|
224
|
+
|
|
225
|
+
Integer defaults can be `'autoincrement()'`.
|
|
226
|
+
|
|
227
|
+
#### Boolean
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const config = {
|
|
231
|
+
enabled: {
|
|
232
|
+
type: 'boolean',
|
|
233
|
+
default: true
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
| Property | Type | Description |
|
|
239
|
+
|----------|-------------|---------------------|
|
|
240
|
+
| `type` | `'boolean'` | Boolean field type. |
|
|
241
|
+
|
|
242
|
+
#### DateTime
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const config = {
|
|
246
|
+
publishedAt: {
|
|
247
|
+
type: 'dateTime',
|
|
248
|
+
defaultGenerator: 'now()'
|
|
249
|
+
},
|
|
250
|
+
eventDate: {
|
|
251
|
+
type: 'dateTime',
|
|
252
|
+
format: 'date'
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
| Property | Type | Description |
|
|
258
|
+
|----------|---------------------------------------|----------------------|
|
|
259
|
+
| `type` | `'dateTime'` | DateTime field type. |
|
|
260
|
+
| `format` | `'date-time'` \| `'time'` \| `'date'` | DateTime format. |
|
|
261
|
+
|
|
262
|
+
Default can be `'now()'` for current timestamp.
|
|
263
|
+
|
|
264
|
+
#### JSON
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
const config = {
|
|
268
|
+
metadata: {
|
|
269
|
+
type: 'json',
|
|
270
|
+
default: {}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Property | Type | Description |
|
|
276
|
+
|----------|----------|------------------------------------------------------|
|
|
277
|
+
| `type` | `'json'` | JSON field type. Stores arbitrary objects or arrays. |
|
|
278
|
+
|
|
279
|
+
#### Enum
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const config = {
|
|
283
|
+
status: {
|
|
284
|
+
type: 'enum',
|
|
285
|
+
values: ['Draft', 'Active', 'Sold'],
|
|
286
|
+
default: 'Draft'
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Property | Type | Description |
|
|
292
|
+
|----------|----------|---------------------------------|
|
|
293
|
+
| `type` | `'enum'` | Enum field type. |
|
|
294
|
+
| `values` | string[] | Allowed enum values (required). |
|
|
295
|
+
|
|
296
|
+
### Relations
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// src/resources/product/model.ts
|
|
300
|
+
const config = {
|
|
301
|
+
relations: {
|
|
302
|
+
category: {
|
|
303
|
+
model: 'Category',
|
|
304
|
+
type: 'oneToMany',
|
|
305
|
+
mappedBy: 'products',
|
|
306
|
+
owner: true,
|
|
307
|
+
output: {
|
|
308
|
+
type: 'always'
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
reviews: {
|
|
312
|
+
model: 'Review',
|
|
313
|
+
type: 'oneToMany',
|
|
314
|
+
mappedBy: 'product',
|
|
315
|
+
output: {
|
|
316
|
+
type: 'single',
|
|
317
|
+
count: true
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
// src/resources/category/model.ts
|
|
326
|
+
const config = {
|
|
327
|
+
relations: {
|
|
328
|
+
products: {
|
|
329
|
+
model: 'Product',
|
|
330
|
+
type: 'oneToMany',
|
|
331
|
+
mappedBy: 'category',
|
|
332
|
+
output: {
|
|
333
|
+
type: 'single'
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
// src/resources/review/model.ts
|
|
342
|
+
const config = {
|
|
343
|
+
relations: {
|
|
344
|
+
product: {
|
|
345
|
+
model: 'Product',
|
|
346
|
+
type: 'oneToMany',
|
|
347
|
+
mappedBy: 'reviews',
|
|
348
|
+
owner: true,
|
|
349
|
+
input: {
|
|
350
|
+
type: 'none'
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
| Property | Type | Default | Description |
|
|
358
|
+
|-----------------|-------------------------------------------------|--------------|--------------------------------------------------------------------------|
|
|
359
|
+
| `model` | string | **required** | Target model name. |
|
|
360
|
+
| `type` | `'oneToOne'` \| `'oneToMany'` \| `'manyToMany'` | **required** | Relation cardinality between the two models. |
|
|
361
|
+
| `owner` | boolean | `false` | This side owns the foreign key column (only one side should be owner). |
|
|
362
|
+
| `mappedBy` | string | - | Name of the inverse relation on the target model. |
|
|
363
|
+
| `required` | boolean | `true` | Whether the relation is required (nullable foreign key if not required). |
|
|
364
|
+
| `minItems` | number | - | Minimum items for list relations. |
|
|
365
|
+
| `orphanRemoval` | boolean | `false` | Delete orphaned records when parent is deleted. |
|
|
366
|
+
| `onDelete` | ReferentialAction | - | Foreign key action on delete. |
|
|
367
|
+
| `onUpdate` | ReferentialAction | - | Foreign key action on update. |
|
|
368
|
+
| `input` | RelationInput | - | Input DTO configuration. |
|
|
369
|
+
| `output` | RelationOutput | - | Output DTO configuration. |
|
|
370
|
+
|
|
371
|
+
**ReferentialAction values**: `'cascade'`, `'restrict'`, `'noAction'`, `'setNull'`, `'setDefault'`
|
|
372
|
+
|
|
373
|
+
Without an explicit `onDelete`, a **required** owning relation falls back to `restrict`, so deleting the referenced
|
|
374
|
+
record fails with a foreign key violation while any child row still exists. Set `onDelete: 'cascade'` on relations whose
|
|
375
|
+
rows are owned by the parent and meaningless without it. Optional owning relations (`required: false`) fall back to
|
|
376
|
+
`setNull`, which already lets the referenced record be deleted.
|
|
377
|
+
|
|
378
|
+
#### Relationship types
|
|
379
|
+
|
|
380
|
+
The `type` property declares the relation cardinality explicitly, and `owner` marks the side that holds the foreign key
|
|
381
|
+
column in the generated table:
|
|
382
|
+
|
|
383
|
+
**One-to-One** (`type: 'oneToOne'`): Both sides reference a single record. The side with `owner: true` holds a unique
|
|
384
|
+
foreign key; the inverse side is always optional.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
// User model
|
|
388
|
+
const config = {
|
|
389
|
+
relations: {
|
|
390
|
+
profile: {
|
|
391
|
+
model: 'Profile',
|
|
392
|
+
type: 'oneToOne',
|
|
393
|
+
mappedBy: 'user',
|
|
394
|
+
owner: true,
|
|
395
|
+
required: false // otherwise the Profile DTO must be sent when creating the user resource
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
// Profile model
|
|
403
|
+
const config = {
|
|
404
|
+
relations: {
|
|
405
|
+
user: {
|
|
406
|
+
model: 'User',
|
|
407
|
+
type: 'oneToOne',
|
|
408
|
+
mappedBy: 'profile'
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**One-to-Many** (`type: 'oneToMany'`): The "many" side (which holds the foreign key) has `owner: true` and references a
|
|
415
|
+
single record; the "one" side has no `owner` and holds a list of related records.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
// Category model (one, list side)
|
|
419
|
+
const config = {
|
|
420
|
+
relations: {
|
|
421
|
+
products: {
|
|
422
|
+
model: 'Product',
|
|
423
|
+
type: 'oneToMany',
|
|
424
|
+
mappedBy: 'category'
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
// Product model (many, foreign key side)
|
|
432
|
+
const config = {
|
|
433
|
+
relations: {
|
|
434
|
+
category: {
|
|
435
|
+
model: 'Category',
|
|
436
|
+
type: 'oneToMany',
|
|
437
|
+
mappedBy: 'products',
|
|
438
|
+
owner: true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Many-to-Many** (`type: 'manyToMany'`): Both sides hold lists of related records, joined through an implicit join
|
|
445
|
+
table. The `owner` property has no effect on this relation type.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
// Post model
|
|
449
|
+
const config = {
|
|
450
|
+
relations: {
|
|
451
|
+
tags: {
|
|
452
|
+
model: 'Tag',
|
|
453
|
+
type: 'manyToMany',
|
|
454
|
+
mappedBy: 'posts'
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
// Tag model
|
|
462
|
+
const config = {
|
|
463
|
+
relations: {
|
|
464
|
+
posts: {
|
|
465
|
+
model: 'Post',
|
|
466
|
+
type: 'manyToMany',
|
|
467
|
+
mappedBy: 'tags'
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### Relation pair validation
|
|
474
|
+
|
|
475
|
+
`weaver generate` validates every bidirectional relation pair linked through `mappedBy` and fails schema generation with
|
|
476
|
+
a descriptive error when the two sides are inconsistent:
|
|
477
|
+
|
|
478
|
+
- Both sides must declare the same relation `type`.
|
|
479
|
+
- The mapped relation must reference the declaring model back via its `model` property.
|
|
480
|
+
- For `oneToOne` and `oneToMany` relations, exactly one side must declare `owner: true` (neither or both is an error).
|
|
481
|
+
|
|
482
|
+
A relation whose `mappedBy` field does not exist on the target model is treated as single-sided and skipped by the
|
|
483
|
+
validation; an inverse field is generated automatically in the Prisma schema.
|
|
484
|
+
|
|
485
|
+
#### Relation input
|
|
486
|
+
|
|
487
|
+
| Property | Type | Description |
|
|
488
|
+
|---------------|-------------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
|
489
|
+
| `type` | `'all'` \| `'create'` \| `'update'` \| `'none'` | When the relation field is available as input. |
|
|
490
|
+
| `allowCreate` | boolean | Allow creating related records inline (input objects without an `id`). |
|
|
491
|
+
| `allowUpdate` | boolean | Allow updating related records inline on parent update requests (input objects with a required `id`). |
|
|
492
|
+
| `uniqueKey` | string | Unique field matching existing records, turning an inline create into a connect-or-create. |
|
|
493
|
+
|
|
494
|
+
Both flags are off by default: a relation only connects existing records unless `allowCreate` / `allowUpdate` is set.
|
|
495
|
+
|
|
496
|
+
By default, a relation input only connects existing records. It accepts an id value, an `{ id }` object, or an array of
|
|
497
|
+
either for list relations. The `allowCreate` and `allowUpdate` flags also accept the related model's own data:
|
|
498
|
+
|
|
499
|
+
- **`allowCreate: true`** — input objects **without** an `id` create the related record inline. The accepted fields are
|
|
500
|
+
the related model's create data, without its own relations and files (`<Model>RelationCreate`).
|
|
501
|
+
- **`allowUpdate: true`** — input objects **with** an `id` and further fields update the related record inline
|
|
502
|
+
(`<Model>RelationUpdate`). Objects carrying only an `id` are connected instead. This applies to parent **update**
|
|
503
|
+
requests only. On parent **create** requests every object with an `id` is connected, since the database updates
|
|
504
|
+
relations only within an update action.
|
|
505
|
+
|
|
506
|
+
Relations that accept inline writes document their request shape as `<Model>RelationInput`. It holds the id and the
|
|
507
|
+
fields of both shapes above, all optional. The shape stays permissive on purpose, since the server strips the properties
|
|
508
|
+
that the matched schema does not declare. The service applies the restrictions instead. Fields excluded by the related
|
|
509
|
+
model's `create` or `update` config are dropped. A missing required create field fails with a `400` error naming the
|
|
510
|
+
field.
|
|
511
|
+
|
|
512
|
+
Connect, create, and update inputs can be mixed within one list relation request:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
// PUT /api/users/1
|
|
516
|
+
{
|
|
517
|
+
posts: [
|
|
518
|
+
5, // connect post 5 by id
|
|
519
|
+
{ id: 7, title: 'Renamed' }, // update post 7 inline
|
|
520
|
+
{ title: 'Fresh post', slug: 'new' } // create a new post inline
|
|
521
|
+
]
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Records without an `id` require `allowCreate: true`. Otherwise, the request fails with a `400` error and the related
|
|
526
|
+
record has to be created through its own endpoint first. With `allowCreate` set, a `uniqueKey` matches an existing
|
|
527
|
+
record by that field before creating a new one, so the inline create becomes a connect-or-create. Without
|
|
528
|
+
`allowCreate` the `uniqueKey` has no effect. Plain connect and inline update always match related records by `id`.
|
|
529
|
+
|
|
530
|
+
#### Relation output
|
|
531
|
+
|
|
532
|
+
| Property | Type | Description |
|
|
533
|
+
|------------|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
|
|
534
|
+
| `type` | `'always'` \| `'single'` \| `'multiple'` \| `'none'` | When to include the relation in output. `always` = all reads, `single` = single record reads, `multiple` = list reads, `none` = never. |
|
|
535
|
+
| `include` | Record\<string, RelationOutput> | Nested relation output configuration. |
|
|
536
|
+
| `maxDepth` | number | Levels of a relation pointing back at its own model. Default `1`. |
|
|
537
|
+
| `count` | boolean | Include a count of related records. |
|
|
538
|
+
|
|
539
|
+
A relation is typed as the related model's `<Model>Single`, so a nested record carries its own relations again and a
|
|
540
|
+
self-reference recurses. The schema says what a response can hold, the config how deep one is read.
|
|
541
|
+
|
|
542
|
+
A relation pointing back at its own model is read one level deep like any other, and `maxDepth` repeats it down the
|
|
543
|
+
tree, counting the relation itself as the first level. An `include` naming that same relation replaces the repetition;
|
|
544
|
+
any other `include` is applied at every level. Each level is a database join, cheap on a to-one relation such as
|
|
545
|
+
`parent` and expensive on a list one such as `children`.
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
const config = {
|
|
549
|
+
relations: {
|
|
550
|
+
// A category response carries three levels of ancestors
|
|
551
|
+
parent: {
|
|
552
|
+
model: 'Category',
|
|
553
|
+
type: 'oneToMany',
|
|
554
|
+
mappedBy: 'children',
|
|
555
|
+
owner: true,
|
|
556
|
+
required: false,
|
|
557
|
+
output: { type: 'always', maxDepth: 3 }
|
|
558
|
+
},
|
|
559
|
+
// Kept out of the response, counted as childrenCount instead
|
|
560
|
+
children: {
|
|
561
|
+
model: 'Category',
|
|
562
|
+
type: 'oneToMany',
|
|
563
|
+
mappedBy: 'parent',
|
|
564
|
+
output: { type: 'none', count: true }
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
A nested `include` entry carries its own `maxDepth`, applied to the model that entry belongs to:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
// A post reads its category with the breadcrumb above it
|
|
574
|
+
const config = {
|
|
575
|
+
category: {
|
|
576
|
+
model: 'Category',
|
|
577
|
+
type: 'oneToMany',
|
|
578
|
+
mappedBy: 'posts',
|
|
579
|
+
owner: true,
|
|
580
|
+
output: {
|
|
581
|
+
type: 'always',
|
|
582
|
+
include: { parent: { type: 'always', maxDepth: 3 } }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### File fields
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
const config = {
|
|
592
|
+
files: {
|
|
593
|
+
photo: {
|
|
594
|
+
mimeType: 'image/*',
|
|
595
|
+
namePattern: 'photos/{userId}-{name}-{hash}.{extension}',
|
|
596
|
+
maxSize: '2 MB',
|
|
597
|
+
image: {
|
|
598
|
+
quality: 80,
|
|
599
|
+
maxWidth: 1200,
|
|
600
|
+
maxHeight: 1200,
|
|
601
|
+
fit: 'inside'
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
documents: {
|
|
605
|
+
mimeType: 'application/pdf',
|
|
606
|
+
array: true,
|
|
607
|
+
maxCount: 5
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
| Property | Type | Description |
|
|
614
|
+
|---------------------|------------------------|-------------------------------------------------------------------------------------------------------------|
|
|
615
|
+
| `mimeType` | string \| RegExp | Allowed MIME types (glob patterns like `'image/*'` supported). |
|
|
616
|
+
| `namePattern` | string \| function | File naming pattern or function (available variables are listed below). |
|
|
617
|
+
| `array` | boolean | Allow multiple files. |
|
|
618
|
+
| `maxSize` | number \| string | Maximum file size (e.g. `'2 MB'`, `5242880`). |
|
|
619
|
+
| `maxCount` | number | Maximum number of files (for array fields). |
|
|
620
|
+
| `output` | RelationOutput | When to include file info in output, and its count. Takes no `include` or `maxDepth`. |
|
|
621
|
+
| `onResourceDeleted` | `'delete'` \| `'keep'` | When the owning resource is deleted. `'delete'` (default) removes files from storage, `'keep'` leaves them. |
|
|
622
|
+
| `image` | ImageConfig | Image compression and resize settings. Only applies to image MIME types (excluding GIF). |
|
|
623
|
+
|
|
624
|
+
#### Available namePattern variables
|
|
625
|
+
|
|
626
|
+
Default pattern is: `{name}-{hash}.{extension}`.
|
|
627
|
+
|
|
628
|
+
| Variable | Type | Description |
|
|
629
|
+
|-----------------|--------|---------------------------------------------|
|
|
630
|
+
| `name` | string | Original filename without extension. |
|
|
631
|
+
| `extension` | string | Original file extension. |
|
|
632
|
+
| `resourceField` | string | Field name the file is assigned to. |
|
|
633
|
+
| `resourceName` | string | Resource model name. |
|
|
634
|
+
| `resourceId` | string | Resource ID. |
|
|
635
|
+
| `userId` | string | Authenticated user ID. |
|
|
636
|
+
| `userEmail` | string | Authenticated user email. |
|
|
637
|
+
| `year` | number | Current UTC year. |
|
|
638
|
+
| `month` | number | Current UTC month (1-12). |
|
|
639
|
+
| `day` | number | Current UTC day of month. |
|
|
640
|
+
| `weekDay` | number | Current UTC day of week (0-6, Sunday is 0). |
|
|
641
|
+
| `yearWeek` | number | ISO week number. |
|
|
642
|
+
| `yearDay` | number | Day of year (1-366). |
|
|
643
|
+
| `hours` | number | Current UTC hours. |
|
|
644
|
+
| `minutes` | number | Current UTC minutes. |
|
|
645
|
+
| `seconds` | number | Current UTC seconds. |
|
|
646
|
+
| `milliseconds` | number | Current UTC milliseconds. |
|
|
647
|
+
| `timestamp` | number | Unix timestamp in milliseconds. |
|
|
648
|
+
| `date` | string | Current date in ISO 8601 format. |
|
|
649
|
+
| `uuid` | string | Generated random UUID. |
|
|
650
|
+
| `hash` | string | Generated random hash (32 bytes). |
|
|
651
|
+
|
|
652
|
+
#### Image compression
|
|
653
|
+
|
|
654
|
+
Configure automatic image compression and resizing by adding the `image` property to a file field. Processing only
|
|
655
|
+
applies to supported image MIME types: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `image/tiff`. GIF files
|
|
656
|
+
are passed through unchanged.
|
|
657
|
+
|
|
658
|
+
| Property | Type | Description |
|
|
659
|
+
|-------------|----------|----------------------------------------------------------------------------------------------------------------|
|
|
660
|
+
| `quality` | number | Compression quality (1-100). Applies to JPEG, PNG, WebP, AVIF, and TIFF. |
|
|
661
|
+
| `width` | number | Exact resize width in pixels. |
|
|
662
|
+
| `height` | number | Exact resize height in pixels. |
|
|
663
|
+
| `maxWidth` | number | Maximum width. Only downscales if the image exceeds this dimension. |
|
|
664
|
+
| `maxHeight` | number | Maximum height. Only downscales if the image exceeds this dimension. |
|
|
665
|
+
| `fit` | ImageFit | How the image fits the target dimensions: `'inside'` (default), `'contain'`, `'cover'`, `'fill'`, `'outside'`. |
|
|
666
|
+
|
|
667
|
+
`width`/`height` take precedence over `maxWidth`/`maxHeight`. When using `maxWidth`/`maxHeight`, images smaller than the
|
|
668
|
+
specified dimensions are not enlarged.
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
// Compress and limit dimensions
|
|
672
|
+
const config = {
|
|
673
|
+
files: {
|
|
674
|
+
avatar: {
|
|
675
|
+
mimeType: 'image/*',
|
|
676
|
+
maxSize: '5 MB',
|
|
677
|
+
image: { quality: 80, maxWidth: 800, maxHeight: 800 }
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
```ts
|
|
684
|
+
// Exact resize for thumbnails
|
|
685
|
+
const config = {
|
|
686
|
+
files: {
|
|
687
|
+
thumbnail: {
|
|
688
|
+
mimeType: 'image/jpeg',
|
|
689
|
+
image: { quality: 70, width: 200, height: 200, fit: 'inside' }
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Virtual fields
|
|
696
|
+
|
|
697
|
+
Virtual fields are computed values not stored in the database. They can appear in input DTOs (to receive data) and/or
|
|
698
|
+
output DTOs (to return computed values).
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
const config = {
|
|
702
|
+
virtual: {
|
|
703
|
+
displayName: {
|
|
704
|
+
type: 'string',
|
|
705
|
+
output: {
|
|
706
|
+
type: 'always',
|
|
707
|
+
value: (resource) => `${resource.firstName} ${resource.lastName}`
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
inviteCode: {
|
|
711
|
+
type: 'string',
|
|
712
|
+
input: {
|
|
713
|
+
type: 'create'
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
| Property | Type | Description |
|
|
721
|
+
|------------------|------------------------------------------------------|------------------------------------------------------------|
|
|
722
|
+
| *(scalar props)* | - | All scalar field properties (type, minLength, etc.) apply. |
|
|
723
|
+
| `input.type` | `'all'` \| `'create'` \| `'update'` \| `'none'` | When the virtual field accepts input. |
|
|
724
|
+
| `input.value` | primitive \| function | Default value or transformer for input. |
|
|
725
|
+
| `output.type` | `'always'` \| `'single'` \| `'multiple'` \| `'none'` | When the virtual field appears in output. |
|
|
726
|
+
| `output.value` | primitive \| function | Computed value or transformer for output. |
|
|
727
|
+
|
|
728
|
+
Virtual output values are applied automatically to responses of resource CRUD routes (including nested relation and file
|
|
729
|
+
objects) and to responses of custom `registerRoute` routes whose 2xx response schemas reference resource output models.
|
|
730
|
+
To apply them manually on a raw resource object (e.g. one fetched directly through a Prisma client), use the
|
|
731
|
+
`projectVirtualFields` helper:
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
import { projectVirtualFields } from '@appweaver/core';
|
|
735
|
+
|
|
736
|
+
const projected = projectVirtualFields(post, 'Post'); // sets virtual values, recursing into relations and files
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Operation config (read, create, update)
|
|
740
|
+
|
|
741
|
+
Control which fields appear in each DTO. Use `pick` for an allowlist or `omit` for a deny-list.
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
const config = {
|
|
745
|
+
create: {
|
|
746
|
+
omit: ['status'] // All fields except status
|
|
747
|
+
},
|
|
748
|
+
update: {
|
|
749
|
+
pick: ['title', 'price'] // Only title and price
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
| Property | Type | Description |
|
|
755
|
+
|----------|----------|------------------------------------------------|
|
|
756
|
+
| `omit` | string[] | Fields to exclude from the DTO. |
|
|
757
|
+
| `pick` | string[] | Fields to include in the DTO (overrides omit). |
|
|
758
|
+
|
|
759
|
+
### Export config
|
|
760
|
+
|
|
761
|
+
Configure CSV export behavior per field:
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
const config = {
|
|
765
|
+
export: {
|
|
766
|
+
price: {
|
|
767
|
+
headerName: 'Product Price',
|
|
768
|
+
mapValue: 'price'
|
|
769
|
+
},
|
|
770
|
+
internalNotes: {
|
|
771
|
+
exclude: true
|
|
772
|
+
},
|
|
773
|
+
status: {
|
|
774
|
+
mapValue: (val) => val.toUpperCase()
|
|
775
|
+
},
|
|
776
|
+
author: {
|
|
777
|
+
firstName: {
|
|
778
|
+
headerName: 'Given Name'
|
|
779
|
+
},
|
|
780
|
+
lastName: {
|
|
781
|
+
headerName: 'Family Name'
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
| Property | Type | Description |
|
|
789
|
+
|--------------|--------------------|------------------------------------|
|
|
790
|
+
| `headerName` | string | Custom CSV column header name. |
|
|
791
|
+
| `exclude` | boolean | Exclude this field from exports. |
|
|
792
|
+
| `mapValue` | string \| function | Transform the value during export. |
|
|
793
|
+
|
|
794
|
+
A `string` `mapValue` names the field to read the column value from. On a relation or file field it is read off the
|
|
795
|
+
related record (and off every item for array relations, joined with `,`); on a scalar field it is read off the exported
|
|
796
|
+
record itself. A function `mapValue` receives the field value (or each item of an array field) and returns the column
|
|
797
|
+
value.
|
|
798
|
+
|
|
799
|
+
Hidden fields and virtual fields with `output: { type: 'none' }` are never exported, nested in a relation either. A
|
|
800
|
+
relation without a `mapValue` writes one column per field of the related record.
|
|
801
|
+
|
|
802
|
+
### Index config
|
|
803
|
+
|
|
804
|
+
Define database indexes as a flat array (single-field indexes) or nested arrays (composite indexes):
|
|
805
|
+
|
|
806
|
+
```ts
|
|
807
|
+
index: ['title'] // Single-field index on title
|
|
808
|
+
index: [['status', 'categoryId']] // Composite index on status + categoryId
|
|
809
|
+
index: ['email', ['status', 'createdAt']] // Both single and composite
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Prefix a field name with `-` for a descending index or `+` for an ascending one. Without a prefix the database default
|
|
813
|
+
order is used:
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
index: ['-createdAt'] // @@index(createdAt(sort: Desc))
|
|
817
|
+
index: ['+title'] // @@index(title(sort: Asc))
|
|
818
|
+
index: [['status', '-createdAt']] // @@index([status, createdAt(sort: Desc)])
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
The prefix is part of the index identity, so `['createdAt', '-createdAt']` emits two separate indexes.
|
|
822
|
+
|
|
823
|
+
### Generated models
|
|
824
|
+
|
|
825
|
+
`createModel` produces the following TypeBox schema models used internally by routes and services:
|
|
826
|
+
|
|
827
|
+
| Model | Purpose |
|
|
828
|
+
|------------------------|------------------------------------------------|
|
|
829
|
+
| `readModel` | Full model with all visible fields |
|
|
830
|
+
| `createModel` | Request body for create operations |
|
|
831
|
+
| `updateModel` | Request body for update operations |
|
|
832
|
+
| `relationsModel` | Relations-only subset |
|
|
833
|
+
| `virtualModel` | Virtual fields-only subset |
|
|
834
|
+
| `filesModel` | File fields-only subset |
|
|
835
|
+
| `readOneModel` | Response for single-item reads |
|
|
836
|
+
| `readManyModel` | Response for list reads |
|
|
837
|
+
| `readOneNullableModel` | `readOneModel` or null, for optional relations |
|
|
838
|
+
| `createOneModel` | Request for create endpoint |
|
|
839
|
+
| `updateOneModel` | Request for update endpoint |
|
|
840
|
+
| `fileUploadModel` | Request for file upload endpoint |
|
|
841
|
+
| `fileDeleteModel` | Request for file delete endpoint |
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## createService
|
|
846
|
+
|
|
847
|
+
Creates a resource service with lifecycle hooks and business logic. The service handles all database operations for a
|
|
848
|
+
model and triggers hooks on each CRUD operation before/after.
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
import { createService } from '@appweaver/core';
|
|
852
|
+
|
|
853
|
+
export default createService({
|
|
854
|
+
modelName: 'Product',
|
|
855
|
+
afterCreate: (resource) => {
|
|
856
|
+
logger.info(`Product created: ${resource.id}`);
|
|
857
|
+
},
|
|
858
|
+
textSearch: {
|
|
859
|
+
title: { contains: '{input}', mode: 'insensitive' }
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
### Configuration
|
|
865
|
+
|
|
866
|
+
```ts
|
|
867
|
+
function createService(config: ResourceServiceConfig, override ?: Partial<ResourceServiceConfig>) {
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
| Property | Type | Description |
|
|
872
|
+
|-------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
|
|
873
|
+
| `modelName` | string | Model name to bind this service to (required). |
|
|
874
|
+
| `beforeFind` | `(id) => void` | Hook called before finding a single resource. |
|
|
875
|
+
| `beforeQuery` | `(filter, page, size, sort, cursor, totalCount) => void` | Hook called before querying resources. `sort` is a field list string or a sort object. |
|
|
876
|
+
| `beforeAggregate` | `(filter, select, dateField, from?, to?, step?, safeIncrement?) => void` | Hook called before aggregation. |
|
|
877
|
+
| `beforeCreate` | `(data) => void` | Hook called before creating a resource. Mutate `data` to modify input. |
|
|
878
|
+
| `beforeUpdate` | `(id, data) => void` | Hook called before updating a resource. |
|
|
879
|
+
| `beforeDelete` | `(id) => void` | Hook called before deleting a resource. |
|
|
880
|
+
| `afterFind` | `(resource) => void` | Hook called after finding a resource. |
|
|
881
|
+
| `afterQuery` | `(response) => void` | Hook called after querying resources. |
|
|
882
|
+
| `afterAggregate` | `(response) => void` | Hook called after aggregation. |
|
|
883
|
+
| `afterCreate` | `(resource) => void` | Hook called after creating a resource. |
|
|
884
|
+
| `afterUpdate` | `(resource) => void` | Hook called after updating a resource. |
|
|
885
|
+
| `afterDelete` | `(resource) => void` | Hook called after deleting a resource. |
|
|
886
|
+
| `textSearch` | object \| function | Prisma filter object or function `(input: string) => filter` for text search. Use `'{input}'` as placeholder in filter objects. |
|
|
887
|
+
|
|
888
|
+
All hooks can be synchronous or return a `Promise`.
|
|
889
|
+
|
|
890
|
+
### Service methods
|
|
891
|
+
|
|
892
|
+
The created service exposes the following methods:
|
|
893
|
+
|
|
894
|
+
| Method | Signature | Description |
|
|
895
|
+
|-------------|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|
896
|
+
| `find` | `(id) => Promise<ReadOne>` | Find a single resource by ID. |
|
|
897
|
+
| `query` | `(filter?, page?, size?, sort?, cursor?, totalCount?) => Promise<QueryResponse>` | Query resources with filtering, pagination, and sorting (see [Query sorting](#query-sorting) and [Cursor pagination](#cursor-pagination)). |
|
|
898
|
+
| `aggregate` | `(filter?, select?, dateField?, from?, to?, step?, safeIncrement?) => Promise<AggregateResponse>` | Aggregate resources with time-series grouping (see [Aggregate selection](#aggregate-selection)). |
|
|
899
|
+
| `create` | `(data) => Promise<ReadOne>` | Create a new resource. |
|
|
900
|
+
| `update` | `(id, data) => Promise<ReadOne>` | Update an existing resource. |
|
|
901
|
+
| `delete` | `(id) => Promise<ReadOne>` | Delete a resource. |
|
|
902
|
+
| `client` | `ResourceClient` (property) | Database client of the model, for operations outside the model contract. |
|
|
903
|
+
|
|
904
|
+
### Typed service injection
|
|
905
|
+
|
|
906
|
+
`weaver generate` emits a `<Model>ResourceService` alias per model, so `injectService` needs no hand-written type:
|
|
907
|
+
|
|
908
|
+
```ts
|
|
909
|
+
import { injectService } from '@appweaver/core';
|
|
910
|
+
import { PostResourceService } from '@/types/generated';
|
|
911
|
+
|
|
912
|
+
const posts = injectService<PostResourceService>('Post');
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
The alias is `IResourceService<<Model>, <Model>Multiple, <Model>Create, <Model>Update, <Model>Query>`, so the
|
|
916
|
+
`<Model>Query`, `<Model>Sort`, and `<Model>Aggregate` aliases are exactly the inputs its methods accept.
|
|
917
|
+
|
|
918
|
+
The `create` and `update` inputs are the model's declared contracts, so a field an operation config omits, a hidden
|
|
919
|
+
scalar, or a relation with `input: { type: 'none' }` is deliberately not part of them. A write outside the contract
|
|
920
|
+
belongs on `service.client`, the database client of the model.
|
|
921
|
+
|
|
922
|
+
### Query filters
|
|
923
|
+
|
|
924
|
+
The `filter` argument of `query`, `aggregate`, and `export` mirrors the WHERE part of a database query. The matching
|
|
925
|
+
`POST /query`, `POST /aggregate`, and `POST /export` routes accept the same structure, validated against a generated
|
|
926
|
+
per-model `<Model>QueryFilter` schema that strips unknown and hidden fields.
|
|
927
|
+
|
|
928
|
+
**Logical operators** (filter level) — take a single filter object (each entry becomes one condition) or a list of them:
|
|
929
|
+
|
|
930
|
+
| Operator | Description |
|
|
931
|
+
|----------|-------------------------------------------|
|
|
932
|
+
| `_and` | All nested conditions must match. |
|
|
933
|
+
| `_or` | At least one nested condition must match. |
|
|
934
|
+
| `_not` | No nested condition may match. |
|
|
935
|
+
| `_nor` | Alias of `_not`. |
|
|
936
|
+
|
|
937
|
+
**Comparison operators** (field level) — combined inside one object, all must match:
|
|
938
|
+
|
|
939
|
+
| Operator | Description |
|
|
940
|
+
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
|
941
|
+
| `_eq` | Equal to the given value. |
|
|
942
|
+
| `_ne` | Not equal to the given value. |
|
|
943
|
+
| `_gt`, `_gte`, `_lt`, `_lte` | Greater/lower than (or equal to) the given value. |
|
|
944
|
+
| `_in`, `_nin` | Included / not included in the given list. |
|
|
945
|
+
| `_between` | Inside the inclusive `[min, max]` range. |
|
|
946
|
+
| `_like` | SQL LIKE pattern with `%` wildcards (`Luk%` → starts with, `%avatar%` → contains, `%png` → ends with, no wildcard → equals). |
|
|
947
|
+
| `_ilike` | Case-insensitive `_like` (uses `mode: 'insensitive'`, PostgreSQL and MongoDB only). |
|
|
948
|
+
| `_starts`, `_ends`, `_contains` | Starts with / ends with / contains the given string. |
|
|
949
|
+
| `_exists` | Not null (`true`) or null (`false`). |
|
|
950
|
+
| `_not` | Negates a nested operator object or plain value. |
|
|
951
|
+
|
|
952
|
+
**List (array scalar) operators**: `_has`, `_hasSome`, `_hasEvery`, `_isEmpty`.
|
|
953
|
+
|
|
954
|
+
**Relation operators**: `_some`, `_every`, `_none` take a filter of the related model; `_exists` maps to an `is`/`isNot`
|
|
955
|
+
null check on a single relation and to `some`/`none` on a list relation.
|
|
956
|
+
|
|
957
|
+
**Plain value shorthands**: a bare value matches by equality, a list by inclusion, a two-value list on a numeric or date
|
|
958
|
+
field as an inclusive range, a value or list on a relation by id, an array field uses `has`/`hasSome`, and `null`
|
|
959
|
+
matches missing values or related records.
|
|
960
|
+
|
|
961
|
+
```json
|
|
962
|
+
{
|
|
963
|
+
"filter": {
|
|
964
|
+
"_and": {
|
|
965
|
+
"firstName": {
|
|
966
|
+
"_eq": "Luka",
|
|
967
|
+
"_exists": true
|
|
968
|
+
},
|
|
969
|
+
"avatar": {
|
|
970
|
+
"_or": {
|
|
971
|
+
"title": {
|
|
972
|
+
"_eq": "New user avatar"
|
|
973
|
+
},
|
|
974
|
+
"description": {
|
|
975
|
+
"_like": "%avatar%"
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
"originalName": {
|
|
979
|
+
"_eq": "new_user_avatar.png"
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
"_or": [
|
|
984
|
+
{
|
|
985
|
+
"firstName": {
|
|
986
|
+
"_like": "Luk%"
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
"lastName": "Matošević"
|
|
991
|
+
}
|
|
992
|
+
],
|
|
993
|
+
"tags": {
|
|
994
|
+
"_some": {
|
|
995
|
+
"name": {
|
|
996
|
+
"_contains": "news"
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
"page": 1,
|
|
1002
|
+
"size": 50,
|
|
1003
|
+
"sort": "-createdAt",
|
|
1004
|
+
"totalCount": true
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
The `QueryFilter<T>` type from `@appweaver/common` provides code completion, and `weaver generate` emits a
|
|
1009
|
+
`<Model>Query = QueryFilter<Model>` alias per model:
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
import { QueryFilter } from '@appweaver/common';
|
|
1013
|
+
import { User, UserQuery } from '@/types/generated';
|
|
1014
|
+
|
|
1015
|
+
const filter: UserQuery = {
|
|
1016
|
+
_and: {
|
|
1017
|
+
firstName: { _eq: 'Luka' },
|
|
1018
|
+
loginAt: { _exists: true }
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const users = await userService.query(filter);
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Query sorting
|
|
1025
|
+
|
|
1026
|
+
The `sort` argument of `query` and `export` (and the `sort` property of the `POST /query` and `POST /export` request
|
|
1027
|
+
bodies) accepts two interchangeable forms, both applying their fields in the declared order:
|
|
1028
|
+
|
|
1029
|
+
```json
|
|
1030
|
+
{
|
|
1031
|
+
"sort": "-author.createdAt,tagsCount,id"
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
```json
|
|
1036
|
+
{
|
|
1037
|
+
"sort": {
|
|
1038
|
+
"author": {
|
|
1039
|
+
"createdAt": "desc"
|
|
1040
|
+
},
|
|
1041
|
+
"tagsCount": "asc",
|
|
1042
|
+
"id": "asc"
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
In the string form a `-` prefix sorts descending (`+` or no prefix ascending) and a dot notation path targets a relation
|
|
1048
|
+
field. In the object form a relation takes a nested object, and the only accepted directions are the lower case `asc`
|
|
1049
|
+
and `desc`.
|
|
1050
|
+
|
|
1051
|
+
| Field | String form | Object form | Notes |
|
|
1052
|
+
|------------------------|---------------------|-------------------------------------|----------------------------------------------------------------------------------------|
|
|
1053
|
+
| Scalar, `id`, audit | `title`, `-id` | `{ title: 'asc' }` | Hidden scalars, array scalars, and virtual fields cannot be sorted by. |
|
|
1054
|
+
| To-one relation field | `-author.createdAt` | `{ author: { createdAt: 'desc' } }` | The relation must be included in the response of the action, at any nesting depth. |
|
|
1055
|
+
| To-many relation count | `-tagsCount` | `{ tagsCount: 'desc' }` | Sorts by the number of related records; the relation name alone (`-tags`) is an alias. |
|
|
1056
|
+
|
|
1057
|
+
Anything else — a relation the action does not include, a field of a to-many relation, a hidden or virtual field, an
|
|
1058
|
+
unknown sort direction — is rejected with a `400` error naming the offending field instead of reaching the database.
|
|
1059
|
+
Over HTTP the sort object is additionally validated against a generated per-model `<Model>QuerySort` schema, which
|
|
1060
|
+
strips unknown fields the same way the query filter schema does.
|
|
1061
|
+
|
|
1062
|
+
The default sort is `-createdAt`. Every sort is terminated with the primary key when it does not already order by one,
|
|
1063
|
+
so paging stays deterministic, and the `createdAt` entry is dropped for models configured with
|
|
1064
|
+
`audit: { createdAt: false }`.
|
|
1065
|
+
|
|
1066
|
+
Sort inputs are typed by `QuerySort<T>` from `@appweaver/common`, and `weaver generate` emits a
|
|
1067
|
+
`<Model>Sort = QuerySort<<Model>Multiple>` alias per model, built from the query output model so it only offers the
|
|
1068
|
+
relations a query response includes:
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
import { PostSort } from '@/types/generated';
|
|
1072
|
+
|
|
1073
|
+
const sort: PostSort = { author: { lastName: 'asc' }, createdAt: 'desc' };
|
|
1074
|
+
const posts = await postService.query({}, 1, 50, sort);
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### Query response
|
|
1078
|
+
|
|
1079
|
+
```ts
|
|
1080
|
+
const config = {
|
|
1081
|
+
resultCount: 50, // Items in this page
|
|
1082
|
+
totalCount: 123, // Total items matching filter, omitted when totalCount is false
|
|
1083
|
+
nextCursor: '...', // Cursor of the following page, absent on the last page
|
|
1084
|
+
prevCursor: '...', // Cursor of the preceding page, absent on the first page
|
|
1085
|
+
items: [] // Page data
|
|
1086
|
+
};
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
### Cursor pagination
|
|
1090
|
+
|
|
1091
|
+
The response returns a `nextCursor` and a `prevCursor`; send one back as `cursor` to get that page. The direction is
|
|
1092
|
+
part of the cursor, so a request never names one. A cursor takes precedence over `page` and does not slow down on the
|
|
1093
|
+
later pages.
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
// First page counted, the following ones skipping the count
|
|
1097
|
+
let result = await postService.query({}, 1, 50);
|
|
1098
|
+
|
|
1099
|
+
while (result.nextCursor) {
|
|
1100
|
+
result = await postService.query({}, 1, 50, undefined, result.nextCursor, false);
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
```json5
|
|
1105
|
+
// POST /posts/query
|
|
1106
|
+
{
|
|
1107
|
+
"filter": {
|
|
1108
|
+
"enabled": true
|
|
1109
|
+
},
|
|
1110
|
+
"size": 50,
|
|
1111
|
+
"sort": "-createdAt",
|
|
1112
|
+
"cursor": "eyJpIjo0MiwiZiI6IkhkQjVfa2VMTVlyNyJ9",
|
|
1113
|
+
"totalCount": false
|
|
1114
|
+
}
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
`totalCount` defaults to `true` and scans every matching record, so count once and send `false` afterward, which returns
|
|
1118
|
+
it as `null`.
|
|
1119
|
+
|
|
1120
|
+
A cursor is opaque and bound to the query that issued it: reusing one under a different resource, filter, or sort is
|
|
1121
|
+
rejected with a 400.
|
|
1122
|
+
|
|
1123
|
+
### Aggregate selection
|
|
1124
|
+
|
|
1125
|
+
The `select` argument of `aggregate` (and the required `select` property of the `POST /aggregate` request body) holds
|
|
1126
|
+
the operators to apply per field. Only the fields the database can aggregate are accepted, which are the numeric and
|
|
1127
|
+
date scalars of the model together with its numeric `id` and audit fields:
|
|
1128
|
+
|
|
1129
|
+
| Field kind | Operators |
|
|
1130
|
+
|------------------------------------|------------------------------------------------------|
|
|
1131
|
+
| Numeric (`int`, `bigInt`, `float`) | `count`, `sum`, `avg`, `min`, `max`, `first`, `last` |
|
|
1132
|
+
| Date (`dateTime`) | `count`, `min`, `max`, `first`, `last` |
|
|
1133
|
+
|
|
1134
|
+
```json
|
|
1135
|
+
{
|
|
1136
|
+
"select": {
|
|
1137
|
+
"counter": {
|
|
1138
|
+
"count": true,
|
|
1139
|
+
"sum": true,
|
|
1140
|
+
"avg": true,
|
|
1141
|
+
"first": true,
|
|
1142
|
+
"last": true
|
|
1143
|
+
},
|
|
1144
|
+
"publishedAt": {
|
|
1145
|
+
"min": true,
|
|
1146
|
+
"max": true
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
"dateField": "createdAt",
|
|
1150
|
+
"from": "2026-01-01T00:00:00.000Z",
|
|
1151
|
+
"to": "2026-01-08T00:00:00.000Z"
|
|
1152
|
+
}
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
**`first` and `last`** take the value held by the earliest and the latest record of a period, ordered by the aggregated
|
|
1156
|
+
`dateField` (ties broken by `id`), or `null` for a period holding no record. The database cannot aggregate them, so each
|
|
1157
|
+
non-empty period requesting them costs up to two extra queries.
|
|
1158
|
+
|
|
1159
|
+
Any other field, an operator its field kind does not support, and an empty selection are rejected with a `400` error.
|
|
1160
|
+
Over HTTP the selection is also validated against a generated per-model `<Model>AggregateSelect` schema. The `dateField`
|
|
1161
|
+
must be a date field of the model (`createdAt` by default).
|
|
1162
|
+
|
|
1163
|
+
Selections are typed by `AggregateSelect<T>` from `@appweaver/common`, with a `<Model>Aggregate` alias emitted per
|
|
1164
|
+
model:
|
|
1165
|
+
|
|
1166
|
+
```ts
|
|
1167
|
+
import { PostAggregate } from '@/types/generated';
|
|
1168
|
+
|
|
1169
|
+
const select: PostAggregate = { counter: { sum: true }, createdAt: { max: true } };
|
|
1170
|
+
const stats = await postService.aggregate({}, select);
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
`aggregate` infers the response type from the selection it is given, so a selection passed as an object literal, or
|
|
1174
|
+
declared with `satisfies`, narrows the response to the fields it names, while one annotated as `<Model>Aggregate` keeps
|
|
1175
|
+
every aggregatable field of the model:
|
|
1176
|
+
|
|
1177
|
+
```ts
|
|
1178
|
+
const narrow = await postService.aggregate({}, { counter: { sum: true } });
|
|
1179
|
+
narrow.total.counter?.sum; // typed
|
|
1180
|
+
narrow.total.createdAt; // compile error, the field was not selected
|
|
1181
|
+
|
|
1182
|
+
const select = { counter: { sum: true } } satisfies PostAggregate; // narrows and checks against the model
|
|
1183
|
+
const wide: PostAggregate = { counter: { sum: true } }; // keeps the whole model in the response type
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
### Aggregate response
|
|
1187
|
+
|
|
1188
|
+
The response shape follows whatever was selected, and its type carries the fields of the selection (see
|
|
1189
|
+
[Aggregate selection](#aggregate-selection)). Each aggregated field holds one property per operator applied to it, and
|
|
1190
|
+
the operators the selection left out are `undefined`:
|
|
1191
|
+
|
|
1192
|
+
```ts
|
|
1193
|
+
const resp = {
|
|
1194
|
+
total: AggregateValue, // Overall aggregation
|
|
1195
|
+
items: Array<AggregateResult> // Per-period results
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// Each AggregateResult:
|
|
1199
|
+
const result = {
|
|
1200
|
+
date: 'Date',
|
|
1201
|
+
result: {
|
|
1202
|
+
[field]: {
|
|
1203
|
+
count: 123,
|
|
1204
|
+
min: 123, // an ISO date string for a date field
|
|
1205
|
+
max: 123, // an ISO date string for a date field
|
|
1206
|
+
avg: 123, // numeric fields only
|
|
1207
|
+
sum: 123, // numeric fields only
|
|
1208
|
+
first: 123, // value of the earliest record of the period
|
|
1209
|
+
last: 123 // value of the latest record of the period
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
### Text search example
|
|
1216
|
+
|
|
1217
|
+
Object form with placeholder:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
const config = {
|
|
1221
|
+
textSearch: {
|
|
1222
|
+
title: {
|
|
1223
|
+
contains: '{input}', mode:
|
|
1224
|
+
'insensitive'
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
Function form for complex queries:
|
|
1231
|
+
|
|
1232
|
+
```ts
|
|
1233
|
+
const config = {
|
|
1234
|
+
textSearch: (input) => ({
|
|
1235
|
+
OR: [
|
|
1236
|
+
{ title: { contains: input, mode: 'insensitive' } },
|
|
1237
|
+
{ description: { contains: input, mode: 'insensitive' } }
|
|
1238
|
+
]
|
|
1239
|
+
})
|
|
1240
|
+
};
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
---
|
|
1244
|
+
|
|
1245
|
+
## createRoutes
|
|
1246
|
+
|
|
1247
|
+
Creates CRUD route definitions for a resource. Routes are automatically registered with Fastify and derive their
|
|
1248
|
+
request/response schemas from the resource model.
|
|
1249
|
+
|
|
1250
|
+
```ts
|
|
1251
|
+
import { createRoutes } from '@appweaver/core';
|
|
1252
|
+
|
|
1253
|
+
export default createRoutes({
|
|
1254
|
+
modelName: 'Product',
|
|
1255
|
+
path: '/products',
|
|
1256
|
+
find: { roles: ['Admin', 'User'], rateLimit: { max: 100 } },
|
|
1257
|
+
query: { cache: true, cacheTTL: 5000 },
|
|
1258
|
+
create: { permissions: ['product:create'] },
|
|
1259
|
+
delete: { exclude: true }
|
|
1260
|
+
});
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
### Configuration
|
|
1264
|
+
|
|
1265
|
+
```ts
|
|
1266
|
+
function createRoutes(config: ResourceRoutesConfig, override ?: Partial<ResourceRoutesConfig>) {
|
|
1267
|
+
}
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
| Property | Type | Description |
|
|
1271
|
+
|--------------|-----------------|----------------------------------------------------------|
|
|
1272
|
+
| `modelName` | string | Model name to bind routes to (required). |
|
|
1273
|
+
| `path` | string | Custom base URL path (default: derived from model name). |
|
|
1274
|
+
| `find` | ReadRouteConfig | `GET /:id` - Find single resource by ID. |
|
|
1275
|
+
| `query` | ReadRouteConfig | `POST /query` - Query resources with filters. |
|
|
1276
|
+
| `aggregate` | ReadRouteConfig | `POST /aggregate` - Aggregate resources. |
|
|
1277
|
+
| `create` | RouteConfig | `POST /` - Create a new resource. |
|
|
1278
|
+
| `update` | RouteConfig | `PUT /:id` - Update a resource. |
|
|
1279
|
+
| `delete` | RouteConfig | `DELETE /:id` - Delete a resource. |
|
|
1280
|
+
| `export` | RouteConfig | `POST /export` - Export resources to CSV. |
|
|
1281
|
+
| `fileUpload` | RouteConfig | `POST /:id/files` - Upload files to a resource. |
|
|
1282
|
+
| `fileDelete` | RouteConfig | `POST /:id/delete-files` - Delete files from a resource. |
|
|
1283
|
+
|
|
1284
|
+
### Route config (all operations)
|
|
1285
|
+
|
|
1286
|
+
| Property | Type | Default | Description |
|
|
1287
|
+
|-------------------|--------------------------|---------|---------------------------------------------------------------|
|
|
1288
|
+
| `exclude` | boolean | `false` | Exclude this operation entirely. |
|
|
1289
|
+
| `public` | boolean | `false` | No authentication required. |
|
|
1290
|
+
| `roles` | string[] | - | Required roles (OR logic by default). |
|
|
1291
|
+
| `permissions` | string[] | - | Required permissions (OR logic by default). |
|
|
1292
|
+
| `auth` | AuthType[] | - | Allowed authentication types: `'jwt'`, `'apiKey'`, `'basic'`. |
|
|
1293
|
+
| `rateLimit` | RateLimitConfig \| false | - | Per-operation rate limiting. `false` disables. |
|
|
1294
|
+
| `recaptcha` | boolean | `false` | Require reCAPTCHA verification. |
|
|
1295
|
+
| `recaptchaAction` | string | - | Expected reCAPTCHA action name for score validation. |
|
|
1296
|
+
|
|
1297
|
+
### Read route config (find, query, aggregate)
|
|
1298
|
+
|
|
1299
|
+
Extends RouteConfig with caching options:
|
|
1300
|
+
|
|
1301
|
+
| Property | Type | Default | Description |
|
|
1302
|
+
|-------------------------|--------------------|---------|----------------------------------------------------------------|
|
|
1303
|
+
| `cache` | boolean | `false` | Enable response caching. |
|
|
1304
|
+
| `cacheKey` | string \| function | - | Custom cache key. Function signature: `(req, user) => string`. |
|
|
1305
|
+
| `cacheTTL` | number | - | Cache TTL in milliseconds (overrides global default). |
|
|
1306
|
+
| `cacheSkipInvalidation` | boolean | `false` | Skip automatic cache invalidation on writes. |
|
|
1307
|
+
|
|
1308
|
+
### Rate limit config
|
|
1309
|
+
|
|
1310
|
+
```ts
|
|
1311
|
+
const config = {
|
|
1312
|
+
rateLimit: {
|
|
1313
|
+
max: 100,
|
|
1314
|
+
timeWindow: 60000,
|
|
1315
|
+
allowList: ['127.0.0.1'],
|
|
1316
|
+
keyGenerator: (req) => req.ip
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
| Property | Type | Description |
|
|
1322
|
+
|----------------|------------------------------|---------------------------------------------------------------------|
|
|
1323
|
+
| `max` | number \| function | Maximum requests per time window. Function: `(req, key) => number`. |
|
|
1324
|
+
| `timeWindow` | number \| string \| function | Window duration in ms. Function: `(req, key) => number`. |
|
|
1325
|
+
| `allowList` | string[] \| function | IPs exempt from limiting. Function: `(req, key) => boolean`. |
|
|
1326
|
+
| `keyGenerator` | function | Custom key generator. Signature: `(req) => string \| number`. |
|
|
1327
|
+
|
|
1328
|
+
---
|
|
1329
|
+
|
|
1330
|
+
## createPolicy
|
|
1331
|
+
|
|
1332
|
+
Creates row-level security policies for a resource. The service layer evaluates the policy on every CRUD operation to
|
|
1333
|
+
enforce fine-grained authorization beyond static role/permission checks.
|
|
1334
|
+
|
|
1335
|
+
```ts
|
|
1336
|
+
import { createPolicy } from '@appweaver/core';
|
|
1337
|
+
|
|
1338
|
+
export default createPolicy({
|
|
1339
|
+
modelName: 'Product',
|
|
1340
|
+
checkAccess: (user, resource, action) => resource.status === 'Draft',
|
|
1341
|
+
readRestrictions: (user, resource, action) => ({
|
|
1342
|
+
enabled: true
|
|
1343
|
+
}),
|
|
1344
|
+
files: {
|
|
1345
|
+
photo: { accessType: 'public' }
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Configuration
|
|
1351
|
+
|
|
1352
|
+
```ts
|
|
1353
|
+
function createPolicy(config: ResourcePolicyConfig, override ?: Partial<ResourcePolicyConfig>) {
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
| Property | Type | Description |
|
|
1358
|
+
|---------------------|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
|
|
1359
|
+
| `modelName` | string | Model name to bind this policy to (required). |
|
|
1360
|
+
| `checkAccess` | `(user, resource, action) => boolean` | Dynamic access check against a resource instance. Return `true` to allow, `false` to deny. |
|
|
1361
|
+
| `readRestrictions` | `(user, resource, action) => filter` | Returns a Prisma filter object applied to all read queries (find, query, aggregate). Restricts which records are visible. |
|
|
1362
|
+
| `writeRestrictions` | `(user, resource, action) => data` | Returns data to merge or validate on create/update operations. |
|
|
1363
|
+
| `files` | Record\<string, FilePolicy> | Per-file field access policy. |
|
|
1364
|
+
|
|
1365
|
+
**Action types**: `'find'`, `'query'`, `'aggregate'`, `'create'`, `'update'`, `'delete'`
|
|
1366
|
+
|
|
1367
|
+
### File policy
|
|
1368
|
+
|
|
1369
|
+
| Property | Type | Default | Description |
|
|
1370
|
+
|--------------|--------------------------------------------|---------------|--------------------------------------------------------------------------------------------------|
|
|
1371
|
+
| `accessType` | `'public'` \| `'protected'` \| `'private'` | `'protected'` | File access level. `public` = anyone, `protected` = authenticated users, `private` = owner only. |
|
|
1372
|
+
| `canAccess` | `(user, resource, file) => boolean` | - | Custom access check for reading files. |
|
|
1373
|
+
| `canCreate` | `(user, resource, file) => boolean` | - | Custom access check for uploading files. |
|
|
1374
|
+
| `canDelete` | `(user, resource, file) => boolean` | - | Custom access check for deleting files. |
|
|
1375
|
+
|
|
1376
|
+
---
|
|
1377
|
+
|
|
1378
|
+
## registerRoute
|
|
1379
|
+
|
|
1380
|
+
Registers a custom Fastify route handler outside the resource system. Use this for endpoints that don't map to a
|
|
1381
|
+
standard CRUD resource.
|
|
1382
|
+
|
|
1383
|
+
```ts
|
|
1384
|
+
import { registerRoute, Router } from '@appweaver/core';
|
|
1385
|
+
import { Type } from '@sinclair/typebox';
|
|
1386
|
+
|
|
1387
|
+
registerRoute(
|
|
1388
|
+
async function (router: Router) {
|
|
1389
|
+
router.get('/search-result', {
|
|
1390
|
+
schema: {
|
|
1391
|
+
summary: 'Sample search result response route',
|
|
1392
|
+
response: { 200: Type.Ref('SearchResult') }
|
|
1393
|
+
},
|
|
1394
|
+
handler: async () => {
|
|
1395
|
+
return { message: 'Hello, world!' };
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
},
|
|
1399
|
+
{ public: true, cacheTTL: 15000 }
|
|
1400
|
+
);
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
### Config options
|
|
1404
|
+
|
|
1405
|
+
| Property | Type | Description |
|
|
1406
|
+
|-------------------------|--------------------------|---------------------------------------------|
|
|
1407
|
+
| `exclude` | boolean | Skip registration of this route. |
|
|
1408
|
+
| `public` | boolean | No authentication required. |
|
|
1409
|
+
| `roles` | string[] | Required roles. |
|
|
1410
|
+
| `permissions` | string[] | Required permissions. |
|
|
1411
|
+
| `auth` | AuthType[] | Allowed authentication types. |
|
|
1412
|
+
| `rateLimit` | RateLimitConfig \| false | Rate limiting configuration. |
|
|
1413
|
+
| `recaptcha` | boolean | Require reCAPTCHA verification. |
|
|
1414
|
+
| `recaptchaAction` | string | Expected reCAPTCHA action. |
|
|
1415
|
+
| `cache` | boolean | Enable response caching. |
|
|
1416
|
+
| `cacheKey` | string \| function | Custom cache key. |
|
|
1417
|
+
| `cacheTTL` | number | Cache TTL in milliseconds. |
|
|
1418
|
+
| `cacheSkipInvalidation` | boolean | Skip automatic cache invalidation. |
|
|
1419
|
+
| `cacheModelName` | string | Model name for cache invalidation tracking. |
|
|
1420
|
+
| `cacheRelations` | string[] | Related model names for cache invalidation. |
|
|
1421
|
+
|
|
1422
|
+
---
|
|
1423
|
+
|
|
1424
|
+
## registerModel
|
|
1425
|
+
|
|
1426
|
+
Registers a custom TypeBox schema as a named model in the schema registry. Registered models can be referenced using
|
|
1427
|
+
`Type.Ref('ModelName')` in route schemas.
|
|
1428
|
+
|
|
1429
|
+
```ts
|
|
1430
|
+
import { registerModel } from '@appweaver/core';
|
|
1431
|
+
import { Nullable } from '@appweaver/common';
|
|
1432
|
+
import { Type } from '@sinclair/typebox';
|
|
1433
|
+
|
|
1434
|
+
registerModel(
|
|
1435
|
+
Type.Object(
|
|
1436
|
+
{
|
|
1437
|
+
id: Type.Integer(),
|
|
1438
|
+
title: Type.String({ example: 'My Title' }),
|
|
1439
|
+
description: Nullable(Type.String({ maxLength: 512 })),
|
|
1440
|
+
score: Type.Number({ minimum: 0, maximum: 1 })
|
|
1441
|
+
},
|
|
1442
|
+
{ $id: 'SearchResult' } // The prefered way for naming the model
|
|
1443
|
+
),
|
|
1444
|
+
'SearchResult' // Model name can be overriden as a second optional argument
|
|
1445
|
+
);
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
| Parameter | Type | Description |
|
|
1449
|
+
|-----------|---------|--------------------------------------------------------------|
|
|
1450
|
+
| `schema` | TObject | TypeBox object schema definition. |
|
|
1451
|
+
| `name` | string? | Override schema name identifier for `Type.Ref()` references. |
|
|
1452
|
+
|
|
1453
|
+
---
|
|
1454
|
+
|
|
1455
|
+
## registerPlugin
|
|
1456
|
+
|
|
1457
|
+
Registers a custom Fastify plugin. Plugins are wrapped with `fastify-plugin` so their decorators and hooks are scoped to
|
|
1458
|
+
the entire server instance.
|
|
1459
|
+
|
|
1460
|
+
```ts
|
|
1461
|
+
import { registerPlugin } from '@appweaver/core';
|
|
1462
|
+
|
|
1463
|
+
registerPlugin(
|
|
1464
|
+
'audit-log',
|
|
1465
|
+
async (server) => {
|
|
1466
|
+
server.addHook('onResponse', async (request, reply) => {
|
|
1467
|
+
logger.info(`${request.method} ${request.url} -> ${reply.statusCode}`);
|
|
1468
|
+
});
|
|
1469
|
+
},
|
|
1470
|
+
['other-plugin'] // optional dependencies
|
|
1471
|
+
);
|
|
1472
|
+
```
|
|
1473
|
+
|
|
1474
|
+
| Parameter | Type | Description |
|
|
1475
|
+
|----------------|-------------------------------------|-------------------------------------------------------|
|
|
1476
|
+
| `name` | string | Plugin name (used for dependency resolution). |
|
|
1477
|
+
| `plugin` | `(server) => void \| Promise<void>` | Fastify plugin function. |
|
|
1478
|
+
| `dependencies` | string[] | Optional list of plugin names this plugin depends on. |
|