remult 0.25.8-exp.9 → 0.26.0-exp.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +188 -117
  2. package/esm/graphql.js +129 -57
  3. package/esm/index.js +1 -1
  4. package/esm/migrations/compare-migration-snapshots.js +26 -4
  5. package/esm/migrations/generate-migrations.js +13 -8
  6. package/esm/migrations/migrate.js +47 -33
  7. package/esm/migrations/migration-types.js +27 -3
  8. package/esm/migrations/update-migrations-ts-file.js +5 -2
  9. package/esm/remult-better-sqlite3.js +47 -0
  10. package/esm/remult-bun-sqlite.js +41 -0
  11. package/esm/remult-knex/index.js +66 -25
  12. package/esm/remult-sql-js.js +3 -83
  13. package/esm/remult-sqlite-core.js +104 -0
  14. package/esm/server/expressBridge.js +1 -1
  15. package/esm/server/initAsyncHooks.js +0 -1
  16. package/esm/src/data-api.js +20 -10
  17. package/esm/src/data-providers/sql-database.js +18 -4
  18. package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +33 -11
  19. package/esm/src/filter/filter-interfaces.js +198 -17
  20. package/esm/src/remult3/RepositoryImplementation.js +35 -14
  21. package/esm/src/remult3/remult3.js +18 -6
  22. package/esm/src/server-action.js +2 -2
  23. package/graphql.js +154 -67
  24. package/index.d.ts +4 -4
  25. package/index.js +2 -1
  26. package/migrations/compare-migration-snapshots.d.ts +3 -2
  27. package/migrations/compare-migration-snapshots.js +107 -42
  28. package/migrations/generate-migrations.d.ts +5 -5
  29. package/migrations/generate-migrations.js +17 -13
  30. package/migrations/migrate.d.ts +12 -3
  31. package/migrations/migrate.js +104 -98
  32. package/migrations/migration-types.d.ts +7 -2
  33. package/migrations/migration-types.js +68 -7
  34. package/migrations/update-migrations-ts-file.js +54 -48
  35. package/package.json +16 -1
  36. package/remult-better-sqlite3.d.ts +12 -0
  37. package/remult-better-sqlite3.js +64 -0
  38. package/remult-bun-sqlite.d.ts +11 -0
  39. package/remult-bun-sqlite.js +56 -0
  40. package/remult-knex/index.d.ts +2 -2
  41. package/remult-knex/index.js +91 -42
  42. package/remult-sql-js.d.ts +2 -17
  43. package/remult-sql-js.js +11 -166
  44. package/remult-sqlite-core.d.ts +21 -0
  45. package/remult-sqlite-core.js +217 -0
  46. package/server/expressBridge.js +1 -1
  47. package/server/initAsyncHooks.js +0 -1
  48. package/src/data-api.d.ts +2 -2
  49. package/src/data-api.js +87 -78
  50. package/src/data-providers/sql-database.d.ts +13 -2
  51. package/src/data-providers/sql-database.js +19 -3
  52. package/src/entity.d.ts +64 -6
  53. package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +30 -7
  54. package/src/filter/filter-consumer-bridge-to-sql-request.js +40 -20
  55. package/src/filter/filter-interfaces.d.ts +106 -39
  56. package/src/filter/filter-interfaces.js +248 -20
  57. package/src/remult3/RepositoryImplementation.d.ts +3 -3
  58. package/src/remult3/RepositoryImplementation.js +43 -18
  59. package/src/remult3/remult3.d.ts +1 -1
  60. package/src/remult3/remult3.js +18 -6
  61. package/src/server-action.d.ts +1 -1
  62. package/src/server-action.js +3 -2
  63. package/src/sql-command.d.ts +1 -1
package/README.md CHANGED
@@ -1,7 +1,4 @@
1
1
  <div align="center">
2
- <a href="http://remult.dev/">
3
- <img src="https://github.com/remult/remult/raw/master/docs/public/logo.png" width="140" height="140">
4
- </a>
5
2
  <h1>Remult</h1>
6
3
  <p>Full-stack CRUD, simplified, with SSOT TypeScript entities</p>
7
4
  <a href="https://circleci.com/gh/remult/remult/tree/master" rel="nofollow">
@@ -29,67 +26,44 @@
29
26
  <hr/>
30
27
  <br/>
31
28
 
32
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
33
- <p align="center">
34
- <img src="https://github.com/remult/remult/raw/master/static/images/video-thumbnail.jpg" alt="Video thumbnail" />
35
- </p>
36
- </a>
37
-
38
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
39
- <p align="center">Watch code demo on YouTube here (14 mins)</p>
40
- </a>
41
-
42
29
  ## What is Remult?
43
30
 
44
- **Remult** is a full-stack CRUD framework that uses your **TypeScript entities
45
- as a single source of truth for your API, frontend type-safe API client and
46
- backend ORM**.
31
+ Remult uses **TypeScript entities** as a single source of truth for: ✅ CRUD + Realtime API, ✅ frontend type-safe API client, and
32
+ ✅ backend ORM.
47
33
 
48
- - :zap: Zero-boilerplate CRUD API routes with paging, sorting, and filtering for Express / Fastify / Next.js / NestJS / Koa / others...
49
- - :ok_hand: Fullstack type-safety for API queries, mutations and RPC, without code generation
50
- - :sparkles: Input validation, defined once, runs both on the backend and on the frontend for best UX
51
- - :lock: Fine-grained code-based API authorization
34
+ - :zap: **Zero-boilerplate CRUD + Realtime API** with paging, sorting, and filtering
35
+ - :ok_hand: **Fullstack type-safety** for API queries, mutations and RPC, without code generation
36
+ - :sparkles: Input validation, **defined once**, runs both on the backend and on the frontend for best UX
37
+ - :lock: Fine-grained **code-based API authorization**
52
38
  - :relieved: Incrementally adoptable
53
- - :rocket: Production ready
54
- - :mega: **NEW - Zero-boilerplate realtime live-queries**
55
-
56
- ### Status
57
39
 
58
- Remult is **production-ready** and, in fact, used in production apps since 2018.
59
- However, we’re keeping the major version at zero so we can use community
60
- feedback to finalize the v1 API.
40
+ **Remult supports all major databases**, including: PostgreSQL, MySQL, SQLite, MongoDB, MSSQL and Oracle.
61
41
 
62
- ## Motivation
63
-
64
- Full-stack web development is (still) too complicated. **Simple CRUD, a common
65
- requirement of any business application, should be simple to build, maintain,
66
- and extend** when the need arises.
67
-
68
- Remult abstracts away repetitive, boilerplate, error-prone, and poorly designed
69
- code on the one hand, and enables total flexibility and control on the other.
70
- **Remult helps building fullstack apps using only TypeScript code you can easily
71
- follow and safely refactor**, and fits nicely into any existing or new project
72
- by being minimalistic and completely unopinionated regarding the developer’s
73
- choice of other frameworks and tools.
74
-
75
- Other frameworks tend to fall into either too much abstraction (no-code,
76
- low-code, BaaS) or partial abstraction (MVC frameworks, GraphQL, ORMs, API
77
- generators, code generators), and tend to be opinionated regarding the
78
- development tool-chain, deployment environment, configuration/conventions or
79
- DSL. Remult attempts to strike a better balance.
42
+ **Remult is frontend and backend framework agnostic** and comes with adapters for Express, Fastify, Next.js, Nuxt, SvelteKit, Nest, Koa, Hapi and Hono.
80
43
 
81
- ## Installation
44
+ Remult promotes a **consistent query syntax for both frontend and Backend code**:
82
45
 
83
- The _remult_ package is one and the same for both the frontend bundle and the
84
- backend. Install it once for a monolith project or per-repo in a monorepo.
46
+ ```ts
47
+ // Frontend - GET: /api/products?_limit=10&unitPrice.gt=5,_sort=name
48
+ // Backend - 'select name, unitPrice from products where unitPrice > 5 order by name limit 10'
49
+ await repo(Product).find({
50
+ limit: 10,
51
+ orderBy: {
52
+ name: 'asc',
53
+ },
54
+ where: {
55
+ unitPrice: { $gt: 5 },
56
+ },
57
+ })
85
58
 
86
- ```sh
87
- npm i remult
59
+ // Frontend - PUT: '/api/products/product7' (body: { "unitPrice" : 7 })
60
+ // Backend - 'update products set unitPrice = 7 where id = product7'
61
+ await repo(Product).update('product7', { unitPrice: 7 })
88
62
  ```
89
63
 
90
64
  ## Usage
91
65
 
92
- ### Define model classes
66
+ ### Define schema in code
93
67
 
94
68
  ```ts
95
69
  // shared/product.ts
@@ -100,6 +74,9 @@ import { Entity, Fields } from 'remult'
100
74
  allowApiCrud: true,
101
75
  })
102
76
  export class Product {
77
+ @Fields.cuid()
78
+ id = ''
79
+
103
80
  @Fields.string()
104
81
  name = ''
105
82
 
@@ -108,65 +85,72 @@ export class Product {
108
85
  }
109
86
  ```
110
87
 
111
- ### Setup API backend using an Express middleware
88
+ ### Add backend API with a [single line of code](https://remult.dev/docs/quickstart#server-side-initialization)
89
+
90
+ _Example:_
112
91
 
113
92
  ```ts
114
93
  // backend/index.ts
115
94
 
116
95
  import express from 'express'
117
- import { remultExpress } from 'remult/remult-express'
96
+ import { remultExpress } from 'remult/remult-express' // adapters for: Fastify, Next.js, Nuxt, SvelteKit, Nest, more...
97
+ import { createPostgresDataProvider } from "remult/postgres" // supported: PostgreSQL, MySQL, SQLite, MongoDB, MSSQL and Oracle
118
98
  import { Product } from '../shared/product'
119
99
 
120
- const port = 3001
121
100
  const app = express()
122
101
 
123
102
  app.use(
124
103
  remultExpress({
125
104
  entities: [Product],
105
+ dataProvider: createPostgresDataProvider({
106
+ connectionString: 'postgres://user:password@host:5432/database"',
107
+ }),
126
108
  }),
127
109
  )
128
110
 
129
- app.listen(port, () => {
130
- console.log(`Example API listening at http://localhost:${port}`)
131
- })
111
+ app.listen()
132
112
  ```
133
113
 
134
- ### :rocket: API Ready
135
-
136
- ```sh
137
- > curl http://localhost:3001/api/products
138
-
139
- [{"name":"Tofu","unitPrice":5}]
140
- ```
114
+ Remult adds route handlers for a fully functional REST API and realtime live-query endpoints, optionally including an [Open API](https://remult.dev/docs/adding-swagger) spec and a [GraphQL](https://remult.dev/docs/adding-graphql) endpoint
141
115
 
142
- ### Find and manipulate data in type-safe frontend code
116
+ ### Fetch data with type-safe frontend code
143
117
 
144
118
  ```ts
145
- // frontend/code.ts
146
-
147
- import { remult } from 'remult'
148
- import { Product } from '../shared/product'
149
-
150
- async function increasePriceOfTofu(priceIncrease: number) {
151
- const productsRepo = remult.repo(Product)
152
-
153
- const product = await productsRepo.findFirst({ name: 'Tofu' }) // filter is passed through API request all the way to the db
154
- product.unitPrice += priceIncrease
155
- productsRepo.save(product) // mutation request updates the db with no boilerplate code
156
- }
119
+ const [products, setProducts] = useState<Product[]>([])
120
+
121
+ useEffect(() => {
122
+ repo(Product)
123
+ .find({
124
+ limit: 10,
125
+ orderBy: {
126
+ name: 'asc',
127
+ },
128
+ where: {
129
+ unitPrice: { $gt: 5 },
130
+ },
131
+ })
132
+ .then(setProducts)
133
+ }, [])
157
134
  ```
158
135
 
159
- ### ..._exactly_ the same way as in backend code
136
+ ### :mega: Realtime Live Queries
160
137
 
161
138
  ```ts
162
- @BackendMethod({ allowed: Allow.authenticated })
163
- static async increasePriceOfTofu(priceIncrease: number) {
164
- const productsRepo = remult.repo(Product);
165
-
166
- const product = await productsRepo.findFirst({ name: 'Tofu' }); // use Remult in the backend as an ORM
167
- product.unitPrice += priceIncrease;
168
- productsRepo.save(product);
169
- }
139
+ useEffect(() => {
140
+ return repo(Product)
141
+ .liveQuery({
142
+ limit: 10,
143
+ orderBy: {
144
+ name: 'asc',
145
+ },
146
+ where: {
147
+ unitPrice: { $gt: 5 },
148
+ },
149
+ })
150
+ .subscribe((info) => {
151
+ setProducts(info.applyChanges)
152
+ })
153
+ }, [])
170
154
  ```
171
155
 
172
156
  ### :ballot_box_with_check: Data validation and constraints - defined once
@@ -178,49 +162,52 @@ import { Entity, Fields, Validators } from 'remult'
178
162
  allowApiCrud: true,
179
163
  })
180
164
  export class Product {
165
+ @Fields.cuid()
166
+ id = ''
167
+
181
168
  @Fields.string({
182
169
  validate: Validators.required,
183
170
  })
184
171
  name = ''
185
172
 
186
- @Fields.string<Product>({
187
- validate: (product) => {
188
- if (product.description.trim().length < 50) {
189
- throw 'too short'
190
- }
191
- },
192
- })
193
- description = ''
194
-
195
- @Fields.number({
196
- validate: (_, field) => {
197
- if (field.value < 0) {
198
- field.error = 'must not be less than 0' // or: throw "must not be less than 0";
199
- }
200
- },
173
+ @Fields.number<Product>({
174
+ validate: (product) => product.unitPrice > 0 || 'must be greater than 0',
201
175
  })
202
176
  unitPrice = 0
203
177
  }
204
178
  ```
205
179
 
206
- ### Enforced in frontend:
180
+ #### Enforced in frontend:
207
181
 
208
182
  ```ts
209
- const product = productsRepo.create()
210
-
211
183
  try {
212
- await productsRepo.save(product)
184
+ await repo(Product).insert({ name: '', unitPrice: -1 })
213
185
  } catch (e: any) {
214
- console.error(e.message) // Browser console will display - "Name: required"
186
+ console.error(e)
187
+ /* Detailed error object ->
188
+ {
189
+ "modelState": {
190
+ "name": "Should not be empty",
191
+ "unitPrice": "must be greater than 0"
192
+ },
193
+ "message": "Name: Should not be empty"
194
+ }
195
+ */
215
196
  }
216
197
  ```
217
198
 
218
- ### Enforced in backend:
199
+ #### Enforced in backend:
219
200
 
220
- ```sh
221
- > curl http://localhost:3001/api/products -H "Content-Type: application/json" -d "{""unitPrice"":-1}"
222
-
223
- {"modelState":{"unitPrice":"must not be less than 0","name":"required"},"message":"Name: required"}
201
+ ```ts
202
+ // POST '/api/products' BODY: { "name":"", "unitPrice":-1 }
203
+ // Response: status 400, body:
204
+ {
205
+ "modelState": {
206
+ "name": "Should not be empty",
207
+ "unitPrice": "must be greater than 0"
208
+ },
209
+ "message": "Name: Should not be empty"
210
+ }
224
211
  ```
225
212
 
226
213
  ### :lock: Secure the API with fine-grained authorization
@@ -228,21 +215,60 @@ try {
228
215
  ```ts
229
216
  @Entity<Article>('Articles', {
230
217
  allowApiRead: true,
231
- allowApiInsert: (_, remult) => remult.authenticated(),
232
- allowApiUpdate: (article, remult) => article.author.id == remult.user.id,
218
+ allowApiInsert: Allow.authenticated,
219
+ allowApiUpdate: (article) => article.author == remult.user.id,
220
+ apiPrefilter: () => {
221
+ if (remult.isAllowed('admin')) return {}
222
+ return {
223
+ author: remult.user.id,
224
+ }
225
+ },
233
226
  })
234
227
  export class Article {
235
228
  @Fields.string({ allowApiUpdate: false })
236
229
  slug = ''
237
230
 
238
- @Field(() => Profile, { allowApiUpdate: false })
239
- author!: Profile
231
+ @Fields.string({ allowApiUpdate: false })
232
+ authorId = remult.user!.id
240
233
 
241
234
  @Fields.string()
242
235
  content = ''
243
236
  }
244
237
  ```
245
238
 
239
+ ### :rocket: Relations
240
+
241
+ ```ts
242
+ await repo(Categories).find({
243
+ orderBy: {
244
+ name: 'asc ',
245
+ },
246
+ include: {
247
+ products: {
248
+ where: {
249
+ unitPrice: { $gt: 5 },
250
+ },
251
+ },
252
+ },
253
+ })
254
+
255
+ // Entity Definitions
256
+ export class Product {
257
+ //...
258
+ @Relations.toOne(Category)
259
+ category?: Category
260
+ }
261
+ export class Category {
262
+ //...
263
+ @Relations.toMany<Category, Product>(() => Product, `category`)
264
+ products?: Product[]
265
+ }
266
+ ```
267
+
268
+ ### Automatic admin UI
269
+
270
+ ![Automatic admin UI](static/images/admin-ui.gif)
271
+
246
272
  ## What about complex CRUD?
247
273
 
248
274
  While simple CRUD shouldn’t require any backend coding, using Remult means
@@ -256,12 +282,20 @@ numerous ways:
256
282
  [entity lifecycle hooks](https://remult.dev/docs/ref_entity.html#saving)
257
283
  (before/after saving/deleting)
258
284
  - Backend only updatable fields (e.g. “last updated at”)
259
- - Many-to-one [relations](https://remult.dev/docs/entity-relations.html) with
260
- [lazy/eager loading](https://remult.dev/docs/lazy-loading-of-related-entities.html)
285
+ - [Relations](https://remult.dev/docs/entity-relations.html)
261
286
  - Roll-your-own type-safe endpoints with
262
287
  [Backend Methods](https://remult.dev/docs/backendMethods.html)
263
288
  - Roll-your-own low-level endpoints (Express, Fastify, koa, others…)
264
289
 
290
+ ## Installation
291
+
292
+ The _remult_ package is one and the same for both the frontend bundle and the
293
+ backend. Install it once for a monolith project or per-repo in a monorepo.
294
+
295
+ ```sh
296
+ npm i remult
297
+ ```
298
+
265
299
  ## Tutorials
266
300
 
267
301
  The best way to learn Remult is by following a tutorial of a simple Todo web app
@@ -273,6 +307,18 @@ with a Node.js Express backend.
273
307
  - [Tutorial with Next.js](https://remult.dev/tutorials/react-next/)
274
308
  - [Tutorial with Sveltekit](https://remult.dev/tutorials/sveltekit/)
275
309
 
310
+ ## Demo
311
+
312
+ <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
313
+ <p align="center">
314
+ <img src="https://github.com/remult/remult/raw/master/static/images/video-thumbnail.jpg" alt="Video thumbnail" />
315
+ </p>
316
+ </a>
317
+
318
+ <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
319
+ <p align="center">Watch code demo on YouTube here (14 mins)</p>
320
+ </a>
321
+
276
322
  ## Documentation
277
323
 
278
324
  The [documentation](https://remult.dev/docs) covers the main features of Remult.
@@ -287,6 +333,31 @@ However, it is still a work-in-progress.
287
333
  - [CRM demo](https://github.com/remult/crm-demo) with a React +
288
334
  [MUI](https://mui.com) front-end and Postgres database.
289
335
 
336
+ ### Status
337
+
338
+ Remult is **production-ready** and, in fact, used in production apps since 2018.
339
+ However, we’re keeping the major version at zero so we can use community
340
+ feedback to finalize the v1 API.
341
+
342
+ ## Motivation
343
+
344
+ Full-stack web development is (still) too complicated. **Simple CRUD, a common
345
+ requirement of any business application, should be simple to build, maintain,
346
+ and extend** when the need arises.
347
+
348
+ Remult abstracts away repetitive, boilerplate, error-prone, and poorly designed
349
+ code on the one hand, and enables total flexibility and control on the other.
350
+ **Remult helps building fullstack apps using only TypeScript code you can easily
351
+ follow and safely refactor**, and fits nicely into any existing or new project
352
+ by being minimalistic and completely unopinionated regarding the developer’s
353
+ choice of other frameworks and tools.
354
+
355
+ Other frameworks tend to fall into either too much abstraction (no-code,
356
+ low-code, BaaS) or partial abstraction (MVC frameworks, GraphQL, ORMs, API
357
+ generators, code generators), and tend to be opinionated regarding the
358
+ development tool-chain, deployment environment, configuration/conventions or
359
+ DSL. Remult attempts to strike a better balance.
360
+
290
361
  ## Contributing
291
362
 
292
363
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
package/esm/graphql.js CHANGED
@@ -118,6 +118,11 @@ export function remultGraphql(options) {
118
118
  for (const meta of entities) {
119
119
  const orderByFields = [];
120
120
  const key = meta.key;
121
+ const requiredWhereArg = {
122
+ key: 'where',
123
+ value: `${key}Where!`,
124
+ comment: `Remult filtering options`,
125
+ };
121
126
  const currentType = upsertTypes(getMetaType(meta), 'type_impl_node');
122
127
  if (key) {
123
128
  const createResultPromise = (work) => {
@@ -213,6 +218,7 @@ export function remultGraphql(options) {
213
218
  });
214
219
  };
215
220
  const queryArgsConnection = getQueryArgsConnection(key);
221
+ let pluralEntityKey = key;
216
222
  let getSingleEntityKey = toCamelCase(getMetaType(meta));
217
223
  if (getSingleEntityKey === toCamelCase(key))
218
224
  getSingleEntityKey = 'single' + key;
@@ -325,66 +331,126 @@ export function remultGraphql(options) {
325
331
  if (checkCanExist(meta.options.allowApiUpdate)) {
326
332
  // update
327
333
  const updateInput = `Update${getMetaType(meta)}Input`;
328
- const updatePayload = `Update${getMetaType(meta)}Payload`;
329
- const updateResolverKey = `update${getMetaType(meta)}`;
330
- root_mutation.fields.push({
331
- key: updateResolverKey,
332
- args: [
333
- argId,
334
- { key: 'patch', value: `${updateInput}!` },
335
- argClientMutationId,
336
- ],
337
- value: `${updatePayload}`,
338
- comment: `Update a \`${getMetaType(meta)}\``,
339
- });
340
- currentType.mutation.update.input = upsertTypes(updateInput, 'input');
341
- currentType.mutation.update.payload = upsertTypes(updatePayload);
342
- currentType.mutation.update.payload.fields.push({
343
- key: `${toCamelCase(getMetaType(meta))}`,
344
- value: `${getMetaType(meta)}`,
345
- }, argErrorDetail, argClientMutationId);
346
- root[updateResolverKey] = handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
347
- await dApi.put({
348
- ...response,
349
- success: (y) => {
350
- const orig = { ...y };
351
- currentType.query.resultProcessors.forEach((z) => z(y, orig));
352
- setResult({
353
- [toCamelCase(getMetaType(meta))]: y,
354
- });
355
- },
356
- }, arg1.id, arg1.patch);
357
- });
358
- resolversMutation[updateResolverKey] = (origItem, args, req, gqlInfo) => root[updateResolverKey](args, req, gqlInfo);
334
+ {
335
+ const updatePayload = `Update${getMetaType(meta)}Payload`;
336
+ const updateResolverKey = `update${getMetaType(meta)}`;
337
+ root_mutation.fields.push({
338
+ key: updateResolverKey,
339
+ args: [
340
+ argId,
341
+ { key: 'patch', value: `${updateInput}!` },
342
+ argClientMutationId,
343
+ ],
344
+ value: `${updatePayload}`,
345
+ comment: `Update a \`${getMetaType(meta)}\``,
346
+ });
347
+ currentType.mutation.update.input = upsertTypes(updateInput, 'input');
348
+ currentType.mutation.update.payload = upsertTypes(updatePayload);
349
+ currentType.mutation.update.payload.fields.push({
350
+ key: `${toCamelCase(getMetaType(meta))}`,
351
+ value: `${getMetaType(meta)}`,
352
+ }, argErrorDetail, argClientMutationId);
353
+ resolversMutation[updateResolverKey] = (origItem, args, req, gqlInfo) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
354
+ await dApi.put({
355
+ ...response,
356
+ success: (y) => {
357
+ const orig = { ...y };
358
+ currentType.query.resultProcessors.forEach((z) => z(y, orig));
359
+ setResult({
360
+ [toCamelCase(getMetaType(meta))]: y,
361
+ });
362
+ },
363
+ }, arg1.id, arg1.patch);
364
+ })(args, req);
365
+ }
366
+ // update many
367
+ {
368
+ const updateManyPayload = `UpdateMany${upperFirstChar(pluralEntityKey)}Payload`;
369
+ const updateManyResolverKey = `updateMany${upperFirstChar(pluralEntityKey)}`;
370
+ root_mutation.fields.push({
371
+ key: updateManyResolverKey,
372
+ args: [
373
+ requiredWhereArg,
374
+ { key: 'patch', value: `${updateInput}!` },
375
+ argClientMutationId,
376
+ ],
377
+ value: `${updateManyPayload}`,
378
+ comment: `Update many \`${pluralEntityKey}\``,
379
+ });
380
+ currentType.mutation.update.payload = upsertTypes(updateManyPayload);
381
+ currentType.mutation.update.payload.fields.push({
382
+ key: `updated`,
383
+ value: `Int!`,
384
+ }, argErrorDetail, argClientMutationId);
385
+ resolversMutation[updateManyResolverKey] = (origItem, args, req, gqlInfo) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
386
+ await dApi.updateManyImplementation({
387
+ ...response,
388
+ success: (updateManyResult) => {
389
+ setResult(updateManyResult);
390
+ },
391
+ }, { get: () => undefined }, {
392
+ ...translateWhereToRestBody(meta.fields, arg1),
393
+ set: arg1.patch,
394
+ });
395
+ })(args, req);
396
+ }
359
397
  }
360
398
  if (checkCanExist(meta.options.allowApiDelete)) {
361
399
  // delete
362
- const deletePayload = `Delete${getMetaType(meta)}Payload`;
363
- const deleteResolverKey = `delete${getMetaType(meta)}`;
364
- root_mutation.fields.push({
365
- key: deleteResolverKey,
366
- args: [argId, argClientMutationId],
367
- value: `${deletePayload}`,
368
- comment: `Delete a \`${getMetaType(meta)}\``,
369
- });
370
- currentType.mutation.delete.payload = upsertTypes(deletePayload);
371
- const deletedResultKey = `id`;
372
- const type = meta.idMetadata.field instanceof CompoundIdField
373
- ? 'String'
374
- : getGraphqlBaseType(meta.idMetadata.field);
375
- currentType.mutation.delete.payload.fields.push({
376
- key: deletedResultKey,
377
- value: `${type}`,
378
- }, argErrorDetail, argClientMutationId);
379
- root[deleteResolverKey] = handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
380
- await dApi.delete({
381
- ...response,
382
- deleted: () => {
383
- setResult({ [deletedResultKey]: arg1.id });
384
- },
385
- }, arg1.id);
386
- });
387
- resolversMutation[deleteResolverKey] = (origItem, args, req, gqlInfo) => root[deleteResolverKey](args, req, gqlInfo);
400
+ {
401
+ const deletePayload = `Delete${getMetaType(meta)}Payload`;
402
+ const deleteResolverKey = `delete${getMetaType(meta)}`;
403
+ root_mutation.fields.push({
404
+ key: deleteResolverKey,
405
+ args: [argId, argClientMutationId],
406
+ value: `${deletePayload}`,
407
+ comment: `Delete a \`${getMetaType(meta)}\``,
408
+ });
409
+ currentType.mutation.delete.payload = upsertTypes(deletePayload);
410
+ const deletedResultKey = `id`;
411
+ const type = meta.idMetadata.field instanceof CompoundIdField
412
+ ? 'String'
413
+ : getGraphqlBaseType(meta.idMetadata.field);
414
+ currentType.mutation.delete.payload.fields.push({
415
+ key: deletedResultKey,
416
+ value: `${type}`,
417
+ }, argErrorDetail, argClientMutationId);
418
+ resolversMutation[deleteResolverKey] = (origItem, args, req) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
419
+ await dApi.delete({
420
+ ...response,
421
+ deleted: () => {
422
+ setResult({ [deletedResultKey]: arg1.id });
423
+ },
424
+ }, arg1.id);
425
+ })(args, req);
426
+ }
427
+ // delete
428
+ {
429
+ const deleteManyPayload = `DeleteMany${upperFirstChar(pluralEntityKey)}Payload`;
430
+ const deleteManyResolverKey = `deleteMany${upperFirstChar(pluralEntityKey)}`;
431
+ root_mutation.fields.push({
432
+ key: deleteManyResolverKey,
433
+ args: [requiredWhereArg, argClientMutationId],
434
+ value: `${deleteManyPayload}`,
435
+ comment: `Delete many \`${pluralEntityKey}\``,
436
+ });
437
+ currentType.mutation.delete.payload = upsertTypes(deleteManyPayload);
438
+ const type = meta.idMetadata.field instanceof CompoundIdField
439
+ ? 'String'
440
+ : getGraphqlBaseType(meta.idMetadata.field);
441
+ currentType.mutation.delete.payload.fields.push({
442
+ key: 'deleted',
443
+ value: `Int!`,
444
+ }, argErrorDetail, argClientMutationId);
445
+ resolversMutation[deleteManyResolverKey] = (origItem, args, req) => handleMutationWithErrors(async (dApi, response, setResult, arg1, req) => {
446
+ await dApi.deleteMany({
447
+ ...response,
448
+ success: (deleteManyResult) => {
449
+ setResult(deleteManyResult);
450
+ },
451
+ }, { get: () => undefined }, translateWhereToRestBody(meta.fields, arg1));
452
+ })(args, req);
453
+ }
388
454
  }
389
455
  const whereTypeFields = [];
390
456
  for (const f of meta.fields) {
@@ -838,6 +904,12 @@ function getGraphqlBaseType(field) {
838
904
  function getMetaType(entityMeta) {
839
905
  return entityMeta.entityType.name;
840
906
  }
907
+ function upperFirstChar(str) {
908
+ return str
909
+ .split('')
910
+ .map((c, i) => (i === 0 ? c.toUpperCase() : c))
911
+ .join('');
912
+ }
841
913
  function toCamelCase(str) {
842
914
  return str
843
915
  .split('')
package/esm/index.js CHANGED
@@ -13,7 +13,7 @@ export { CustomSqlFilterBuilder, dbNamesOf, } from './src/filter/filter-consumer
13
13
  export { JsonDataProvider, } from './src/data-providers/json-data-provider.js'; //V
14
14
  export { JsonEntityOpfsStorage } from './src/data-providers/json-entity-opfs-storage.js';
15
15
  //export * from './src/data-api'; //reconsider if to make internal
16
- export { Controller, BackendMethod, ProgressListener, } from './src/server-action.js';
16
+ export { Controller, BackendMethod, ProgressListener, ForbiddenError, } from './src/server-action.js';
17
17
  export { Allow, Remult, withRemult, isBackend, EventSource, } from './src/context.js';
18
18
  export { Sort } from './src/sort.js';
19
19
  export { CompoundIdField } from './src/CompoundIdField.js';