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.
- package/README.md +188 -117
- package/esm/graphql.js +129 -57
- package/esm/index.js +1 -1
- package/esm/migrations/compare-migration-snapshots.js +26 -4
- package/esm/migrations/generate-migrations.js +13 -8
- package/esm/migrations/migrate.js +47 -33
- package/esm/migrations/migration-types.js +27 -3
- package/esm/migrations/update-migrations-ts-file.js +5 -2
- package/esm/remult-better-sqlite3.js +47 -0
- package/esm/remult-bun-sqlite.js +41 -0
- package/esm/remult-knex/index.js +66 -25
- package/esm/remult-sql-js.js +3 -83
- package/esm/remult-sqlite-core.js +104 -0
- package/esm/server/expressBridge.js +1 -1
- package/esm/server/initAsyncHooks.js +0 -1
- package/esm/src/data-api.js +20 -10
- package/esm/src/data-providers/sql-database.js +18 -4
- package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +33 -11
- package/esm/src/filter/filter-interfaces.js +198 -17
- package/esm/src/remult3/RepositoryImplementation.js +35 -14
- package/esm/src/remult3/remult3.js +18 -6
- package/esm/src/server-action.js +2 -2
- package/graphql.js +154 -67
- package/index.d.ts +4 -4
- package/index.js +2 -1
- package/migrations/compare-migration-snapshots.d.ts +3 -2
- package/migrations/compare-migration-snapshots.js +107 -42
- package/migrations/generate-migrations.d.ts +5 -5
- package/migrations/generate-migrations.js +17 -13
- package/migrations/migrate.d.ts +12 -3
- package/migrations/migrate.js +104 -98
- package/migrations/migration-types.d.ts +7 -2
- package/migrations/migration-types.js +68 -7
- package/migrations/update-migrations-ts-file.js +54 -48
- package/package.json +16 -1
- package/remult-better-sqlite3.d.ts +12 -0
- package/remult-better-sqlite3.js +64 -0
- package/remult-bun-sqlite.d.ts +11 -0
- package/remult-bun-sqlite.js +56 -0
- package/remult-knex/index.d.ts +2 -2
- package/remult-knex/index.js +91 -42
- package/remult-sql-js.d.ts +2 -17
- package/remult-sql-js.js +11 -166
- package/remult-sqlite-core.d.ts +21 -0
- package/remult-sqlite-core.js +217 -0
- package/server/expressBridge.js +1 -1
- package/server/initAsyncHooks.js +0 -1
- package/src/data-api.d.ts +2 -2
- package/src/data-api.js +87 -78
- package/src/data-providers/sql-database.d.ts +13 -2
- package/src/data-providers/sql-database.js +19 -3
- package/src/entity.d.ts +64 -6
- package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +30 -7
- package/src/filter/filter-consumer-bridge-to-sql-request.js +40 -20
- package/src/filter/filter-interfaces.d.ts +106 -39
- package/src/filter/filter-interfaces.js +248 -20
- package/src/remult3/RepositoryImplementation.d.ts +3 -3
- package/src/remult3/RepositoryImplementation.js +43 -18
- package/src/remult3/remult3.d.ts +1 -1
- package/src/remult3/remult3.js +18 -6
- package/src/server-action.d.ts +1 -1
- package/src/server-action.js +3 -2
- 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
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
- :ok_hand: Fullstack type-safety for API queries, mutations and RPC, without code generation
|
|
50
|
-
- :sparkles: Input validation, defined once
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
Remult promotes a **consistent query syntax for both frontend and Backend code**:
|
|
82
45
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
###
|
|
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(
|
|
130
|
-
console.log(`Example API listening at http://localhost:${port}`)
|
|
131
|
-
})
|
|
111
|
+
app.listen()
|
|
132
112
|
```
|
|
133
113
|
|
|
134
|
-
|
|
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
|
-
###
|
|
116
|
+
### Fetch data with type-safe frontend code
|
|
143
117
|
|
|
144
118
|
```ts
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
###
|
|
136
|
+
### :mega: Realtime Live Queries
|
|
160
137
|
|
|
161
138
|
```ts
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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.
|
|
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
|
-
|
|
180
|
+
#### Enforced in frontend:
|
|
207
181
|
|
|
208
182
|
```ts
|
|
209
|
-
const product = productsRepo.create()
|
|
210
|
-
|
|
211
183
|
try {
|
|
212
|
-
await
|
|
184
|
+
await repo(Product).insert({ name: '', unitPrice: -1 })
|
|
213
185
|
} catch (e: any) {
|
|
214
|
-
console.error(e
|
|
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
|
-
|
|
199
|
+
#### Enforced in backend:
|
|
219
200
|
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
{
|
|
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:
|
|
232
|
-
allowApiUpdate: (article
|
|
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
|
-
@
|
|
239
|
-
|
|
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
|
+

|
|
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
|
-
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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';
|