remult 0.25.8 → 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 (102) hide show
  1. package/README.md +194 -129
  2. package/esm/graphql.js +131 -59
  3. package/esm/index.js +1 -1
  4. package/esm/migrations/compare-migration-snapshots.js +61 -0
  5. package/esm/migrations/generate-migrations.js +68 -0
  6. package/esm/migrations/index.js +2 -0
  7. package/esm/migrations/migrate.js +90 -0
  8. package/esm/migrations/migration-types.js +36 -0
  9. package/esm/migrations/update-migrations-ts-file.js +115 -0
  10. package/esm/postgres/postgres-data-provider.js +24 -3
  11. package/esm/postgres/schema-builder.js +35 -26
  12. package/esm/remult-better-sqlite3.js +47 -0
  13. package/esm/remult-bun-sqlite.js +41 -0
  14. package/esm/remult-knex/index.js +180 -51
  15. package/esm/remult-mongo.js +2 -2
  16. package/esm/remult-sql-js.js +3 -78
  17. package/esm/remult-sqlite-core.js +104 -0
  18. package/esm/server/expressBridge.js +130 -29
  19. package/esm/server/initAsyncHooks.js +75 -1
  20. package/esm/src/CompoundIdField.js +8 -2
  21. package/esm/src/context.js +20 -24
  22. package/esm/src/data-api.js +71 -30
  23. package/esm/src/data-providers/rest-data-provider.js +17 -15
  24. package/esm/src/data-providers/sql-database.js +32 -7
  25. package/esm/src/data-providers/web-sql-data-provider.js +1 -9
  26. package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +37 -11
  27. package/esm/src/filter/filter-interfaces.js +229 -17
  28. package/esm/src/isOfType.js +9 -0
  29. package/esm/src/remult3/Fields.js +70 -18
  30. package/esm/src/remult3/RepositoryImplementation.js +47 -21
  31. package/esm/src/remult3/addValidator.js +15 -0
  32. package/esm/src/remult3/remult3.js +27 -57
  33. package/esm/src/server-action.js +4 -4
  34. package/esm/src/validators.js +6 -3
  35. package/esm/src/valueConverters.js +1 -1
  36. package/graphql.js +156 -71
  37. package/index.d.ts +4 -4
  38. package/index.js +2 -1
  39. package/migrations/compare-migration-snapshots.d.ts +27 -0
  40. package/migrations/compare-migration-snapshots.js +165 -0
  41. package/migrations/generate-migrations.d.ts +21 -0
  42. package/migrations/generate-migrations.js +104 -0
  43. package/migrations/index.d.ts +3 -0
  44. package/migrations/index.js +7 -0
  45. package/migrations/migrate.d.ts +27 -0
  46. package/migrations/migrate.js +175 -0
  47. package/migrations/migration-types.d.ts +48 -0
  48. package/migrations/migration-types.js +88 -0
  49. package/migrations/update-migrations-ts-file.d.ts +1 -0
  50. package/migrations/update-migrations-ts-file.js +141 -0
  51. package/package.json +21 -1
  52. package/postgres/postgres-data-provider.d.ts +8 -3
  53. package/postgres/postgres-data-provider.js +46 -3
  54. package/postgres/schema-builder.js +81 -48
  55. package/remult-better-sqlite3.d.ts +12 -0
  56. package/remult-better-sqlite3.js +64 -0
  57. package/remult-bun-sqlite.d.ts +11 -0
  58. package/remult-bun-sqlite.js +56 -0
  59. package/remult-knex/index.d.ts +12 -5
  60. package/remult-knex/index.js +257 -90
  61. package/remult-mongo.d.ts +2 -2
  62. package/remult-mongo.js +2 -2
  63. package/remult-sql-js.d.ts +2 -15
  64. package/remult-sql-js.js +11 -152
  65. package/remult-sqlite-core.d.ts +21 -0
  66. package/remult-sqlite-core.js +217 -0
  67. package/server/expressBridge.d.ts +30 -1
  68. package/server/expressBridge.js +140 -32
  69. package/server/initAsyncHooks.d.ts +16 -0
  70. package/server/initAsyncHooks.js +128 -2
  71. package/src/CompoundIdField.js +8 -2
  72. package/src/column-interfaces.d.ts +9 -3
  73. package/src/context.d.ts +8 -6
  74. package/src/context.js +27 -36
  75. package/src/data-api.d.ts +13 -9
  76. package/src/data-api.js +184 -135
  77. package/src/data-providers/rest-data-provider.js +26 -15
  78. package/src/data-providers/sql-database.d.ts +17 -5
  79. package/src/data-providers/sql-database.js +31 -7
  80. package/src/data-providers/web-sql-data-provider.d.ts +1 -2
  81. package/src/data-providers/web-sql-data-provider.js +4 -8
  82. package/src/entity.d.ts +64 -6
  83. package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +31 -7
  84. package/src/filter/filter-consumer-bridge-to-sql-request.js +46 -21
  85. package/src/filter/filter-interfaces.d.ts +106 -39
  86. package/src/filter/filter-interfaces.js +312 -32
  87. package/src/isOfType.d.ts +2 -0
  88. package/src/isOfType.js +14 -0
  89. package/src/remult3/Fields.d.ts +36 -0
  90. package/src/remult3/Fields.js +81 -19
  91. package/src/remult3/RepositoryImplementation.d.ts +7 -3
  92. package/src/remult3/RepositoryImplementation.js +106 -71
  93. package/src/remult3/addValidator.d.ts +2 -0
  94. package/src/remult3/addValidator.js +20 -0
  95. package/src/remult3/remult3.d.ts +6 -2
  96. package/src/remult3/remult3.js +27 -57
  97. package/src/server-action.d.ts +1 -1
  98. package/src/server-action.js +5 -4
  99. package/src/sql-command.d.ts +6 -1
  100. package/src/validators.d.ts +1 -0
  101. package/src/validators.js +10 -4
  102. package/src/valueConverters.js +1 -1
package/README.md CHANGED
@@ -1,27 +1,18 @@
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">
8
- <img alt="CircleCI" src="https://circleci.com/gh/remult/remult/tree/master.svg?style=shield">
9
- </a>
5
+ <img alt="CircleCI" src="https://circleci.com/gh/remult/remult/tree/master.svg?style=shield"></a>
10
6
  <a href="https://codecov.io/gh/remult/remult" rel="nofollow">
11
- <img src="https://codecov.io/gh/remult/remult/branch/master/graph/badge.svg?token=LYWQRUN3D1"/>
12
- </a>
7
+ <img src="https://codecov.io/gh/remult/remult/branch/master/graph/badge.svg?token=LYWQRUN3D1"/></a>
13
8
  <a href="https://raw.githubusercontent.com/remult/remult/master/LICENSE" rel="nofollow">
14
- <img alt="GitHub license" src="https://img.shields.io/badge/license-MIT-blue.svg">
15
- </a>
9
+ <img alt="GitHub license" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
16
10
  <a href="https://www.npmjs.com/package/remult" rel="nofollow">
17
- <img alt="npm version" src="https://badge.fury.io/js/remult.svg">
18
- </a>
11
+ <img alt="npm version" src="https://badge.fury.io/js/remult.svg"></a>
19
12
  <a href="https://www.npmjs.com/package/remult" rel="nofollow">
20
- <img alt="npm downloads" src="https://img.shields.io/npm/dm/remult">
21
- </a>
13
+ <img alt="npm downloads" src="https://img.shields.io/npm/dm/remult"></a>
22
14
  <a href="https://discord.gg/GXHk7ZfuG5" rel="nofollow">
23
- <img alt="Join Discord" src="https://badgen.net/discord/online-members/GXHk7ZfuG5?icon=discord&label=Discord"/>
24
- </a>
15
+ <img alt="Join Discord" src="https://badgen.net/discord/members/GXHk7ZfuG5?icon=discord&label=Discord"/></a>
25
16
  </div>
26
17
 
27
18
  <br/>
@@ -35,67 +26,44 @@
35
26
  <hr/>
36
27
  <br/>
37
28
 
38
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
39
- <p align="center">
40
- <img src="https://github.com/remult/remult/raw/master/static/images/video-thumbnail.jpg" alt="Video thumbnail" />
41
- </p>
42
- </a>
43
-
44
- <a href="https://www.youtube.com/watch?v=rEoScmSVNUE" target="_blank">
45
- <p align="center">Watch code demo on YouTube here (14 mins)</p>
46
- </a>
47
-
48
29
  ## What is Remult?
49
30
 
50
- **Remult** is a full-stack CRUD framework that uses your **TypeScript entities
51
- as a single source of truth for your API, frontend type-safe API client and
52
- 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.
53
33
 
54
- - :zap: Zero-boilerplate CRUD API routes with paging, sorting, and filtering for Express / Fastify / Next.js / NestJS / Koa / others...
55
- - :ok_hand: Fullstack type-safety for API queries, mutations and RPC, without code generation
56
- - :sparkles: Input validation, defined once, runs both on the backend and on the frontend for best UX
57
- - :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**
58
38
  - :relieved: Incrementally adoptable
59
- - :rocket: Production ready
60
- - :mega: **NEW - Zero-boilerplate realtime live-queries**
61
-
62
- ### Status
63
39
 
64
- Remult is **production-ready** and, in fact, used in production apps since 2018.
65
- However, we’re keeping the major version at zero so we can use community
66
- feedback to finalize the v1 API.
40
+ **Remult supports all major databases**, including: PostgreSQL, MySQL, SQLite, MongoDB, MSSQL and Oracle.
67
41
 
68
- ## Motivation
69
-
70
- Full-stack web development is (still) too complicated. **Simple CRUD, a common
71
- requirement of any business application, should be simple to build, maintain,
72
- and extend** when the need arises.
73
-
74
- Remult abstracts away repetitive, boilerplate, error-prone, and poorly designed
75
- code on the one hand, and enables total flexibility and control on the other.
76
- **Remult helps building fullstack apps using only TypeScript code you can easily
77
- follow and safely refactor**, and fits nicely into any existing or new project
78
- by being minimalistic and completely unopinionated regarding the developer’s
79
- choice of other frameworks and tools.
80
-
81
- Other frameworks tend to fall into either too much abstraction (no-code,
82
- low-code, BaaS) or partial abstraction (MVC frameworks, GraphQL, ORMs, API
83
- generators, code generators), and tend to be opinionated regarding the
84
- development tool-chain, deployment environment, configuration/conventions or
85
- 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.
86
43
 
87
- ## Installation
44
+ Remult promotes a **consistent query syntax for both frontend and Backend code**:
88
45
 
89
- The _remult_ package is one and the same for both the frontend bundle and the
90
- 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
+ })
91
58
 
92
- ```sh
93
- 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 })
94
62
  ```
95
63
 
96
64
  ## Usage
97
65
 
98
- ### Define model classes
66
+ ### Define schema in code
99
67
 
100
68
  ```ts
101
69
  // shared/product.ts
@@ -106,6 +74,9 @@ import { Entity, Fields } from 'remult'
106
74
  allowApiCrud: true,
107
75
  })
108
76
  export class Product {
77
+ @Fields.cuid()
78
+ id = ''
79
+
109
80
  @Fields.string()
110
81
  name = ''
111
82
 
@@ -114,65 +85,72 @@ export class Product {
114
85
  }
115
86
  ```
116
87
 
117
- ### 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:_
118
91
 
119
92
  ```ts
120
93
  // backend/index.ts
121
94
 
122
95
  import express from 'express'
123
- 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
124
98
  import { Product } from '../shared/product'
125
99
 
126
- const port = 3001
127
100
  const app = express()
128
101
 
129
102
  app.use(
130
103
  remultExpress({
131
104
  entities: [Product],
105
+ dataProvider: createPostgresDataProvider({
106
+ connectionString: 'postgres://user:password@host:5432/database"',
107
+ }),
132
108
  }),
133
109
  )
134
110
 
135
- app.listen(port, () => {
136
- console.log(`Example API listening at http://localhost:${port}`)
137
- })
111
+ app.listen()
138
112
  ```
139
113
 
140
- ### :rocket: API Ready
141
-
142
- ```sh
143
- > curl http://localhost:3001/api/products
144
-
145
- [{"name":"Tofu","unitPrice":5}]
146
- ```
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
147
115
 
148
- ### Find and manipulate data in type-safe frontend code
116
+ ### Fetch data with type-safe frontend code
149
117
 
150
118
  ```ts
151
- // frontend/code.ts
152
-
153
- import { remult } from 'remult'
154
- import { Product } from '../shared/product'
155
-
156
- async function increasePriceOfTofu(priceIncrease: number) {
157
- const productsRepo = remult.repo(Product)
158
-
159
- const product = await productsRepo.findFirst({ name: 'Tofu' }) // filter is passed through API request all the way to the db
160
- product.unitPrice += priceIncrease
161
- productsRepo.save(product) // mutation request updates the db with no boilerplate code
162
- }
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
+ }, [])
163
134
  ```
164
135
 
165
- ### ..._exactly_ the same way as in backend code
136
+ ### :mega: Realtime Live Queries
166
137
 
167
138
  ```ts
168
- @BackendMethod({ allowed: Allow.authenticated })
169
- static async increasePriceOfTofu(priceIncrease: number) {
170
- const productsRepo = remult.repo(Product);
171
-
172
- const product = await productsRepo.findFirst({ name: 'Tofu' }); // use Remult in the backend as an ORM
173
- product.unitPrice += priceIncrease;
174
- productsRepo.save(product);
175
- }
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
+ }, [])
176
154
  ```
177
155
 
178
156
  ### :ballot_box_with_check: Data validation and constraints - defined once
@@ -184,49 +162,52 @@ import { Entity, Fields, Validators } from 'remult'
184
162
  allowApiCrud: true,
185
163
  })
186
164
  export class Product {
165
+ @Fields.cuid()
166
+ id = ''
167
+
187
168
  @Fields.string({
188
169
  validate: Validators.required,
189
170
  })
190
171
  name = ''
191
172
 
192
- @Fields.string<Product>({
193
- validate: (product) => {
194
- if (product.description.trim().length < 50) {
195
- throw 'too short'
196
- }
197
- },
198
- })
199
- description = ''
200
-
201
- @Fields.number({
202
- validate: (_, field) => {
203
- if (field.value < 0) {
204
- field.error = 'must not be less than 0' // or: throw "must not be less than 0";
205
- }
206
- },
173
+ @Fields.number<Product>({
174
+ validate: (product) => product.unitPrice > 0 || 'must be greater than 0',
207
175
  })
208
176
  unitPrice = 0
209
177
  }
210
178
  ```
211
179
 
212
- ### Enforced in frontend:
180
+ #### Enforced in frontend:
213
181
 
214
182
  ```ts
215
- const product = productsRepo.create()
216
-
217
183
  try {
218
- await productsRepo.save(product)
184
+ await repo(Product).insert({ name: '', unitPrice: -1 })
219
185
  } catch (e: any) {
220
- 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
+ */
221
196
  }
222
197
  ```
223
198
 
224
- ### Enforced in backend:
199
+ #### Enforced in backend:
225
200
 
226
- ```sh
227
- > curl http://localhost:3001/api/products -H "Content-Type: application/json" -d "{""unitPrice"":-1}"
228
-
229
- {"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
+ }
230
211
  ```
231
212
 
232
213
  ### :lock: Secure the API with fine-grained authorization
@@ -234,21 +215,60 @@ try {
234
215
  ```ts
235
216
  @Entity<Article>('Articles', {
236
217
  allowApiRead: true,
237
- allowApiInsert: (_, remult) => remult.authenticated(),
238
- 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
+ },
239
226
  })
240
227
  export class Article {
241
228
  @Fields.string({ allowApiUpdate: false })
242
229
  slug = ''
243
230
 
244
- @Field(() => Profile, { allowApiUpdate: false })
245
- author!: Profile
231
+ @Fields.string({ allowApiUpdate: false })
232
+ authorId = remult.user!.id
246
233
 
247
234
  @Fields.string()
248
235
  content = ''
249
236
  }
250
237
  ```
251
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
+
252
272
  ## What about complex CRUD?
253
273
 
254
274
  While simple CRUD shouldn’t require any backend coding, using Remult means
@@ -262,12 +282,20 @@ numerous ways:
262
282
  [entity lifecycle hooks](https://remult.dev/docs/ref_entity.html#saving)
263
283
  (before/after saving/deleting)
264
284
  - Backend only updatable fields (e.g. “last updated at”)
265
- - Many-to-one [relations](https://remult.dev/docs/entity-relations.html) with
266
- [lazy/eager loading](https://remult.dev/docs/lazy-loading-of-related-entities.html)
285
+ - [Relations](https://remult.dev/docs/entity-relations.html)
267
286
  - Roll-your-own type-safe endpoints with
268
287
  [Backend Methods](https://remult.dev/docs/backendMethods.html)
269
288
  - Roll-your-own low-level endpoints (Express, Fastify, koa, others…)
270
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
+
271
299
  ## Tutorials
272
300
 
273
301
  The best way to learn Remult is by following a tutorial of a simple Todo web app
@@ -279,6 +307,18 @@ with a Node.js Express backend.
279
307
  - [Tutorial with Next.js](https://remult.dev/tutorials/react-next/)
280
308
  - [Tutorial with Sveltekit](https://remult.dev/tutorials/sveltekit/)
281
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
+
282
322
  ## Documentation
283
323
 
284
324
  The [documentation](https://remult.dev/docs) covers the main features of Remult.
@@ -293,6 +333,31 @@ However, it is still a work-in-progress.
293
333
  - [CRM demo](https://github.com/remult/crm-demo) with a React +
294
334
  [MUI](https://mui.com) front-end and Postgres database.
295
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
+
296
361
  ## Contributing
297
362
 
298
363
  Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).