remult 0.14.7 → 0.14.8

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/esm/assign.d.ts +1 -0
  3. package/esm/assign.js +5 -0
  4. package/esm/classType.d.ts +3 -0
  5. package/esm/classType.js +1 -0
  6. package/esm/graphql.d.ts +5 -0
  7. package/esm/graphql.js +102 -0
  8. package/esm/index.d.ts +36 -0
  9. package/esm/index.js +30 -0
  10. package/esm/inputTypes.d.ts +9 -0
  11. package/esm/inputTypes.js +9 -0
  12. package/esm/postgres/index.d.ts +2 -0
  13. package/esm/postgres/index.js +2 -0
  14. package/esm/postgres/postgres-data-provider.d.ts +28 -0
  15. package/esm/postgres/postgres-data-provider.js +97 -0
  16. package/esm/postgres/schema-builder.d.ts +15 -0
  17. package/esm/postgres/schema-builder.js +128 -0
  18. package/esm/remult-express.d.ts +5 -0
  19. package/esm/remult-express.js +16 -0
  20. package/esm/remult-knex/index.d.ts +51 -0
  21. package/esm/remult-knex/index.js +381 -0
  22. package/esm/remult-middleware.d.ts +2 -0
  23. package/esm/remult-middleware.js +63 -0
  24. package/esm/remult-mongo.d.ts +9 -0
  25. package/esm/remult-mongo.js +220 -0
  26. package/esm/server/JsonEntityFileStorage.d.ts +7 -0
  27. package/esm/server/JsonEntityFileStorage.js +20 -0
  28. package/esm/server/expressBridge.d.ts +140 -0
  29. package/esm/server/expressBridge.js +668 -0
  30. package/esm/server/index.d.ts +4 -0
  31. package/esm/server/index.js +4 -0
  32. package/esm/server/register-actions-on-server.d.ts +3 -0
  33. package/esm/server/register-actions-on-server.js +24 -0
  34. package/esm/server/register-entities-on-server.d.ts +3 -0
  35. package/esm/server/register-entities-on-server.js +15 -0
  36. package/esm/src/__EntityValueProvider.d.ts +10 -0
  37. package/esm/src/__EntityValueProvider.js +1 -0
  38. package/esm/src/__RowsOfDataForTesting.d.ts +3 -0
  39. package/esm/src/__RowsOfDataForTesting.js +1 -0
  40. package/esm/src/backend-tests/backend-database-test-setup.backend-spec.d.ts +5 -0
  41. package/esm/src/backend-tests/backend-database-test-setup.backend-spec.js +113 -0
  42. package/esm/src/backend-tests/my-test.backend-spec.d.ts +1 -0
  43. package/esm/src/backend-tests/my-test.backend-spec.js +49 -0
  44. package/esm/src/column-interfaces.d.ts +95 -0
  45. package/esm/src/column-interfaces.js +7 -0
  46. package/esm/src/column.d.ts +49 -0
  47. package/esm/src/column.js +133 -0
  48. package/esm/src/context.d.ts +124 -0
  49. package/esm/src/context.js +285 -0
  50. package/esm/src/data-api.d.ts +33 -0
  51. package/esm/src/data-api.js +204 -0
  52. package/esm/src/data-interfaces.d.ts +37 -0
  53. package/esm/src/data-interfaces.js +12 -0
  54. package/esm/src/data-providers/array-entity-data-provider.d.ts +22 -0
  55. package/esm/src/data-providers/array-entity-data-provider.js +264 -0
  56. package/esm/src/data-providers/in-memory-database.d.ts +9 -0
  57. package/esm/src/data-providers/in-memory-database.js +16 -0
  58. package/esm/src/data-providers/json-data-provider.d.ts +12 -0
  59. package/esm/src/data-providers/json-data-provider.js +52 -0
  60. package/esm/src/data-providers/rest-data-provider.d.ts +35 -0
  61. package/esm/src/data-providers/rest-data-provider.js +188 -0
  62. package/esm/src/data-providers/sql-database.d.ts +16 -0
  63. package/esm/src/data-providers/sql-database.js +261 -0
  64. package/esm/src/data-providers/web-sql-data-provider.d.ts +18 -0
  65. package/esm/src/data-providers/web-sql-data-provider.js +121 -0
  66. package/esm/src/entity.d.ts +82 -0
  67. package/esm/src/entity.js +1 -0
  68. package/esm/src/filter/filter-consumer-bridge-to-sql-request.d.ts +50 -0
  69. package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +139 -0
  70. package/esm/src/filter/filter-consumer-bridge-to-url-builder.d.ts +1 -0
  71. package/esm/src/filter/filter-consumer-bridge-to-url-builder.js +1 -0
  72. package/esm/src/filter/filter-interfaces.d.ts +85 -0
  73. package/esm/src/filter/filter-interfaces.js +467 -0
  74. package/esm/src/id-entity.d.ts +4 -0
  75. package/esm/src/id-entity.js +8 -0
  76. package/esm/src/remult3/RepositoryImplementation.d.ts +326 -0
  77. package/esm/src/remult3/RepositoryImplementation.js +1806 -0
  78. package/esm/src/remult3/index.d.ts +2 -0
  79. package/esm/src/remult3/index.js +2 -0
  80. package/esm/src/remult3/remult3.d.ts +312 -0
  81. package/esm/src/remult3/remult3.js +1 -0
  82. package/esm/src/server-action.d.ts +70 -0
  83. package/esm/src/server-action.js +365 -0
  84. package/esm/src/shared-tests/db-tests-setup.d.ts +23 -0
  85. package/esm/src/shared-tests/db-tests-setup.js +66 -0
  86. package/esm/src/shared-tests/db-tests.db-tests.d.ts +26 -0
  87. package/esm/src/shared-tests/db-tests.db-tests.js +723 -0
  88. package/esm/src/shared-tests/deleteAll.d.ts +2 -0
  89. package/esm/src/shared-tests/deleteAll.js +4 -0
  90. package/esm/src/shared-tests/entityWithValidations.d.ts +14 -0
  91. package/esm/src/shared-tests/entityWithValidations.js +53 -0
  92. package/esm/src/shared-tests/index.d.ts +4 -0
  93. package/esm/src/shared-tests/index.js +4 -0
  94. package/esm/src/shared-tests/test-custom-id-column.spec.d.ts +1 -0
  95. package/esm/src/shared-tests/test-custom-id-column.spec.js +62 -0
  96. package/esm/src/shared-tests/test-special-value.spec.d.ts +10 -0
  97. package/esm/src/shared-tests/test-special-value.spec.js +88 -0
  98. package/esm/src/shared-tests/test-update-with-null.spec.d.ts +1 -0
  99. package/esm/src/shared-tests/test-update-with-null.spec.js +47 -0
  100. package/esm/src/sort.d.ts +15 -0
  101. package/esm/src/sort.js +82 -0
  102. package/esm/src/sql-command.d.ts +15 -0
  103. package/esm/src/sql-command.js +1 -0
  104. package/esm/src/tests/Done.d.ts +5 -0
  105. package/esm/src/tests/Done.js +11 -0
  106. package/esm/src/tests/TestDataApiResponse.d.ts +10 -0
  107. package/esm/src/tests/TestDataApiResponse.js +22 -0
  108. package/esm/src/tests/c.d.ts +8 -0
  109. package/esm/src/tests/c.js +22 -0
  110. package/esm/src/tests/createData.d.ts +7 -0
  111. package/esm/src/tests/createData.js +22 -0
  112. package/esm/src/tests/d.d.ts +6 -0
  113. package/esm/src/tests/d.js +19 -0
  114. package/esm/src/tests/dWithPrefilter.d.ts +7 -0
  115. package/esm/src/tests/dWithPrefilter.js +32 -0
  116. package/esm/src/tests/entityForCustomFilter.d.ts +16 -0
  117. package/esm/src/tests/entityForCustomFilter.js +44 -0
  118. package/esm/src/tests/entityWithValidationsOnColumn.d.ts +5 -0
  119. package/esm/src/tests/entityWithValidationsOnColumn.js +21 -0
  120. package/esm/src/tests/frontend-database-tests-setup.spec.d.ts +4 -0
  121. package/esm/src/tests/frontend-database-tests-setup.spec.js +38 -0
  122. package/esm/src/tests/h.d.ts +6 -0
  123. package/esm/src/tests/h.js +35 -0
  124. package/esm/src/tests/remult-3-entities.d.ts +23 -0
  125. package/esm/src/tests/remult-3-entities.js +72 -0
  126. package/esm/src/tests/tasks.d.ts +6 -0
  127. package/esm/src/tests/tasks.js +24 -0
  128. package/esm/src/tests/testHelper.spec.d.ts +34 -0
  129. package/esm/src/tests/testHelper.spec.js +217 -0
  130. package/esm/src/tests/testModel/models.d.ts +16 -0
  131. package/esm/src/tests/testModel/models.js +31 -0
  132. package/esm/src/validators.d.ts +14 -0
  133. package/esm/src/validators.js +33 -0
  134. package/esm/urlBuilder.d.ts +6 -0
  135. package/esm/urlBuilder.js +19 -0
  136. package/esm/valueConverters.d.ts +12 -0
  137. package/esm/valueConverters.js +169 -0
  138. package/index.d.ts +1 -0
  139. package/index.js +1 -0
  140. package/package.json +22 -1
  141. package/postgres/index.d.ts +1 -0
  142. package/postgres/index.js +1 -0
  143. package/postgres/postgres-data-provider.d.ts +1 -13
  144. package/postgres/postgres-data-provider.js +28 -262
  145. package/postgres/schema-builder.d.ts +15 -0
  146. package/postgres/schema-builder.js +270 -0
  147. package/readme.md +68 -30
  148. package/src/data-providers/web-sql-data-provider.js +4 -3
  149. package/src/entity.d.ts +9 -2
  150. package/src/remult3/RepositoryImplementation.js +11 -3
  151. package/src/shared-tests/test-update-with-null.spec.js +38 -0
  152. package/valueConverters.js +4 -0
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PostgresSchemaBuilder = exports.verifyStructureOfAllEntities = exports.postgresColumnSyntax = void 0;
4
+ var tslib_1 = require("tslib");
5
+ var context_1 = require("../src/context");
6
+ var filter_consumer_bridge_to_sql_request_1 = require("../src/filter/filter-consumer-bridge-to-sql-request");
7
+ var remult3_1 = require("../src/remult3");
8
+ var valueConverters_1 = require("../valueConverters");
9
+ function postgresColumnSyntax(x, dbName) {
10
+ var result = dbName;
11
+ if (x.valueType == Number) {
12
+ if (!x.valueConverter.fieldTypeInDb)
13
+ result += " numeric" + (x.allowNull ? "" : " default 0 not null");
14
+ else
15
+ result += " " + x.valueConverter.fieldTypeInDb + (x.allowNull ? "" : " default 0 not null");
16
+ }
17
+ else if (x.valueType == Date) {
18
+ if (!x.valueConverter.fieldTypeInDb)
19
+ if (x.valueConverter == valueConverters_1.ValueConverters.DateOnly)
20
+ result += " date";
21
+ else
22
+ result += " timestamp";
23
+ else
24
+ result += " " + x.valueConverter.fieldTypeInDb;
25
+ }
26
+ else if (x.valueType == Boolean)
27
+ result += " boolean" + (x.allowNull ? "" : " default false not null");
28
+ else if (x.valueConverter.fieldTypeInDb) {
29
+ result += " " + x.valueConverter.fieldTypeInDb;
30
+ if (!x.allowNull && x.valueConverter.fieldTypeInDb == 'integer') {
31
+ result += " default 0 not null";
32
+ }
33
+ }
34
+ else
35
+ result += " varchar" + (x.allowNull ? "" : " default '' not null");
36
+ return result;
37
+ }
38
+ exports.postgresColumnSyntax = postgresColumnSyntax;
39
+ function verifyStructureOfAllEntities(db, remult) {
40
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
41
+ return tslib_1.__generator(this, function (_a) {
42
+ switch (_a.label) {
43
+ case 0: return [4 /*yield*/, new PostgresSchemaBuilder(db).verifyStructureOfAllEntities(remult)];
44
+ case 1: return [2 /*return*/, _a.sent()];
45
+ }
46
+ });
47
+ });
48
+ }
49
+ exports.verifyStructureOfAllEntities = verifyStructureOfAllEntities;
50
+ var PostgresSchemaBuilder = /** @class */ (function () {
51
+ function PostgresSchemaBuilder(pool, schema) {
52
+ this.pool = pool;
53
+ this.additionalWhere = '';
54
+ if (schema) {
55
+ this.additionalWhere = ' and table_schema=\'' + schema + '\'';
56
+ }
57
+ }
58
+ PostgresSchemaBuilder.prototype.verifyStructureOfAllEntities = function (remult) {
59
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
60
+ var allEntities_1, allEntities_1_1, entityClass, entity, e, err_1, e_1_1;
61
+ var e_1, _a;
62
+ return tslib_1.__generator(this, function (_b) {
63
+ switch (_b.label) {
64
+ case 0:
65
+ console.log("start verify structure");
66
+ _b.label = 1;
67
+ case 1:
68
+ _b.trys.push([1, 12, 13, 14]);
69
+ allEntities_1 = tslib_1.__values(context_1.allEntities), allEntities_1_1 = allEntities_1.next();
70
+ _b.label = 2;
71
+ case 2:
72
+ if (!!allEntities_1_1.done) return [3 /*break*/, 11];
73
+ entityClass = allEntities_1_1.value;
74
+ entity = remult.repo(entityClass).metadata;
75
+ return [4 /*yield*/, filter_consumer_bridge_to_sql_request_1.getDbNameProvider(entity)];
76
+ case 3:
77
+ e = _b.sent();
78
+ _b.label = 4;
79
+ case 4:
80
+ _b.trys.push([4, 9, , 10]);
81
+ if (!!entity.options.sqlExpression) return [3 /*break*/, 8];
82
+ return [4 /*yield*/, e.entityName];
83
+ case 5:
84
+ if (!((_b.sent()).toLowerCase().indexOf('from ') < 0)) return [3 /*break*/, 8];
85
+ return [4 /*yield*/, this.createIfNotExist(entity)];
86
+ case 6:
87
+ _b.sent();
88
+ return [4 /*yield*/, this.verifyAllColumns(entity)];
89
+ case 7:
90
+ _b.sent();
91
+ _b.label = 8;
92
+ case 8: return [3 /*break*/, 10];
93
+ case 9:
94
+ err_1 = _b.sent();
95
+ console.log("failed verify structure of " + e.entityName + " ", err_1);
96
+ return [3 /*break*/, 10];
97
+ case 10:
98
+ allEntities_1_1 = allEntities_1.next();
99
+ return [3 /*break*/, 2];
100
+ case 11: return [3 /*break*/, 14];
101
+ case 12:
102
+ e_1_1 = _b.sent();
103
+ e_1 = { error: e_1_1 };
104
+ return [3 /*break*/, 14];
105
+ case 13:
106
+ try {
107
+ if (allEntities_1_1 && !allEntities_1_1.done && (_a = allEntities_1.return)) _a.call(allEntities_1);
108
+ }
109
+ finally { if (e_1) throw e_1.error; }
110
+ return [7 /*endfinally*/];
111
+ case 14: return [2 /*return*/];
112
+ }
113
+ });
114
+ });
115
+ };
116
+ PostgresSchemaBuilder.prototype.createIfNotExist = function (entity) {
117
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
118
+ var c, e;
119
+ var _this = this;
120
+ return tslib_1.__generator(this, function (_a) {
121
+ switch (_a.label) {
122
+ case 0:
123
+ c = this.pool.createCommand();
124
+ return [4 /*yield*/, filter_consumer_bridge_to_sql_request_1.getDbNameProvider(entity)];
125
+ case 1:
126
+ e = _a.sent();
127
+ return [4 /*yield*/, c.execute("select 1 from information_Schema.tables where table_name=" + c.addParameterAndReturnSqlToken((e.entityName).toLowerCase()) + this.additionalWhere).then(function (r) { return tslib_1.__awaiter(_this, void 0, void 0, function () {
128
+ var result, _a, _b, x, sql;
129
+ var e_2, _c;
130
+ return tslib_1.__generator(this, function (_d) {
131
+ switch (_d.label) {
132
+ case 0:
133
+ if (!(r.rows.length == 0)) return [3 /*break*/, 2];
134
+ result = '';
135
+ try {
136
+ for (_a = tslib_1.__values(entity.fields), _b = _a.next(); !_b.done; _b = _a.next()) {
137
+ x = _b.value;
138
+ if (!e.isDbReadonly(x) || remult3_1.isAutoIncrement(x)) {
139
+ if (result.length != 0)
140
+ result += ',';
141
+ result += '\r\n ';
142
+ if (remult3_1.isAutoIncrement(x))
143
+ result += e.nameOf(x) + ' serial';
144
+ else {
145
+ result += postgresColumnSyntax(x, e.nameOf(x));
146
+ if (x == entity.idMetadata.field)
147
+ result += ' primary key';
148
+ }
149
+ }
150
+ }
151
+ }
152
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
153
+ finally {
154
+ try {
155
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
156
+ }
157
+ finally { if (e_2) throw e_2.error; }
158
+ }
159
+ sql = 'create table ' + e.entityName + ' (' + result + '\r\n)';
160
+ //console.log(sql);
161
+ return [4 /*yield*/, this.pool.execute(sql)];
162
+ case 1:
163
+ //console.log(sql);
164
+ _d.sent();
165
+ _d.label = 2;
166
+ case 2: return [2 /*return*/];
167
+ }
168
+ });
169
+ }); })];
170
+ case 2:
171
+ _a.sent();
172
+ return [2 /*return*/];
173
+ }
174
+ });
175
+ });
176
+ };
177
+ PostgresSchemaBuilder.prototype.addColumnIfNotExist = function (entity, c) {
178
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
179
+ var e, cmd, colName, sql, err_2;
180
+ return tslib_1.__generator(this, function (_a) {
181
+ switch (_a.label) {
182
+ case 0: return [4 /*yield*/, filter_consumer_bridge_to_sql_request_1.getDbNameProvider(entity)];
183
+ case 1:
184
+ e = _a.sent();
185
+ if (e.isDbReadonly(c(entity)))
186
+ return [2 /*return*/];
187
+ _a.label = 2;
188
+ case 2:
189
+ _a.trys.push([2, 6, , 7]);
190
+ cmd = this.pool.createCommand();
191
+ colName = e.nameOf(c(entity));
192
+ return [4 /*yield*/, cmd.execute("select 1 \n FROM information_schema.columns \n WHERE table_name=" + cmd.addParameterAndReturnSqlToken((e.entityName).toLocaleLowerCase()) + " and column_name=" + cmd.addParameterAndReturnSqlToken((colName).toLocaleLowerCase()) + this.additionalWhere)];
193
+ case 3:
194
+ if (!((_a.sent()).rows.length == 0)) return [3 /*break*/, 5];
195
+ sql = "alter table " + e.entityName + " add column " + postgresColumnSyntax(c(entity), colName);
196
+ //console.log(sql);
197
+ return [4 /*yield*/, this.pool.execute(sql)];
198
+ case 4:
199
+ //console.log(sql);
200
+ _a.sent();
201
+ _a.label = 5;
202
+ case 5: return [3 /*break*/, 7];
203
+ case 6:
204
+ err_2 = _a.sent();
205
+ console.log(err_2);
206
+ return [3 /*break*/, 7];
207
+ case 7: return [2 /*return*/];
208
+ }
209
+ });
210
+ });
211
+ };
212
+ PostgresSchemaBuilder.prototype.verifyAllColumns = function (entity) {
213
+ return tslib_1.__awaiter(this, void 0, void 0, function () {
214
+ var cmd, e, cols, _a, _b, col, sql, e_3_1, err_3;
215
+ var e_3, _c;
216
+ return tslib_1.__generator(this, function (_d) {
217
+ switch (_d.label) {
218
+ case 0:
219
+ _d.trys.push([0, 11, , 12]);
220
+ cmd = this.pool.createCommand();
221
+ return [4 /*yield*/, filter_consumer_bridge_to_sql_request_1.getDbNameProvider(entity)];
222
+ case 1:
223
+ e = _d.sent();
224
+ return [4 /*yield*/, cmd.execute("select column_name \n FROM information_schema.columns \n WHERE table_name=" + cmd.addParameterAndReturnSqlToken((e.entityName).toLocaleLowerCase()) + " " + this.additionalWhere)];
225
+ case 2:
226
+ cols = (_d.sent()).rows.map(function (x) { return x.column_name; });
227
+ _d.label = 3;
228
+ case 3:
229
+ _d.trys.push([3, 8, 9, 10]);
230
+ _a = tslib_1.__values(entity.fields), _b = _a.next();
231
+ _d.label = 4;
232
+ case 4:
233
+ if (!!_b.done) return [3 /*break*/, 7];
234
+ col = _b.value;
235
+ if (!!e.isDbReadonly(col)) return [3 /*break*/, 6];
236
+ if (!!cols.includes(e.nameOf(col).toLocaleLowerCase())) return [3 /*break*/, 6];
237
+ sql = "alter table " + e.entityName + " add column " + postgresColumnSyntax(col, e.nameOf(col));
238
+ //console.log(sql);
239
+ return [4 /*yield*/, this.pool.execute(sql)];
240
+ case 5:
241
+ //console.log(sql);
242
+ _d.sent();
243
+ _d.label = 6;
244
+ case 6:
245
+ _b = _a.next();
246
+ return [3 /*break*/, 4];
247
+ case 7: return [3 /*break*/, 10];
248
+ case 8:
249
+ e_3_1 = _d.sent();
250
+ e_3 = { error: e_3_1 };
251
+ return [3 /*break*/, 10];
252
+ case 9:
253
+ try {
254
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
255
+ }
256
+ finally { if (e_3) throw e_3.error; }
257
+ return [7 /*endfinally*/];
258
+ case 10: return [3 /*break*/, 12];
259
+ case 11:
260
+ err_3 = _d.sent();
261
+ console.log(err_3);
262
+ return [3 /*break*/, 12];
263
+ case 12: return [2 /*return*/];
264
+ }
265
+ });
266
+ });
267
+ };
268
+ return PostgresSchemaBuilder;
269
+ }());
270
+ exports.PostgresSchemaBuilder = PostgresSchemaBuilder;
package/readme.md CHANGED
@@ -25,51 +25,40 @@
25
25
  <img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/remultjs?style=social">
26
26
  </a>
27
27
  </div>
28
+ <hr/>
29
+
30
+ [Getting Started](#getting-started) | [Documentation](#documentation) | [Example App](#example-app)
28
31
 
29
32
  ## What is Remult?
30
33
 
31
- **Remult** is a full-stack CRUD framework that uses your TypeScript model types to provide:
34
+ **Remult** is a full-stack CRUD framework that uses your **TypeScript entities as a single source of truth for your API, frontend type-safe API client and backend ORM**.
32
35
 
33
- * Secure REST API (highly configurable)
34
- * Type-safe frontend API client
35
- * Type-safe backend query builder
36
+ ### Status
36
37
 
37
- #### Remult :heart: Code Sharing
38
+ Remult is **production-ready** and, in fact, used in production apps since 2017. However, we’re keeping the major version at zero so we can use community feedback to finalize the v1 API.
38
39
 
39
- With model types shared between frontend and backend code, Remult can **enforce data validation and constraints, defined once, both in the front-end and within back-end API routes**.
40
+ ## Motivation
40
41
 
41
- ## Getting started
42
- The best way to learn Remult is by following a tutorial of a simple Todo web app with a Node.js Express backend.
42
+ Full-stack web development is (still) too complicated. **Simple CRUD, a common requirement of any business application, should be simple to build, maintain, and extend** when the need arises.
43
43
 
44
- * [Tutorial with React](https://remult.dev/tutorials/react/)
45
- * [Tutorial with Angular](https://remult.dev/tutorials/angular/)
46
- * [Tutorial with Vue](https://remult.dev/tutorials/vue/)
47
- * [Tutorial with Next.js](https://remult.dev/tutorials/react-next/)
44
+ Remult abstracts away repetitive, boilerplate, error-prone, and poorly designed code on the one hand, and enables total flexibility and control on the other. **Remult let you write TypeScript code you can follow and safely refactor**, and fits nicely into any existing or new project by being minimalistic and completely unopinionated regarding the developer’s choice of other frameworks and tools.
45
+
46
+ Other frameworks tend to fall into either too much abstraction (no-code, low-code, BaaS) or partial abstraction (MVC frameworks, GraphQL, ORMs, API generators, code generators), and tend to be opinionated regarding the development tool-chain, deployment environment, configuration/conventions or DSL. Remult attempts to strike a better balance.
48
47
 
49
48
  ## Installation
49
+
50
+ The *remult* package is one and the same for both the frontend bundle and the backend. Install it once for a monolith project or per-repo in a monorepo.
51
+
50
52
  ```sh
51
53
  npm i remult
52
54
  ```
53
55
 
54
56
  ## Usage
55
57
 
56
- ### Setup API backend using a Node.js Express middleware
57
- ```ts
58
- import express from 'express';
59
- import { remultExpress } from 'remult/remult-express';
60
-
61
- const port = 3001;
62
- const app = express();
63
-
64
- app.use(remultExpress());
65
-
66
- app.listen(port, () => {
67
- console.log(`Example API listening at http://localhost:${port}`);
68
- });
69
- ```
70
-
71
58
  ### Define model classes
72
59
  ```ts
60
+ // shared/product.ts
61
+
73
62
  import { Entity, Fields } from 'remult';
74
63
 
75
64
  @Entity('products', {
@@ -84,6 +73,26 @@ export class Product {
84
73
  }
85
74
  ```
86
75
 
76
+ ### Setup API backend using an Express middleware
77
+ ```ts
78
+ // backend/index.ts
79
+
80
+ import express from 'express';
81
+ import { remultExpress } from 'remult/remult-express';
82
+ import { Product } from '../shared/product';
83
+
84
+ const port = 3001;
85
+ const app = express();
86
+
87
+ app.use(remultExpress({
88
+ entities: [Product]
89
+ }));
90
+
91
+ app.listen(port, () => {
92
+ console.log(`Example API listening at http://localhost:${port}`);
93
+ });
94
+ ```
95
+
87
96
  ### :rocket: API Ready
88
97
  ```sh
89
98
  > curl http://localhost:3001/api/products
@@ -93,12 +102,19 @@ export class Product {
93
102
 
94
103
  ### Find and manipulate data in type-safe frontend code
95
104
  ```ts
105
+ // frontend/code.ts
106
+
107
+ import { Remult } from 'remult';
108
+ import { Product } from '../shared/product';
109
+
110
+ const remult = new Remult();
111
+
96
112
  async function increasePriceOfTofu(priceIncrease: number) {
97
113
  const productsRepo = remult.repo(Product);
98
114
 
99
- const product = await productsRepo.findFirst({ name: 'Tofu' });
115
+ const product = await productsRepo.findFirst({ name: 'Tofu' }); // filter is passed through API request all the way to the db
100
116
  product.unitPrice += priceIncrease;
101
- productsRepo.save(product);
117
+ productsRepo.save(product); // mutation request updates the db with no boilerplate code
102
118
  }
103
119
  ```
104
120
 
@@ -108,7 +124,7 @@ async function increasePriceOfTofu(priceIncrease: number) {
108
124
  static async increasePriceOfTofu(priceIncrease: number, remult?: Remult) {
109
125
  const productsRepo = remult!.repo(Product);
110
126
 
111
- const product = await productsRepo.findFirst({ name: 'Tofu' });
127
+ const product = await productsRepo.findFirst({ name: 'Tofu' }); // use Remult in the backend as an ORM
112
128
  product.unitPrice += priceIncrease;
113
129
  productsRepo.save(product);
114
130
  }
@@ -184,6 +200,28 @@ export class Article {
184
200
  }
185
201
  ```
186
202
 
203
+ ## What about complex CRUD?
204
+
205
+ While simple CRUD shouldn’t require any backend coding, using Remult means having the ability to handle any complex scenario by controlling the backend in numerous ways:
206
+
207
+ * Backend computed (read-only) fields - from simple [expressions](https://remult.dev/docs/ref_field.html#serverexpression) to complex data lookups or even direct db access (SQL)
208
+ * Custom side-effects with [entity lifecycle hooks](https://remult.dev/docs/ref_entity.html#saving) (before/after saving/deleting)
209
+ * Backend only updatable fields (e.g. “last updated at”)
210
+ * Many-to-one [relations](https://remult.dev/docs/entity-relations.html) with [lazy/eager loading](https://remult.dev/docs/lazy-loading-of-related-entities.html)
211
+ * Roll-your-own type-safe endpoints with [Backend Methods](https://remult.dev/docs/backendMethods.html)
212
+ * Roll-your-own low-level endpoints (Express, Fastify, koa, others…)
213
+
214
+ ## Getting started
215
+ The best way to learn Remult is by following a tutorial of a simple Todo web app with a Node.js Express backend.
216
+
217
+ * [Tutorial with React](https://remult.dev/tutorials/react/)
218
+ * [Tutorial with Angular](https://remult.dev/tutorials/angular/)
219
+ * [Tutorial with Vue](https://remult.dev/tutorials/vue/)
220
+ * [Tutorial with Next.js](https://remult.dev/tutorials/react-next/)
221
+
222
+ ## Documentation
223
+ The [documentation](https://remult.dev/docs) covers the main features of Remult. However, it is still a work-in-progress.
224
+
187
225
  ## Example App
188
226
 
189
227
  [CRM demo](https://github.com/remult/crm-demo) with a React + [MUI](https://mui.com) front-end and Postgres database.
@@ -109,15 +109,16 @@ var WebSqlDataProvider = /** @class */ (function () {
109
109
  };
110
110
  WebSqlDataProvider.prototype.addColumnSqlSyntax = function (x, dbName) {
111
111
  var result = dbName;
112
+ var nullNumber = x.allowNull ? "" : " default 0 not null";
112
113
  if (x.valueType == Date)
113
114
  result += " integer";
114
115
  else if (x.valueType == Boolean)
115
- result += " integer default 0 not null";
116
+ result += " integer " + nullNumber;
116
117
  else if (x.valueType == Number) {
117
118
  if (!x.valueConverter.fieldTypeInDb)
118
- result += ' real default 0 not null';
119
+ result += ' real ' + nullNumber;
119
120
  else
120
- result += ' ' + x.valueConverter.fieldTypeInDb + ' default 0 not null';
121
+ result += ' ' + x.valueConverter.fieldTypeInDb + ' ' + nullNumber;
121
122
  }
122
123
  else
123
124
  result += " text" + (x.allowNull ? " " : " default '' not null ");
package/src/entity.d.ts CHANGED
@@ -68,8 +68,15 @@ export interface EntityOptions<entityType = any> {
68
68
  dbName?: string;
69
69
  /** For entities that are based on SQL expressions instead of a physical table or view*/
70
70
  sqlExpression?: string | ((entity: FieldsMetadata<entityType>) => string | Promise<string>);
71
- /** An arrow function that identifies the `id` column to use for this entity */
72
- id?: (entity: FieldsMetadata<entityType>) => FieldMetadata;
71
+ /** An arrow function that identifies the `id` column to use for this entity
72
+ * @example
73
+ * //Single column id
74
+ * @Entity<Products>("products", { id:p=>p.productCode })
75
+ * @example
76
+ * //Multiple columns id
77
+ * @Entity<OrderDetails>("orderDetails", { id:od=> [od.orderId, od.productCode] })
78
+ */
79
+ id?: (entity: FieldsMetadata<entityType>) => FieldMetadata | FieldMetadata[];
73
80
  entityRefInit?: (ref: EntityRef<entityType>, row: entityType) => void;
74
81
  apiRequireId?: Allowed;
75
82
  }
@@ -2181,7 +2181,15 @@ var EntityFullInfo = /** @class */ (function () {
2181
2181
  this.fields = r;
2182
2182
  this.caption = buildCaption(entityInfo.caption, this.key, remult);
2183
2183
  if (entityInfo.id) {
2184
- this.idMetadata.field = entityInfo.id(this.fields);
2184
+ var r_2 = entityInfo.id(this.fields);
2185
+ if (Array.isArray(r_2)) {
2186
+ if (r_2.length > 1)
2187
+ this.idMetadata.field = new (column_1.CompoundIdField.bind.apply(column_1.CompoundIdField, tslib_1.__spread([void 0], r_2)))();
2188
+ else if (r_2.length == 1)
2189
+ this.idMetadata.field = r_2[0];
2190
+ }
2191
+ else
2192
+ this.idMetadata.field = r_2;
2185
2193
  }
2186
2194
  else {
2187
2195
  if (this.fields["id"])
@@ -2331,8 +2339,8 @@ var Fields = /** @class */ (function () {
2331
2339
  }());
2332
2340
  exports.Fields = Fields;
2333
2341
  function isAutoIncrement(f) {
2334
- var _a;
2335
- return ((_a = f.options.valueConverter) === null || _a === void 0 ? void 0 : _a.fieldTypeInDb) === 'autoincrement';
2342
+ var _a, _b;
2343
+ return ((_b = (_a = f.options) === null || _a === void 0 ? void 0 : _a.valueConverter) === null || _b === void 0 ? void 0 : _b.fieldTypeInDb) === 'autoincrement';
2336
2344
  }
2337
2345
  exports.isAutoIncrement = isAutoIncrement;
2338
2346
  function ValueListFieldType() {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  var tslib_1 = require("tslib");
4
+ var sql_database_1 = require("../data-providers/sql-database");
4
5
  var remult3_1 = require("../remult3");
5
6
  var db_tests_setup_1 = require("./db-tests-setup");
6
7
  var testNull = /** @class */ (function (_super) {
@@ -8,6 +9,7 @@ var testNull = /** @class */ (function (_super) {
8
9
  function testNull() {
9
10
  var _this = _super !== null && _super.apply(this, arguments) || this;
10
11
  _this.id = 0;
12
+ _this.numberWithNull = null;
11
13
  return _this;
12
14
  }
13
15
  tslib_1.__decorate([
@@ -18,6 +20,10 @@ var testNull = /** @class */ (function (_super) {
18
20
  remult3_1.Field(undefined, { allowNull: true }),
19
21
  tslib_1.__metadata("design:type", Object)
20
22
  ], testNull.prototype, "myCol", void 0);
23
+ tslib_1.__decorate([
24
+ remult3_1.Fields.integer({ allowNull: true }),
25
+ tslib_1.__metadata("design:type", Number)
26
+ ], testNull.prototype, "numberWithNull", void 0);
21
27
  testNull = tslib_1.__decorate([
22
28
  remult3_1.Entity('testNull', { allowApiCrud: true })
23
29
  ], testNull);
@@ -49,3 +55,35 @@ db_tests_setup_1.testAll("test that update null works", function (_a) {
49
55
  });
50
56
  });
51
57
  }, false);
58
+ db_tests_setup_1.testAll("test number with null", function (_a) {
59
+ var createEntity = _a.createEntity;
60
+ return tslib_1.__awaiter(void 0, void 0, void 0, function () {
61
+ var r, i, _b, _c;
62
+ return tslib_1.__generator(this, function (_d) {
63
+ switch (_d.label) {
64
+ case 0: return [4 /*yield*/, createEntity(testNull)];
65
+ case 1:
66
+ r = _d.sent();
67
+ return [4 /*yield*/, r.insert({ id: 1, myCol: { value: 'abc' }, numberWithNull: null })];
68
+ case 2:
69
+ i = _d.sent();
70
+ expect(i.numberWithNull).toBe(null);
71
+ _b = expect;
72
+ return [4 /*yield*/, r.count({ numberWithNull: null })];
73
+ case 3:
74
+ _b.apply(void 0, [_d.sent()]).toBe(1);
75
+ i.numberWithNull = 0;
76
+ return [4 /*yield*/, i.save()];
77
+ case 4:
78
+ _d.sent();
79
+ expect(i.numberWithNull).toBe(0);
80
+ sql_database_1.SqlDatabase.LogToConsole = true;
81
+ _c = expect;
82
+ return [4 /*yield*/, r.count({ numberWithNull: null })];
83
+ case 5:
84
+ _c.apply(void 0, [_d.sent()]).toBe(0);
85
+ return [2 /*return*/];
86
+ }
87
+ });
88
+ });
89
+ }, false);
@@ -86,6 +86,8 @@ var ValueConverters = /** @class */ (function () {
86
86
  return undefined;
87
87
  return val.replace(/-/g, '');
88
88
  }, fromDb: function (val) {
89
+ if (val === null)
90
+ return null;
89
91
  if (!val)
90
92
  return undefined;
91
93
  return new Date(val.substring(0, 4) + '-' + val.substring(4, 6) + '-' + val.substring(6, 8));
@@ -110,6 +112,8 @@ var ValueConverters = /** @class */ (function () {
110
112
  };
111
113
  ValueConverters.Number = {
112
114
  fromDb: function (value) {
115
+ if (value === null)
116
+ return null;
113
117
  if (value !== undefined)
114
118
  return +value;
115
119
  return undefined;