@appweaver/create-weaver-app 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skill/SKILL.md +764 -753
- package/skill/references/resources.md +120 -11
- package/skill/references/security.md +3 -2
- package/skill/references/storage.md +29 -6
|
@@ -51,6 +51,8 @@ function createModel(config: ResourceModelConfig, override ?: Partial<ResourceMo
|
|
|
51
51
|
| `update` | OperationConfig | no | - | Pick/omit fields for the update DTO. |
|
|
52
52
|
| `export` | Record\<string, ExportField> | no | - | CSV export field configuration. |
|
|
53
53
|
| `index` | string[] \| string[][] | no | - | Database index definitions (`-field` desc, `+field` asc). |
|
|
54
|
+
| `unique` | string[] \| string[][] | no | - | Composite unique constraints, in the same shape as `index`. |
|
|
55
|
+
| `softDelete` | boolean | no | `false` | Mark deleted records instead of removing them. |
|
|
54
56
|
|
|
55
57
|
### ID field
|
|
56
58
|
|
|
@@ -145,6 +147,76 @@ const config = {
|
|
|
145
147
|
| `updatedAt` | boolean | `true` | Add `updatedAt` timestamp field. |
|
|
146
148
|
| `createdById` | boolean | `true` | Add `createdById` foreign key to the auth user. |
|
|
147
149
|
|
|
150
|
+
### Soft delete
|
|
151
|
+
|
|
152
|
+
A model with `softDelete` enabled keeps its deleted records in the database instead of removing them. The delete marks
|
|
153
|
+
the record with two extra columns. They are hidden fields: typed on the full generated model type (i.e. `Post`), so
|
|
154
|
+
server code can read them, but never part of any API request or response, nor of the OpenAPI document:
|
|
155
|
+
|
|
156
|
+
| Column | Type | Description |
|
|
157
|
+
|---------------|---------------------|------------------------------------------------------------------------|
|
|
158
|
+
| `deletedAt` | `DateTime?` | When the record was deleted, `null` for a live record. |
|
|
159
|
+
| `deletedById` | auth model id (`?`) | The user who deleted the record. Only added when an auth model exists. |
|
|
160
|
+
|
|
161
|
+
`deletedById` is indexed like any other foreign key, but `deletedAt` is not. Every read filters on `deletedAt IS NULL`,
|
|
162
|
+
so add the column to the indexes matching the queries of the model where it helps, i.e.
|
|
163
|
+
`index: [['deletedAt', '-createdAt', 'id']]` for the default listing.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const config = {
|
|
167
|
+
softDelete: true
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The stored files of a soft deleted record are **kept** by default, so a manual restore loses nothing, while a record
|
|
172
|
+
removed from the database loses them by default. Each file field decides for itself with `onResourceSoftDeleted` and
|
|
173
|
+
`onResourceDeleted` (see [File fields](#file-fields)). A kept file is never served again, see
|
|
174
|
+
[Files of deleted resources](./storage.md#deleting-all-files-on-resource-deletion).
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const config = {
|
|
178
|
+
softDelete: true,
|
|
179
|
+
files: {
|
|
180
|
+
avatar: {}, // kept on soft delete, removed on a database delete
|
|
181
|
+
idScan: { onResourceSoftDeleted: 'delete' } // removed on both
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Soft deleted records are hidden from every read, so for API users a soft delete is indistinguishable from a real one:
|
|
187
|
+
|
|
188
|
+
- `find`, `update` and `delete` respond with 404, `query`, `aggregate` and `export` leave the record out.
|
|
189
|
+
- List relations and relation counts skip it, a single relation pointing at it reads as `null`.
|
|
190
|
+
- Relation filters never match through it: `_some`, `_none`, `_every` and `_exists` only consider live records, and a
|
|
191
|
+
`null` filter on a single relation matches a deleted related record.
|
|
192
|
+
- Connecting or inline updating it through a relation input is rejected with 400.
|
|
193
|
+
- Its files are served as missing, whether they were kept or removed.
|
|
194
|
+
|
|
195
|
+
Restoring a record, or reading the deleted ones, is not part of the API: set `deletedAt` and `deletedById` back to
|
|
196
|
+
`null` in the database, on the record, on the records soft deleted with it and on the `File` rows of its kept files,
|
|
197
|
+
which all share the same `deletedAt` value. Unique values of a deleted record stay taken until the row is removed.
|
|
198
|
+
|
|
199
|
+
The delete follows the `onDelete` action of every relation referencing the record, mirroring a database delete:
|
|
200
|
+
|
|
201
|
+
| `onDelete` of the referencing relation | On soft delete |
|
|
202
|
+
|-----------------------------------------------|-------------------------------------------------------------------------------------|
|
|
203
|
+
| `cascade` | The referencing records are soft deleted with the same `deletedAt`, level by level. |
|
|
204
|
+
| `restrict`, `noAction` (default if required) | The delete fails with 409 while a live record references it. |
|
|
205
|
+
| `setNull`, `setDefault` (default if optional) | The reference is kept, so a manual restore brings the link back. |
|
|
206
|
+
|
|
207
|
+
A soft delete never runs the database cascade, so every model a soft deleted model cascades into must enable
|
|
208
|
+
`softDelete` too. The application refuses to start and `weaver generate` fails otherwise:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
Model 'Comment' must enable 'softDelete', since its relation 'Comment.post' cascades on delete from the soft deleted model 'Post'.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The framework owned `ApiKey` and `ConnectedAccount` models cascade from the auth model, so they enable `softDelete`
|
|
215
|
+
automatically when the auth model does.
|
|
216
|
+
|
|
217
|
+
After enabling `softDelete` on a model, regenerate the schema and create a migration (`weaver generate`, then
|
|
218
|
+
`weaver migration new <name>`).
|
|
219
|
+
|
|
148
220
|
### Scalar field types
|
|
149
221
|
|
|
150
222
|
All scalar fields share these common properties:
|
|
@@ -362,9 +434,10 @@ const config = {
|
|
|
362
434
|
| `mappedBy` | string | - | Name of the inverse relation on the target model. |
|
|
363
435
|
| `required` | boolean | `true` | Whether the relation is required (nullable foreign key if not required). |
|
|
364
436
|
| `minItems` | number | - | Minimum items for list relations. |
|
|
365
|
-
| `orphanRemoval` | boolean | `false` | Delete
|
|
437
|
+
| `orphanRemoval` | boolean | `false` | Delete the related records an update removes from the relation. |
|
|
366
438
|
| `onDelete` | ReferentialAction | - | Foreign key action on delete. |
|
|
367
439
|
| `onUpdate` | ReferentialAction | - | Foreign key action on update. |
|
|
440
|
+
| `index` | boolean | `true` | Index the foreign key column; `false` leaves it unindexed. |
|
|
368
441
|
| `input` | RelationInput | - | Input DTO configuration. |
|
|
369
442
|
| `output` | RelationOutput | - | Output DTO configuration. |
|
|
370
443
|
|
|
@@ -375,6 +448,15 @@ record fails with a foreign key violation while any child row still exists. Set
|
|
|
375
448
|
rows are owned by the parent and meaningless without it. Optional owning relations (`required: false`) fall back to
|
|
376
449
|
`setNull`, which already lets the referenced record be deleted.
|
|
377
450
|
|
|
451
|
+
The stored files of the records a `cascade` removes are cleaned up together with the files of the deleted record. A
|
|
452
|
+
model with soft delete applies the same actions itself, see its section.
|
|
453
|
+
|
|
454
|
+
With `orphanRemoval: true`, an update deletes the related records it removes from the relation instead of only
|
|
455
|
+
disconnecting them. The orphans are deleted like any other record: their stored files are removed following
|
|
456
|
+
`onResourceDeleted` and the records cascading from them are removed too. Orphans of a model with `softDelete` enabled
|
|
457
|
+
are soft deleted instead, keep their
|
|
458
|
+
references for a manual restore, and keep their files unless a field sets `onResourceSoftDeleted: 'delete'`.
|
|
459
|
+
|
|
378
460
|
#### Relationship types
|
|
379
461
|
|
|
380
462
|
The `type` property declares the relation cardinality explicitly, and `owner` marks the side that holds the foreign key
|
|
@@ -610,16 +692,17 @@ const config = {
|
|
|
610
692
|
};
|
|
611
693
|
```
|
|
612
694
|
|
|
613
|
-
| Property
|
|
614
|
-
|
|
615
|
-
| `mimeType`
|
|
616
|
-
| `namePattern`
|
|
617
|
-
| `array`
|
|
618
|
-
| `maxSize`
|
|
619
|
-
| `maxCount`
|
|
620
|
-
| `output`
|
|
621
|
-
| `onResourceDeleted`
|
|
622
|
-
| `
|
|
695
|
+
| Property | Type | Description |
|
|
696
|
+
|-------------------------|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
697
|
+
| `mimeType` | string \| RegExp | Allowed MIME types (glob patterns like `'image/*'` supported). |
|
|
698
|
+
| `namePattern` | string \| function | File naming pattern or function (available variables are listed below). |
|
|
699
|
+
| `array` | boolean | Allow multiple files. |
|
|
700
|
+
| `maxSize` | number \| string | Maximum file size (e.g. `'2 MB'`, `5242880`). |
|
|
701
|
+
| `maxCount` | number | Maximum number of files (for array fields). |
|
|
702
|
+
| `output` | RelationOutput | When to include file info in output, and its count. Takes no `include` or `maxDepth`. |
|
|
703
|
+
| `onResourceDeleted` | `'delete'` \| `'keep'` | When the owning resource is removed from the database, also by a cascade or an orphan removal. `'delete'` (default) removes files from storage, `'keep'` retains them without serving them. |
|
|
704
|
+
| `onResourceSoftDeleted` | `'delete'` \| `'keep'` | When the owning resource is soft deleted (see `softDelete`). `'keep'` (default) retains files for a manual restore without serving them, `'delete'` removes them from storage. |
|
|
705
|
+
| `image` | ImageConfig | Image compression and resize settings. Only applies to image MIME types (excluding GIF). |
|
|
623
706
|
|
|
624
707
|
#### Available namePattern variables
|
|
625
708
|
|
|
@@ -820,6 +903,32 @@ index: [['status', '-createdAt']] // @@index([status, createdAt(sort:
|
|
|
820
903
|
|
|
821
904
|
The prefix is part of the index identity, so `['createdAt', '-createdAt']` emits two separate indexes.
|
|
822
905
|
|
|
906
|
+
Every foreign key column (relation `<name>Id` columns, `createdById` and `deletedById`) is indexed automatically, since
|
|
907
|
+
PostgreSQL and SQLite do not index them on their own. A foreign key is skipped when it is already unique (`oneToOne`
|
|
908
|
+
relations and file fields) or when an explicit `index` or `unique` entry leads with it, so
|
|
909
|
+
`index: [['categoryId', '-publishedAt']]` replaces the automatic `@@index([categoryId])`. Set `index: false` on a
|
|
910
|
+
relation to leave its foreign key unindexed, i.e. for a rarely queried relation of a write-heavy model:
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
relations: {
|
|
914
|
+
author: { model: 'User', type: 'oneToMany', owner: true, index: false }
|
|
915
|
+
}
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
The audit columns `createdById` and `deletedById` are always indexed.
|
|
919
|
+
|
|
920
|
+
### Unique config
|
|
921
|
+
|
|
922
|
+
`unique` takes the same shape as `index` and emits `@@unique` constraints. Use it for a combination of columns that must
|
|
923
|
+
be unique together; a single column is better marked with the scalar's own `unique: true`:
|
|
924
|
+
|
|
925
|
+
```ts
|
|
926
|
+
unique: [['provider', 'providerAccountId']] // @@unique([provider, providerAccountId])
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
A create or update breaking the constraint fails in the database, so handle the conflict where concurrent writes can
|
|
930
|
+
race to insert the same combination.
|
|
931
|
+
|
|
823
932
|
### Generated models
|
|
824
933
|
|
|
825
934
|
`createModel` produces the following TypeBox schema models used internally by routes and services:
|
|
@@ -280,8 +280,9 @@ exchange succeeds.
|
|
|
280
280
|
### Connected accounts
|
|
281
281
|
|
|
282
282
|
`ConnectedAccount` pairs a provider account with a local user: `provider`, `providerAccountId`, `scope`, `lastLoginAt`
|
|
283
|
-
(`createdAt` holds the link date) and a relation to the auth model,
|
|
284
|
-
provider account belongs to one user only — relinking it elsewhere fails with a 403
|
|
283
|
+
(`createdAt` holds the link date) and a relation to the auth model, unique on `[provider, providerAccountId]`. A
|
|
284
|
+
provider account belongs to one user only — relinking it elsewhere fails with a 403, also when two first sign-ins race
|
|
285
|
+
to create the link.
|
|
285
286
|
|
|
286
287
|
The table exists when any OAuth2 provider is enabled; `SECURITY_OAUTH2_CONNECTED_ACCOUNTS_KEEP_DATABASE_TABLE=true`
|
|
287
288
|
keeps it after disabling OAuth2, like `SECURITY_API_KEY_KEEP_DATABASE_TABLE` does for API keys.
|
|
@@ -328,11 +328,30 @@ await fileService.deleteFile(
|
|
|
328
328
|
|
|
329
329
|
### Deleting all files on resource deletion
|
|
330
330
|
|
|
331
|
-
When a resource is deleted, files
|
|
332
|
-
automatically cleaned up. This is handled by `deleteResourceFiles()`, which is called automatically by the framework's
|
|
333
|
-
delete route handler.
|
|
331
|
+
When a resource is deleted, its stored files are cleaned up automatically, as configured per file field:
|
|
334
332
|
|
|
335
|
-
|
|
333
|
+
| Option | Applies to | Default |
|
|
334
|
+
|-------------------------|----------------------------------------------|------------|
|
|
335
|
+
| `onResourceDeleted` | A resource removed from the database | `'delete'` |
|
|
336
|
+
| `onResourceSoftDeleted` | A soft deleted resource (model `softDelete`) | `'keep'` |
|
|
337
|
+
|
|
338
|
+
Both options behave the same way:
|
|
339
|
+
|
|
340
|
+
- `'delete'` removes the file from the storage and its `File` row from the database.
|
|
341
|
+
- `'keep'` retains the file in the storage and its `File` row in the database, e.g. for audit, but never serves it
|
|
342
|
+
again: the row is soft deleted with the same `deletedAt` and `deletedById` values as its resource, and downloads
|
|
343
|
+
respond with 404 whatever the file access type is.
|
|
344
|
+
|
|
345
|
+
The resource service `delete` method removes them once the delete commits, whether it is called by the delete route or
|
|
346
|
+
directly from code, together with the files of every record deleted with it through an `onDelete: 'cascade'` relation.
|
|
347
|
+
The same happens to the orphans an `update` deletes through a relation with `orphanRemoval: true`.
|
|
348
|
+
|
|
349
|
+
The kept files are marked in the same transaction as the delete, while the removed ones leave the storage once it
|
|
350
|
+
commits. A model with `softDelete` enabled keeps the files of its deleted records by default, so a manual restore loses
|
|
351
|
+
nothing; set `onResourceSoftDeleted: 'delete'` on a field to remove its files on a soft delete too.
|
|
352
|
+
|
|
353
|
+
The framework never removes kept files by itself. Purging them after a retention period is up to the application, e.g.
|
|
354
|
+
with a scheduled job removing the `File` rows with an old `deletedAt` and their stored files.
|
|
336
355
|
|
|
337
356
|
```ts
|
|
338
357
|
const config = {
|
|
@@ -343,16 +362,20 @@ const config = {
|
|
|
343
362
|
},
|
|
344
363
|
documents: {
|
|
345
364
|
array: true,
|
|
346
|
-
onResourceDeleted: 'keep' // opt out: files are
|
|
365
|
+
onResourceDeleted: 'keep' // opt out: files are retained, but no longer served
|
|
366
|
+
},
|
|
367
|
+
idScan: {
|
|
368
|
+
onResourceSoftDeleted: 'delete' // also removed when the resource is soft deleted
|
|
347
369
|
}
|
|
348
370
|
}
|
|
349
371
|
};
|
|
350
372
|
```
|
|
351
373
|
|
|
352
|
-
You can also call `deleteResourceFiles` manually if needed:
|
|
374
|
+
You can also call `deleteResourceFiles` manually if needed, or `deleteResourcesFiles` for many records of one model:
|
|
353
375
|
|
|
354
376
|
```ts
|
|
355
377
|
await fileService.deleteResourceFiles('Post', postId);
|
|
378
|
+
await fileService.deleteResourcesFiles('Post', [firstId, secondId]);
|
|
356
379
|
```
|
|
357
380
|
|
|
358
381
|
### Owning resource reference
|