remult 3.4.0-next.5 → 3.4.0-next.7
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/SseSubscriptionServer.d.ts +2 -0
- package/SseSubscriptionServer.js +40 -0
- package/esm/SseSubscriptionServer.js +13 -0
- package/esm/remult-mongo.js +18 -12
- package/esm/server/remult-api-server.js +15 -14
- package/esm/src/data-api.js +6 -6
- package/esm/src/data-providers/sql-database.js +165 -106
- package/esm/src/remult3/Fields.js +12 -2
- package/esm/src/remult3/RepositoryImplementation.js +20 -2
- package/index.d.ts +1 -0
- package/internals.d.ts +2 -0
- package/package.json +1 -1
- package/remult-mongo.js +23 -16
- package/server/remult-api-server.d.ts +12 -0
- package/server/remult-api-server.js +19 -39
- package/src/column-interfaces.d.ts +5 -1
- package/src/data-api.d.ts +2 -5
- package/src/data-api.js +80 -69
- package/src/data-providers/sql-database.d.ts +46 -2
- package/src/data-providers/sql-database.js +337 -215
- package/src/live-query/SubscriptionServer.d.ts +4 -0
- package/src/remult3/Fields.js +6 -0
- package/src/remult3/RepositoryImplementation.d.ts +3 -0
- package/src/remult3/RepositoryImplementation.js +19 -2
- package/src/remult3/repository-internals.d.ts +4 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { GenericResponse } from './server/index.js';
|
|
2
|
+
import type { SubscriptionServerRouteApi } from './server/remult-api-server.js';
|
|
2
3
|
import type { Remult } from './src/context.js';
|
|
3
4
|
import type { SubscriptionServer } from './src/live-query/SubscriptionServer.js';
|
|
4
5
|
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
5
6
|
export declare const ssePingMs = 15000;
|
|
6
7
|
export declare class SseSubscriptionServer implements SubscriptionServer {
|
|
7
8
|
private canUserConnectToChannel?;
|
|
9
|
+
initApiServer(api: SubscriptionServerRouteApi): void;
|
|
8
10
|
constructor(canUserConnectToChannel?: ((channel: string, remult: Remult) => boolean) | undefined);
|
|
9
11
|
publishMessage<T>(channel: string, message: any): Promise<void>;
|
|
10
12
|
}
|
package/SseSubscriptionServer.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.clientConnection = exports.SseSubscriptionServer = exports.ssePingMs = void 0;
|
|
4
4
|
var tslib_1 = require("tslib");
|
|
5
5
|
var SseSubscriptionClient_js_1 = require("./src/live-query/SseSubscriptionClient.js");
|
|
6
|
+
var SubscriptionChannel_js_1 = require("./src/live-query/SubscriptionChannel.js");
|
|
6
7
|
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
7
8
|
exports.ssePingMs = 15000;
|
|
8
9
|
var SseSubscriptionServer = /** @class */ (function () {
|
|
@@ -18,6 +19,45 @@ var SseSubscriptionServer = /** @class */ (function () {
|
|
|
18
19
|
this.canUserConnectToChannel = function () { return true; };
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
SseSubscriptionServer.prototype.initApiServer = function (api) {
|
|
23
|
+
var _this = this;
|
|
24
|
+
var streamPath = '/' + SubscriptionChannel_js_1.streamUrl;
|
|
25
|
+
api.addRoute(streamPath, 'get', function (_a) { return tslib_1.__awaiter(_this, [_a], void 0, function (_b) {
|
|
26
|
+
var req = _b.req, origRes = _b.origRes;
|
|
27
|
+
return tslib_1.__generator(this, function (_c) {
|
|
28
|
+
this.openHttpServerStream(req, origRes);
|
|
29
|
+
return [2 /*return*/];
|
|
30
|
+
});
|
|
31
|
+
}); });
|
|
32
|
+
api.addRoute(streamPath + '/subscribe', 'post', function (_a) { return tslib_1.__awaiter(_this, [_a], void 0, function (_b) {
|
|
33
|
+
var _c;
|
|
34
|
+
var remult = _b.remult, res = _b.res, getBody = _b.getBody;
|
|
35
|
+
return tslib_1.__generator(this, function (_d) {
|
|
36
|
+
switch (_d.label) {
|
|
37
|
+
case 0:
|
|
38
|
+
_c = this.subscribeToChannel;
|
|
39
|
+
return [4 /*yield*/, getBody()];
|
|
40
|
+
case 1:
|
|
41
|
+
_c.apply(this, [_d.sent(), res, remult]);
|
|
42
|
+
return [2 /*return*/];
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}); });
|
|
46
|
+
api.addRoute(streamPath + '/unsubscribe', 'post', function (_a) { return tslib_1.__awaiter(_this, [_a], void 0, function (_b) {
|
|
47
|
+
var _c;
|
|
48
|
+
var remult = _b.remult, res = _b.res, getBody = _b.getBody;
|
|
49
|
+
return tslib_1.__generator(this, function (_d) {
|
|
50
|
+
switch (_d.label) {
|
|
51
|
+
case 0:
|
|
52
|
+
_c = this.subscribeToChannel;
|
|
53
|
+
return [4 /*yield*/, getBody()];
|
|
54
|
+
case 1:
|
|
55
|
+
_c.apply(this, [_d.sent(), res, remult, true]);
|
|
56
|
+
return [2 /*return*/];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}); });
|
|
60
|
+
};
|
|
21
61
|
//@internal
|
|
22
62
|
SseSubscriptionServer.prototype.subscribeToChannel = function (_a, res, remult, remove) {
|
|
23
63
|
var e_1, _b;
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { ConnectionNotFoundError } from './src/live-query/SseSubscriptionClient.js';
|
|
2
|
+
import { streamUrl } from './src/live-query/SubscriptionChannel.js';
|
|
2
3
|
/** Keep below typical proxy idle (30s) and Heroku H15 (55s). */
|
|
3
4
|
export const ssePingMs = 15_000;
|
|
4
5
|
export class SseSubscriptionServer {
|
|
5
6
|
canUserConnectToChannel;
|
|
7
|
+
initApiServer(api) {
|
|
8
|
+
const streamPath = '/' + streamUrl;
|
|
9
|
+
api.addRoute(streamPath, 'get', async ({ req, origRes }) => {
|
|
10
|
+
this.openHttpServerStream(req, origRes);
|
|
11
|
+
});
|
|
12
|
+
api.addRoute(streamPath + '/subscribe', 'post', async ({ remult, res, getBody }) => {
|
|
13
|
+
this.subscribeToChannel(await getBody(), res, remult);
|
|
14
|
+
});
|
|
15
|
+
api.addRoute(streamPath + '/unsubscribe', 'post', async ({ remult, res, getBody }) => {
|
|
16
|
+
this.subscribeToChannel(await getBody(), res, remult, true);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
6
19
|
//@internal
|
|
7
20
|
subscribeToChannel({ channel, clientId }, res, remult, remove = false) {
|
|
8
21
|
for (const c of this.connections) {
|
package/esm/remult-mongo.js
CHANGED
|
@@ -119,6 +119,8 @@ class MongoEntityDataProvider {
|
|
|
119
119
|
translateToDb(row, nameProvider) {
|
|
120
120
|
let result = {};
|
|
121
121
|
for (const col of this.entity.fields) {
|
|
122
|
+
if (col.dbReadOnly || col.isServerExpression)
|
|
123
|
+
continue;
|
|
122
124
|
let val = toDb(col, row[col.key]);
|
|
123
125
|
if (val === null)
|
|
124
126
|
val = NULL;
|
|
@@ -255,7 +257,7 @@ class MongoEntityDataProvider {
|
|
|
255
257
|
for (const f of this.entity.fields) {
|
|
256
258
|
if (!f.dbReadOnly && !f.isServerExpression) {
|
|
257
259
|
if (keys.includes(f.key)) {
|
|
258
|
-
newR[f
|
|
260
|
+
newR[e.$dbNameOf(f)] = toDb(f, data[f.key]);
|
|
259
261
|
}
|
|
260
262
|
}
|
|
261
263
|
}
|
|
@@ -386,23 +388,27 @@ class FilterConsumerBridgeToMongo {
|
|
|
386
388
|
this.add(col, val, '$lt');
|
|
387
389
|
}
|
|
388
390
|
containsCaseInsensitive(col, val) {
|
|
389
|
-
this.
|
|
391
|
+
this.addRegex(col, val);
|
|
390
392
|
}
|
|
391
393
|
notContainsCaseInsensitive(col, val) {
|
|
392
|
-
this.
|
|
393
|
-
[this.nameProvider.$dbNameOf(col)]: {
|
|
394
|
-
$not: {
|
|
395
|
-
$regex: isNull(val) ? val : toDb(col, val),
|
|
396
|
-
$options: 'i',
|
|
397
|
-
},
|
|
398
|
-
},
|
|
399
|
-
}));
|
|
394
|
+
this.addRegex(col, val, '', '', true);
|
|
400
395
|
}
|
|
401
396
|
startsWithCaseInsensitive(col, val) {
|
|
402
|
-
this.
|
|
397
|
+
this.addRegex(col, val, '^');
|
|
403
398
|
}
|
|
404
399
|
endsWithCaseInsensitive(col, val) {
|
|
405
|
-
this.
|
|
400
|
+
this.addRegex(col, val, '', '$');
|
|
401
|
+
}
|
|
402
|
+
addRegex(col, val, start = '', end = '', negate = false) {
|
|
403
|
+
this.result.push(() => {
|
|
404
|
+
const regex = {
|
|
405
|
+
$regex: start + toDb(col, val).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + end,
|
|
406
|
+
$options: 'i',
|
|
407
|
+
};
|
|
408
|
+
return {
|
|
409
|
+
[this.nameProvider.$dbNameOf(col)]: negate ? { $not: regex } : regex,
|
|
410
|
+
};
|
|
411
|
+
});
|
|
406
412
|
}
|
|
407
413
|
add(col, val, operator, moreOptions) {
|
|
408
414
|
this.result.push(() => ({
|
|
@@ -2,7 +2,7 @@ import { __decorate, __metadata } from "tslib";
|
|
|
2
2
|
import { SseSubscriptionServer } from '../SseSubscriptionServer.js';
|
|
3
3
|
import { Remult, RemultAsyncLocalStorage, withRemult } from '../src/context.js';
|
|
4
4
|
import { DataApi, serializeError } from '../src/data-api.js';
|
|
5
|
-
import { liveQueryKeepAliveRoute
|
|
5
|
+
import { liveQueryKeepAliveRoute } from '../src/live-query/SubscriptionChannel.js';
|
|
6
6
|
import { InMemoryLiveQueryStorage, LiveQueryPublisher, } from '../src/live-query/SubscriptionServer.js';
|
|
7
7
|
import { IdEntity } from '../src/remult3/IdEntity.js';
|
|
8
8
|
import { Fields } from '../src/remult3/Fields.js';
|
|
@@ -266,19 +266,20 @@ export class RemultServerImplementation {
|
|
|
266
266
|
r.route(this.options.rootPath + '/admin').get(admin());
|
|
267
267
|
}
|
|
268
268
|
r.route(this.options.rootPath + '/me').get(this.process(async (remult, req, res) => res.success(remult.user ?? null)));
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
269
|
+
const subscriptionServer = this.options.subscriptionServer;
|
|
270
|
+
if (subscriptionServer?.initApiServer) {
|
|
271
|
+
subscriptionServer.initApiServer({
|
|
272
|
+
rootPath: this.options.rootPath,
|
|
273
|
+
addRoute: (relativePath, method, handler) => {
|
|
274
|
+
r.route(this.options.rootPath + relativePath)[method](this.process(async (remult, _, res, req, origRes, origReq) => handler({
|
|
275
|
+
remult,
|
|
276
|
+
res,
|
|
277
|
+
req,
|
|
278
|
+
origRes,
|
|
279
|
+
getBody: () => this.coreOptions.getRequestBody(origReq),
|
|
280
|
+
})));
|
|
281
|
+
},
|
|
282
|
+
});
|
|
282
283
|
}
|
|
283
284
|
r.route(this.options.rootPath + '/' + liveQueryKeepAliveRoute).post(this.process(async (remult, req, res, reqInfo, origRes, origReq) => {
|
|
284
285
|
const body = await this.coreOptions.getRequestBody(origReq);
|
package/esm/src/data-api.js
CHANGED
|
@@ -12,7 +12,7 @@ export class DataApi {
|
|
|
12
12
|
this.repository = repository;
|
|
13
13
|
this.remult = remult;
|
|
14
14
|
}
|
|
15
|
-
httpGet(res, req, serializeContext) {
|
|
15
|
+
async httpGet(res, req, serializeContext) {
|
|
16
16
|
try {
|
|
17
17
|
const action = req?.get('__action');
|
|
18
18
|
if (action?.startsWith(liveQueryAction))
|
|
@@ -22,13 +22,13 @@ export class DataApi {
|
|
|
22
22
|
case 'count':
|
|
23
23
|
return this.count(res, req, undefined);
|
|
24
24
|
case 'groupBy':
|
|
25
|
-
return res.success(this.groupBy(req, undefined));
|
|
25
|
+
return res.success(await this.groupBy(req, undefined));
|
|
26
26
|
}
|
|
27
27
|
return this.getArray(res, req, undefined);
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
30
30
|
if (err.isForbiddenError)
|
|
31
|
-
res.forbidden(err);
|
|
31
|
+
res.forbidden(err.message);
|
|
32
32
|
else
|
|
33
33
|
res.error(err, this.repository.metadata);
|
|
34
34
|
}
|
|
@@ -70,7 +70,7 @@ export class DataApi {
|
|
|
70
70
|
res.success('ok');
|
|
71
71
|
return;
|
|
72
72
|
case 'query':
|
|
73
|
-
return
|
|
73
|
+
return this.query(res, req, body);
|
|
74
74
|
default:
|
|
75
75
|
return res.created(await this.post(body, req));
|
|
76
76
|
}
|
|
@@ -124,10 +124,10 @@ export class DataApi {
|
|
|
124
124
|
this.getArrayImpl(request, rest),
|
|
125
125
|
this.groupBy(aggregateRequest, { ...aggregate, where: body.where }),
|
|
126
126
|
]);
|
|
127
|
-
return {
|
|
127
|
+
return response.success({
|
|
128
128
|
items: r,
|
|
129
129
|
aggregates,
|
|
130
|
-
};
|
|
130
|
+
});
|
|
131
131
|
}
|
|
132
132
|
catch (err) {
|
|
133
133
|
if (err.isForbiddenError)
|
|
@@ -159,6 +159,34 @@ export class SqlDatabase {
|
|
|
159
159
|
await (await getRepositoryInternals(r)._translateWhereToFilter(condition)).__applyToConsumer(b);
|
|
160
160
|
return await b.resolveWhere();
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Builds the select that `repo.groupBy` would run, without running it - for a statement remult cannot run on its own, such as one `UNION ALL` over several databases.
|
|
164
|
+
* @returns `sql`, and `toResult`, which maps a result row (read by column alias) to the shape `groupBy` returns.
|
|
165
|
+
* @see [Building a select with remult](https://remult.dev/docs/running-sql-on-the-server#building-a-select-with-remult)
|
|
166
|
+
*/
|
|
167
|
+
static async groupByToRaw(repo, options, sql) {
|
|
168
|
+
const r = getRepository(repo);
|
|
169
|
+
const built = await buildGroupBySql(await getRepositoryInternals(r).__buildGroupByOptions(options), sql?.dbNames ??
|
|
170
|
+
(await dbNamesOfWithForceSqlExpression(r.metadata, sql?.wrapIdentifier ?? wrapIdentifierOf(r))), sql?.sqlCommand ?? new myDummySQLCommand(), false, sql?.limitSyntax ?? limitSyntaxRequired);
|
|
171
|
+
return {
|
|
172
|
+
sql: built.select,
|
|
173
|
+
toResult: (row) => built.toResult((_, alias) => row[alias]),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Builds the select that `repo.find` would run, without running it. Columns are aliased by field key; no default order is added, so the sql can be a branch of a `UNION`.
|
|
178
|
+
* @returns `sql`, and `toResult`, which maps a result row (read by column alias) to the fields' values.
|
|
179
|
+
* @see [Building a select with remult](https://remult.dev/docs/running-sql-on-the-server#building-a-select-with-remult)
|
|
180
|
+
*/
|
|
181
|
+
static async selectToRaw(repo, options, sql) {
|
|
182
|
+
const r = getRepository(repo);
|
|
183
|
+
const built = await buildFindSql(r.metadata, await getRepositoryInternals(r)._buildEntityDataProviderFindOptions(options ?? {}), sql?.dbNames ??
|
|
184
|
+
(await dbNamesOfWithForceSqlExpression(r.metadata, sql?.wrapIdentifier ?? wrapIdentifierOf(r))), sql?.sqlCommand ?? new myDummySQLCommand(), false, sql?.limitSyntax ?? limitSyntaxRequired, { aliasColumns: true, defaultOrderBy: false });
|
|
185
|
+
return {
|
|
186
|
+
sql: built.select,
|
|
187
|
+
toResult: (row) => built.toResult((_, alias) => row[alias]),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
162
190
|
/**
|
|
163
191
|
* `false` _(default)_ - No logging
|
|
164
192
|
*
|
|
@@ -323,105 +351,17 @@ class ActualSQLEntityDataProvider {
|
|
|
323
351
|
return await groupByImpl(options, await this.init(), this.sql.createCommand(), this.sql._getSourceSql().orderByNullsFirst, this.sql._getSourceSql().getLimitSqlSyntax);
|
|
324
352
|
}
|
|
325
353
|
async find(options) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (options) {
|
|
332
|
-
if (options.where) {
|
|
333
|
-
let where = new FilterConsumerBridgeToSqlRequest(r, e);
|
|
334
|
-
options.where.__applyToConsumer(where);
|
|
335
|
-
select += await where.resolveWhere();
|
|
336
|
-
}
|
|
337
|
-
if (options.limit) {
|
|
338
|
-
options.orderBy = Sort.createUniqueSort(this.entity, options.orderBy);
|
|
339
|
-
}
|
|
340
|
-
if (!options.orderBy) {
|
|
341
|
-
options.orderBy = Sort.createUniqueSort(this.entity, new Sort());
|
|
342
|
-
}
|
|
343
|
-
if (options.orderBy) {
|
|
344
|
-
let first = true;
|
|
345
|
-
let segs = [];
|
|
346
|
-
for (const s of options.orderBy.Segments) {
|
|
347
|
-
segs.push(s);
|
|
348
|
-
}
|
|
349
|
-
for (const c of segs) {
|
|
350
|
-
if (first) {
|
|
351
|
-
select += ' Order By ';
|
|
352
|
-
first = false;
|
|
353
|
-
}
|
|
354
|
-
else
|
|
355
|
-
select += ', ';
|
|
356
|
-
select += c.field.options.sqlExpression
|
|
357
|
-
? c.field.options.key
|
|
358
|
-
: await e.$dbNameOf(c.field);
|
|
359
|
-
if (c.isDescending)
|
|
360
|
-
select += ' desc';
|
|
361
|
-
if (this.sql._getSourceSql().orderByNullsFirst) {
|
|
362
|
-
if (c.isDescending)
|
|
363
|
-
select += ' nulls last';
|
|
364
|
-
else
|
|
365
|
-
select += ' nulls first';
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (options.limit) {
|
|
370
|
-
let page = 1;
|
|
371
|
-
if (options.page)
|
|
372
|
-
page = options.page;
|
|
373
|
-
if (page < 1)
|
|
374
|
-
page = 1;
|
|
375
|
-
select +=
|
|
376
|
-
' ' +
|
|
377
|
-
this.strategy.getLimitSqlSyntax(options.limit, (page - 1) * options.limit);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return r.execute(select).then((r) => {
|
|
381
|
-
return r.rows.map((y) => {
|
|
382
|
-
return this.buildResultRow(colKeys, y, r);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
354
|
+
const e = await this.init();
|
|
355
|
+
const r = this.sql.createCommand();
|
|
356
|
+
const { select, toResult } = await buildFindSql(this.entity, options, e, r, this.sql._getSourceSql().orderByNullsFirst, (limit, offset) => this.strategy.getLimitSqlSyntax(limit, offset));
|
|
357
|
+
const result = await r.execute(select);
|
|
358
|
+
return result.rows.map((y) => toResult((i) => y[result.getColumnKeyInResultForIndexInSelect(i)]));
|
|
385
359
|
}
|
|
386
360
|
buildResultRow(colKeys, y, r) {
|
|
387
|
-
|
|
388
|
-
for (let index = 0; index < colKeys.length; index++) {
|
|
389
|
-
const col = colKeys[index];
|
|
390
|
-
try {
|
|
391
|
-
result[col.key] = col.valueConverter.fromDb(y[r.getColumnKeyInResultForIndexInSelect(index)]);
|
|
392
|
-
}
|
|
393
|
-
catch (err) {
|
|
394
|
-
throw new Error('Failed to load from db:' + col.key + '\r\n' + err);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return result;
|
|
361
|
+
return resultRow(colKeys, (i) => y[r.getColumnKeyInResultForIndexInSelect(i)]);
|
|
398
362
|
}
|
|
399
363
|
async buildSelect(e, r, selectedFields, args) {
|
|
400
|
-
|
|
401
|
-
let colKeys = [];
|
|
402
|
-
for (const x of this.entity.fields) {
|
|
403
|
-
if (selectedFields && !selectedFields.includes(x.key))
|
|
404
|
-
continue;
|
|
405
|
-
if (x.isServerExpression) {
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
if (colKeys.length > 0)
|
|
409
|
-
select += ', ';
|
|
410
|
-
if (typeof x.options.sqlExpression === 'function') {
|
|
411
|
-
let sql = await x[originalSqlExpressionKey](this.entity, args, r);
|
|
412
|
-
if (sql.includes(' '))
|
|
413
|
-
select += '(' + sql + ')';
|
|
414
|
-
else
|
|
415
|
-
select += sql;
|
|
416
|
-
}
|
|
417
|
-
else
|
|
418
|
-
select += e.$dbNameOf(x);
|
|
419
|
-
if (x.options.sqlExpression)
|
|
420
|
-
select += ' as ' + x.key;
|
|
421
|
-
colKeys.push(x);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return { colKeys, select };
|
|
364
|
+
return selectColumns(this.entity, e, r, selectedFields, args, false);
|
|
425
365
|
}
|
|
426
366
|
async update(id, data, options) {
|
|
427
367
|
let e = await this.init();
|
|
@@ -573,6 +513,16 @@ class ActualSQLEntityDataProvider {
|
|
|
573
513
|
});
|
|
574
514
|
}
|
|
575
515
|
}
|
|
516
|
+
/** The quoting of the repository's own data provider, so a raw select matches the SQL remult would run against it. */
|
|
517
|
+
function wrapIdentifierOf(r) {
|
|
518
|
+
const dp = getRepositoryInternals(r)._dataProvider;
|
|
519
|
+
if (isOfType(dp, 'wrapIdentifier'))
|
|
520
|
+
return dp.wrapIdentifier?.bind(dp);
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
function limitSyntaxRequired() {
|
|
524
|
+
throw new Error('A raw select with a limit needs limitSyntax, for example SqlDatabase.getDb()._getSourceSql().getLimitSqlSyntax');
|
|
525
|
+
}
|
|
576
526
|
class myDummySQLCommand {
|
|
577
527
|
execute(sql) {
|
|
578
528
|
throw new Error('Method not implemented.');
|
|
@@ -609,8 +559,116 @@ export function getRowAfterUpdate(meta, dataProvider, data, id, operation) {
|
|
|
609
559
|
return r[0];
|
|
610
560
|
});
|
|
611
561
|
}
|
|
562
|
+
/** The select that `find` runs, and the mapping of one result row to field values. With `aliasColumns`, every column is aliased through `wrapIdentifier`, so a row can be read by alias as well as by position. */
|
|
563
|
+
export async function buildFindSql(entity, options, e, r, orderByNullsFirst, limitSyntax, { aliasColumns = false, defaultOrderBy = true } = {}) {
|
|
564
|
+
const { colKeys, select: columns } = await selectColumns(entity, e, r, options?.select, options?.args, aliasColumns);
|
|
565
|
+
let select = 'select ' + columns + '\n from ' + e.$entityName;
|
|
566
|
+
if (options) {
|
|
567
|
+
if (options.where) {
|
|
568
|
+
let where = new FilterConsumerBridgeToSqlRequest(r, e);
|
|
569
|
+
options.where.__applyToConsumer(where);
|
|
570
|
+
select += await where.resolveWhere();
|
|
571
|
+
}
|
|
572
|
+
if (options.limit) {
|
|
573
|
+
options.orderBy = Sort.createUniqueSort(entity, options.orderBy);
|
|
574
|
+
}
|
|
575
|
+
if (!options.orderBy && defaultOrderBy) {
|
|
576
|
+
options.orderBy = Sort.createUniqueSort(entity, new Sort());
|
|
577
|
+
}
|
|
578
|
+
if (options.orderBy) {
|
|
579
|
+
let first = true;
|
|
580
|
+
for (const c of options.orderBy.Segments) {
|
|
581
|
+
if (first) {
|
|
582
|
+
select += ' Order By ';
|
|
583
|
+
first = false;
|
|
584
|
+
}
|
|
585
|
+
else
|
|
586
|
+
select += ', ';
|
|
587
|
+
select += c.field.options.sqlExpression
|
|
588
|
+
? c.field.options.key
|
|
589
|
+
: await e.$dbNameOf(c.field);
|
|
590
|
+
if (c.isDescending)
|
|
591
|
+
select += ' desc';
|
|
592
|
+
if (orderByNullsFirst) {
|
|
593
|
+
if (c.isDescending)
|
|
594
|
+
select += ' nulls last';
|
|
595
|
+
else
|
|
596
|
+
select += ' nulls first';
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (options.limit) {
|
|
601
|
+
let page = 1;
|
|
602
|
+
if (options.page)
|
|
603
|
+
page = options.page;
|
|
604
|
+
if (page < 1)
|
|
605
|
+
page = 1;
|
|
606
|
+
select += ' ' + limitSyntax(options.limit, (page - 1) * options.limit);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
select,
|
|
611
|
+
colKeys,
|
|
612
|
+
/** `valueAt` reads one column of a result row, by its position in the select or by its alias (the field key). */
|
|
613
|
+
toResult(valueAt) {
|
|
614
|
+
return resultRow(colKeys, valueAt);
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
async function selectColumns(entity, e, r, selectedFields, args, aliasColumns) {
|
|
619
|
+
let select = '';
|
|
620
|
+
let colKeys = [];
|
|
621
|
+
for (const x of entity.fields) {
|
|
622
|
+
if (selectedFields && !selectedFields.includes(x.key))
|
|
623
|
+
continue;
|
|
624
|
+
if (x.isServerExpression) {
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
if (colKeys.length > 0)
|
|
628
|
+
select += ', ';
|
|
629
|
+
if (typeof x.options.sqlExpression === 'function') {
|
|
630
|
+
let sql = await x[originalSqlExpressionKey](entity, args, r);
|
|
631
|
+
if (sql.includes(' '))
|
|
632
|
+
select += '(' + sql + ')';
|
|
633
|
+
else
|
|
634
|
+
select += sql;
|
|
635
|
+
}
|
|
636
|
+
else
|
|
637
|
+
select += e.$dbNameOf(x);
|
|
638
|
+
if (aliasColumns)
|
|
639
|
+
select += ' as ' + e.wrapIdentifier(x.key);
|
|
640
|
+
else if (x.options.sqlExpression)
|
|
641
|
+
select += ' as ' + x.key;
|
|
642
|
+
colKeys.push(x);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { colKeys, select };
|
|
646
|
+
}
|
|
647
|
+
function resultRow(colKeys, valueAt) {
|
|
648
|
+
let result = {};
|
|
649
|
+
colKeys.forEach((col, index) => {
|
|
650
|
+
try {
|
|
651
|
+
result[col.key] = col.valueConverter.fromDb(valueAt(index, col.key));
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
throw new Error('Failed to load from db:' + col.key + '\r\n' + err);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
612
659
|
export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax) {
|
|
613
|
-
|
|
660
|
+
const { select, toResult } = await buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax);
|
|
661
|
+
const result = await r.execute(select);
|
|
662
|
+
return result.rows.map((sql) => toResult((i) => sql[result.getColumnKeyInResultForIndexInSelect(i)]));
|
|
663
|
+
}
|
|
664
|
+
/** The select that `groupBy` runs, and the mapping of one result row back to a `GroupByResult`. Every column is aliased through `wrapIdentifier`, so a row can be read by alias as well as by position. */
|
|
665
|
+
export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax) {
|
|
666
|
+
const aliases = [];
|
|
667
|
+
function alias(name) {
|
|
668
|
+
aliases.push(name);
|
|
669
|
+
return e.wrapIdentifier(name);
|
|
670
|
+
}
|
|
671
|
+
let select = 'select count(*) as ' + alias('count');
|
|
614
672
|
let groupBy = '';
|
|
615
673
|
const groupByCols = [];
|
|
616
674
|
const processResultRow = [];
|
|
@@ -622,9 +680,7 @@ export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax)
|
|
|
622
680
|
if (x.isServerExpression) {
|
|
623
681
|
}
|
|
624
682
|
else {
|
|
625
|
-
select += ', ' + e.$dbNameOf(x);
|
|
626
|
-
if (x.options.sqlExpression)
|
|
627
|
-
select += ' as ' + x.key;
|
|
683
|
+
select += ', ' + e.$dbNameOf(x) + ' as ' + alias(x.key);
|
|
628
684
|
if (groupBy == '')
|
|
629
685
|
groupBy = ' group by ';
|
|
630
686
|
else
|
|
@@ -644,7 +700,7 @@ export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax)
|
|
|
644
700
|
}
|
|
645
701
|
else {
|
|
646
702
|
const dbName = await e.$dbNameOf(x);
|
|
647
|
-
select += `, ${aggregateSqlSyntax(operator, dbName)} as ${x.key
|
|
703
|
+
select += `, ${aggregateSqlSyntax(operator, dbName)} as ${alias(x.key + '_' + operator)}`;
|
|
648
704
|
}
|
|
649
705
|
const turnToNumber = x.valueType === Number || operator == 'distinctCount';
|
|
650
706
|
processResultRow.push((sqlResult, theResult) => {
|
|
@@ -708,12 +764,15 @@ export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax)
|
|
|
708
764
|
page = 1;
|
|
709
765
|
select += ' ' + limitSyntax(options.limit, (page - 1) * options.limit);
|
|
710
766
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
767
|
+
return {
|
|
768
|
+
select,
|
|
769
|
+
/** `valueAt` reads one column of a result row, by its position in the select or by its alias. */
|
|
770
|
+
toResult(valueAt) {
|
|
771
|
+
let theResult = {};
|
|
772
|
+
processResultRow.forEach((x, i) => x(valueAt(i, aliases[i]), theResult));
|
|
773
|
+
return theResult;
|
|
774
|
+
},
|
|
775
|
+
};
|
|
717
776
|
function aggregateSqlSyntax(operator, dbName) {
|
|
718
777
|
return operator === 'distinctCount'
|
|
719
778
|
? `count (distinct ${dbName})`
|
|
@@ -190,7 +190,12 @@ export class Fields {
|
|
|
190
190
|
*/
|
|
191
191
|
static literal(optionalValues, ...options) {
|
|
192
192
|
return Fields.string({
|
|
193
|
-
validate: (entity, event) =>
|
|
193
|
+
validate: (entity, event) => {
|
|
194
|
+
// A value already stored is a fact. Only a new row or a new value has to be in the list.
|
|
195
|
+
if (!event.isNew && !event.valueChanged())
|
|
196
|
+
return;
|
|
197
|
+
return Validators.in(optionalValues())(entity, event);
|
|
198
|
+
},
|
|
194
199
|
//@ts-expect-error as we are adding this to options without it being defined in options
|
|
195
200
|
[fieldOptionalValuesFunctionKey]: optionalValues,
|
|
196
201
|
}, ...options);
|
|
@@ -200,7 +205,12 @@ export class Fields {
|
|
|
200
205
|
return Field(
|
|
201
206
|
//@ts-ignore
|
|
202
207
|
() => enumType(), {
|
|
203
|
-
validate: (entity, event) =>
|
|
208
|
+
validate: (entity, event) => {
|
|
209
|
+
// A value already stored is a fact. Only a new row or a new value has to be in the enum.
|
|
210
|
+
if (!event.isNew && !event.valueChanged())
|
|
211
|
+
return;
|
|
212
|
+
return Validators.enum(enumType())(entity, event);
|
|
213
|
+
},
|
|
204
214
|
[fieldOptionalValuesFunctionKey]: () => getEnumValues(enumType()),
|
|
205
215
|
}, ...options, (options) => {
|
|
206
216
|
;
|
|
@@ -449,6 +449,8 @@ export class RepositoryImplementation {
|
|
|
449
449
|
if (ref)
|
|
450
450
|
return returnResult((await ref.save(options)));
|
|
451
451
|
}
|
|
452
|
+
if (id === undefined || id === null)
|
|
453
|
+
throw this._notFoundError(id);
|
|
452
454
|
{
|
|
453
455
|
let ref = getEntityRef(id, false);
|
|
454
456
|
if (ref) {
|
|
@@ -2387,6 +2389,9 @@ export function ValueListFieldType(...options) {
|
|
|
2387
2389
|
if (ref.value && !values.find((v) => v === ref.value)) {
|
|
2388
2390
|
ref.value = values.find((v) => v.id === ref.value.id) || ref.value;
|
|
2389
2391
|
}
|
|
2392
|
+
// A value already stored is a fact. Only a new row or a new value has to be in the list.
|
|
2393
|
+
if (!ref.isNew && !ref.valueChanged())
|
|
2394
|
+
return;
|
|
2390
2395
|
return Validators.in(values)(entity, ref);
|
|
2391
2396
|
};
|
|
2392
2397
|
}, ...options)(type);
|
|
@@ -2405,6 +2410,7 @@ export class ValueListInfo {
|
|
|
2405
2410
|
byIdMap = new Map();
|
|
2406
2411
|
values = [];
|
|
2407
2412
|
isNumeric = false;
|
|
2413
|
+
unknownIdValue;
|
|
2408
2414
|
constructor(valueListType) {
|
|
2409
2415
|
this.valueListType = valueListType;
|
|
2410
2416
|
for (let member in this.valueListType) {
|
|
@@ -2425,7 +2431,9 @@ export class ValueListInfo {
|
|
|
2425
2431
|
var options = this.valueListType[storableMember];
|
|
2426
2432
|
if (options) {
|
|
2427
2433
|
for (const op of options) {
|
|
2428
|
-
if (op
|
|
2434
|
+
if (!op || typeof op === 'function')
|
|
2435
|
+
continue;
|
|
2436
|
+
if (op.getValues) {
|
|
2429
2437
|
this.values.splice(0, this.values.length, ...op.getValues());
|
|
2430
2438
|
this.byIdMap.clear();
|
|
2431
2439
|
this.values.forEach((s) => {
|
|
@@ -2433,6 +2441,8 @@ export class ValueListInfo {
|
|
|
2433
2441
|
this.byIdMap.set(s.id, s);
|
|
2434
2442
|
});
|
|
2435
2443
|
}
|
|
2444
|
+
if ('unknownIdValue' in op)
|
|
2445
|
+
this.unknownIdValue = op.unknownIdValue;
|
|
2436
2446
|
}
|
|
2437
2447
|
if (this.values.find((s) => s.id === undefined))
|
|
2438
2448
|
throw new Error(`ValueType ${this.valueListType} has values without an id`);
|
|
@@ -2450,7 +2460,15 @@ export class ValueListInfo {
|
|
|
2450
2460
|
return this.byIdMap.get(key);
|
|
2451
2461
|
}
|
|
2452
2462
|
fromJson(val) {
|
|
2453
|
-
|
|
2463
|
+
const known = this.byId(val);
|
|
2464
|
+
if (known !== undefined)
|
|
2465
|
+
return known;
|
|
2466
|
+
if (val == null)
|
|
2467
|
+
return undefined;
|
|
2468
|
+
const fallback = this.unknownIdValue;
|
|
2469
|
+
if (typeof fallback !== 'function')
|
|
2470
|
+
return fallback;
|
|
2471
|
+
return fallback(this.isNumeric ? +val : val);
|
|
2454
2472
|
}
|
|
2455
2473
|
toJson(val) {
|
|
2456
2474
|
if (!val)
|
package/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ ValueOrExpression, } from './src/column-interfaces.js';
|
|
|
30
30
|
export { RestDataProvider } from './src/data-providers/rest-data-provider.js';
|
|
31
31
|
export { InMemoryDataProvider } from './src/data-providers/in-memory-database.js';
|
|
32
32
|
export { SqlDatabase } from './src/data-providers/sql-database.js';
|
|
33
|
+
export type { RawSelectOptions } from './src/data-providers/sql-database.js';
|
|
33
34
|
export { CustomSqlFilterBuilder, dbNamesOf, } from './src/filter/filter-consumer-bridge-to-sql-request.js';
|
|
34
35
|
export type { dbNamesOfOptions, CustomSqlFilterBuilderFunction, EntityDbNames, } from './src/filter/filter-consumer-bridge-to-sql-request.js';
|
|
35
36
|
export { JsonDataProvider } from './src/data-providers/json-data-provider.js';
|