@appweaver/create-weaver-app 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,10 +46,13 @@ weaver openapi|oa [options]
46
46
 
47
47
  Generate application OpenAPI specification schema.
48
48
 
49
- | Option | Description | Default |
50
- |---------------------------|------------------------------------------------------------------|-----------------|
51
- | `-o, --outputPath [path]` | Output path for generated OpenAPI specification | `./schema.json` |
52
- | `-f, --format [format]` | Output format for generated OpenAPI specification (json or yaml) | `json` |
49
+ | Option | Description | Default |
50
+ |---------------------------|------------------------------------------------------------------|------------------|
51
+ | `-o, --outputPath [path]` | Output path for generated OpenAPI specification | `./openapi.json` |
52
+ | `-f, --format [format]` | Output format for generated OpenAPI specification (json or yaml) | `json` |
53
+
54
+ When `--format yaml` is used and `--outputPath` is left at its default, the output path becomes `./openapi.yaml`.
55
+ Missing output directories are created automatically.
53
56
 
54
57
  ---
55
58
 
@@ -80,9 +83,12 @@ Per model, the type file holds `<Model>`, `<Model>Single`, `<Model>Multiple`, `<
80
83
  ## `weaver migrate`
81
84
 
82
85
  ```
83
- weaver migrate|mge [options]
86
+ weaver migrate|mge
84
87
  ```
85
88
 
89
+ Apply all pending database migrations (`prisma migrate deploy`). Takes no options, creates nothing, and never
90
+ prompts, which makes it the command to run in CI, in containers, and in production.
91
+
86
92
  ---
87
93
 
88
94
  ## `weaver migration`
@@ -102,10 +108,15 @@ Database migration commands.
102
108
 
103
109
  ### `weaver migration reset`
104
110
 
105
- | Option | Description | Default |
106
- |---------------|----------------------------------------------|---------|
107
- | `-f, --force` | Force reset for non-development environments | `false` |
108
- | `-y, --yes` | Skip confirmation prompt | `false` |
111
+ Drops the database, recreates it, and re-applies every migration (`prisma migrate reset`). **All data is lost.**
112
+
113
+ | Option | Description | Default |
114
+ |---------------|-----------------------------------------------------------|---------|
115
+ | `-f, --force` | Allow the reset outside the `dev` and `test` environments | `false` |
116
+ | `-y, --yes` | Skip confirmation prompt | `false` |
117
+
118
+ Without `--force` the command runs only when `NODE_ENV` resolves to `dev` or `test`, and aborts otherwise. Either
119
+ `--force` or `--yes` skips the Prisma confirmation prompt.
109
120
 
110
121
  ---
111
122
 
@@ -117,13 +128,13 @@ weaver seed|sd [options]
117
128
 
118
129
  Seed the database.
119
130
 
120
- | Option | Description | Default |
121
- |-------------------------|---------------------------------------------------------------------|-----------------------|
122
- | `--seedersPath [path]` | Seeders directory path | `config.SEEDERS_PATH` |
123
- | `-b, --buildProject` | Build the project before seeding | `false` |
124
- | `-p, --project` | TypeScript project build config file (used when `-b` is set) | `tsconfig.build.json` |
125
- | `-c, --continueOnError` | Continue seeder execution if error is thrown | `false` |
126
- | `-f, --fixWarnings` | Fix all seeder warnings like wrong checksum or deleted seeder files | `false` |
131
+ | Option | Description | Default |
132
+ |-------------------------|---------------------------------------------------------------------|------------------------------------|
133
+ | `--seedersPath [path]` | Seeders directory path | `config.DATABASE_SEEDERS_DIR_PATH` |
134
+ | `-b, --buildProject` | Build the project before seeding | `false` |
135
+ | `-p, --project` | TypeScript project build config file (used when `-b` is set) | `tsconfig.build.json` |
136
+ | `-c, --continueOnError` | Continue seeder execution if error is thrown | `false` |
137
+ | `-f, --fixWarnings` | Fix all seeder warnings like wrong checksum or deleted seeder files | `false` |
127
138
 
128
139
  ---
129
140
 
@@ -135,10 +146,10 @@ weaver start|s [options]
135
146
 
136
147
  Start the application.
137
148
 
138
- | Option | Description | Default |
139
- |-----------------|-------------------------------------------------------------|-----------------|
140
- | `-p, --project` | TypeScript project config file | `tsconfig.json` |
141
- | `-w, --watch` | Run in watch mode (recompiles and restarts on file changes) | `false` |
149
+ | Option | Description | Default |
150
+ |-----------------|-------------------------------------------------------------|-----------------------|
151
+ | `-p, --project` | TypeScript project config file | `tsconfig.build.json` |
152
+ | `-w, --watch` | Run in watch mode (recompiles and restarts on file changes) | `false` |
142
153
 
143
154
  ---
144
155
 
@@ -171,6 +182,9 @@ Requires `NODE_ENV=test`.
171
182
  | `--migrationName [name]` | Name for the initial migration | `init_test` |
172
183
  | `--verbose` | Print verbose output | `false` |
173
184
 
185
+ Aborts unless the storage, schema and client paths all resolve inside `--dir`, so a misconfigured test run cannot
186
+ touch the development database or uploads.
187
+
174
188
  ### `weaver test reset`
175
189
 
176
190
  Requires `NODE_ENV=test`. With no flags, resets both database and storage.
@@ -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 orphaned records when parent is deleted. |
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 | 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). |
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, indexed on `[provider, providerAccountId]`. A
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 belonging to file fields configured with `onResourceDeleted: 'delete'` can be
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
- To enable automatic file cleanup, set `onResourceDeleted: 'delete'` on the file field in your model config:
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 kept
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