remult 0.25.8-exp.11 → 0.25.8-exp.13

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 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/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';
@@ -1,7 +1,7 @@
1
1
  // consider replacing with https://www.npmjs.com/package/ts-morph
2
- import ts from 'typescript';
3
- import * as fs from 'fs';
4
2
  export async function updateMigrationsFile(filePath, steps) {
3
+ const ts = await import('typescript');
4
+ const fs = await import('fs');
5
5
  let maxStep = -1;
6
6
  function buildSteps() {
7
7
  return steps.map((step) => ts.factory.createPropertyAssignment(ts.factory.createIdentifier((++maxStep).toString()), ts.factory.createIdentifier(step)));
@@ -38,8 +38,7 @@ export class BetterSqlite3SqlResult {
38
38
  result;
39
39
  constructor(result) {
40
40
  this.result = result;
41
- this.rows =
42
- result.map((row) => Object.keys(row).reduce((prev, curr) => ({ ...prev, [curr]: row[curr] }), {}));
41
+ this.rows = result;
43
42
  }
44
43
  rows;
45
44
  getColumnKeyInResultForIndexInSelect(index) {
@@ -0,0 +1,41 @@
1
+ import { SqliteCoreDataProvider } from "./remult-sqlite-core.js";
2
+ export class BunSqliteDataProvider extends SqliteCoreDataProvider {
3
+ constructor(db) {
4
+ super(() => new BunSqliteCommand(db), async () => { db.close(); });
5
+ }
6
+ }
7
+ class BunSqliteCommand {
8
+ db;
9
+ values = {};
10
+ i = 0;
11
+ constructor(db) {
12
+ this.db = db;
13
+ }
14
+ async execute(sql) {
15
+ const s = this.db.query(sql);
16
+ return new BunSqliteSqlResult(s.all(this.values));
17
+ }
18
+ addParameterAndReturnSqlToken(val) {
19
+ return this.param(val);
20
+ }
21
+ param(val) {
22
+ if (val instanceof Date)
23
+ val = val.valueOf();
24
+ if (typeof val === "boolean")
25
+ val = val ? 1 : 0;
26
+ const key = ':' + (this.i++);
27
+ this.values[key] = (val);
28
+ return key;
29
+ }
30
+ }
31
+ class BunSqliteSqlResult {
32
+ result;
33
+ constructor(result) {
34
+ this.result = result;
35
+ this.rows = result;
36
+ }
37
+ rows;
38
+ getColumnKeyInResultForIndexInSelect(index) {
39
+ return Object.keys(this.result[0])[index];
40
+ }
41
+ }
@@ -213,19 +213,29 @@ export class DataApi {
213
213
  where.push(this.repository.metadata.options.apiPrefilter);
214
214
  }
215
215
  if (request) {
216
- where.push(buildFilterFromRequestParameters(this.repository.metadata, {
216
+ let f = buildFilterFromRequestParameters(this.repository.metadata, {
217
217
  get: (key) => {
218
- let result = request.get(key);
218
+ let result = body?.where?.[key];
219
+ if (result !== undefined)
220
+ return result;
221
+ result = request.get(key);
219
222
  if (key.startsWith(customUrlToken) &&
220
223
  result &&
221
224
  typeof result === 'string')
222
225
  return JSON.parse(result);
223
226
  return result;
224
227
  },
225
- }));
228
+ });
229
+ if (this.repository.metadata.options.apiPreprocessFilter) {
230
+ f = await this.repository.metadata.options.apiPreprocessFilter(f, {
231
+ metadata: this.repository.metadata,
232
+ getFilterInfo: async (filter) => {
233
+ return Filter.getInfo(this.repository.metadata, filter || f);
234
+ },
235
+ });
236
+ }
237
+ where.push(f);
226
238
  }
227
- if (body?.where)
228
- where.push(Filter.entityFilterFromJson(this.repository.metadata, body.where));
229
239
  return { $and: where };
230
240
  }
231
241
  async doOnId(response, id, what) {
@@ -342,7 +352,7 @@ export class DataApi {
342
352
  }
343
353
  catch (err) {
344
354
  if (err.isForbiddenError)
345
- response.forbidden();
355
+ response.forbidden(err.message);
346
356
  else
347
357
  response.error(err, this.repository.metadata);
348
358
  }