remult 3.4.0-next.5 → 3.4.0-next.6
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/RepositoryImplementation.js +2 -0
- 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/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/RepositoryImplementation.js +2 -0
- 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})`
|
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';
|
package/internals.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare const actionInfo: {
|
|
|
14
14
|
};
|
|
15
15
|
export { isOfType } from './src/isOfType.js';
|
|
16
16
|
export type { ClassType } from './classType.js';
|
|
17
|
+
export type { SubscriptionServerRouteApi, SubscriptionServerRouteHandlerArgs, } from './server/remult-api-server.js';
|
|
18
|
+
export type { DataApiResponse } from './src/data-api.js';
|
|
17
19
|
export { flags } from './src/remult3/remult3.js';
|
|
18
20
|
export { SseSubscriptionClient } from './src/live-query/SseSubscriptionClient.js';
|
|
19
21
|
export { DataProviderPromiseWrapper } from './src/data-interfaces.js';
|
package/package.json
CHANGED
package/remult-mongo.js
CHANGED
|
@@ -228,6 +228,8 @@ var MongoEntityDataProvider = /** @class */ (function () {
|
|
|
228
228
|
try {
|
|
229
229
|
for (var _b = tslib_1.__values(this.entity.fields), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
230
230
|
var col = _c.value;
|
|
231
|
+
if (col.dbReadOnly || col.isServerExpression)
|
|
232
|
+
continue;
|
|
231
233
|
var val = toDb(col, row[col.key]);
|
|
232
234
|
if (val === null)
|
|
233
235
|
val = NULL;
|
|
@@ -494,7 +496,7 @@ var MongoEntityDataProvider = /** @class */ (function () {
|
|
|
494
496
|
f_1 = _c.value;
|
|
495
497
|
if (!f_1.dbReadOnly && !f_1.isServerExpression) {
|
|
496
498
|
if (keys.includes(f_1.key)) {
|
|
497
|
-
newR[f_1
|
|
499
|
+
newR[e.$dbNameOf(f_1)] = toDb(f_1, data[f_1.key]);
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
502
|
}
|
|
@@ -797,27 +799,32 @@ var FilterConsumerBridgeToMongo = /** @class */ (function () {
|
|
|
797
799
|
this.add(col, val, '$lt');
|
|
798
800
|
};
|
|
799
801
|
FilterConsumerBridgeToMongo.prototype.containsCaseInsensitive = function (col, val) {
|
|
800
|
-
this.
|
|
802
|
+
this.addRegex(col, val);
|
|
801
803
|
};
|
|
802
804
|
FilterConsumerBridgeToMongo.prototype.notContainsCaseInsensitive = function (col, val) {
|
|
803
|
-
|
|
804
|
-
this.result.push(function () {
|
|
805
|
-
var _a;
|
|
806
|
-
return (_a = {},
|
|
807
|
-
_a[_this.nameProvider.$dbNameOf(col)] = {
|
|
808
|
-
$not: {
|
|
809
|
-
$regex: isNull(val) ? val : toDb(col, val),
|
|
810
|
-
$options: 'i',
|
|
811
|
-
},
|
|
812
|
-
},
|
|
813
|
-
_a);
|
|
814
|
-
});
|
|
805
|
+
this.addRegex(col, val, '', '', true);
|
|
815
806
|
};
|
|
816
807
|
FilterConsumerBridgeToMongo.prototype.startsWithCaseInsensitive = function (col, val) {
|
|
817
|
-
this.
|
|
808
|
+
this.addRegex(col, val, '^');
|
|
818
809
|
};
|
|
819
810
|
FilterConsumerBridgeToMongo.prototype.endsWithCaseInsensitive = function (col, val) {
|
|
820
|
-
this.
|
|
811
|
+
this.addRegex(col, val, '', '$');
|
|
812
|
+
};
|
|
813
|
+
FilterConsumerBridgeToMongo.prototype.addRegex = function (col, val, start, end, negate) {
|
|
814
|
+
var _this = this;
|
|
815
|
+
if (start === void 0) { start = ''; }
|
|
816
|
+
if (end === void 0) { end = ''; }
|
|
817
|
+
if (negate === void 0) { negate = false; }
|
|
818
|
+
this.result.push(function () {
|
|
819
|
+
var _a;
|
|
820
|
+
var regex = {
|
|
821
|
+
$regex: start + toDb(col, val).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + end,
|
|
822
|
+
$options: 'i',
|
|
823
|
+
};
|
|
824
|
+
return _a = {},
|
|
825
|
+
_a[_this.nameProvider.$dbNameOf(col)] = negate ? { $not: regex } : regex,
|
|
826
|
+
_a;
|
|
827
|
+
});
|
|
821
828
|
};
|
|
822
829
|
FilterConsumerBridgeToMongo.prototype.add = function (col, val, operator, moreOptions) {
|
|
823
830
|
var _this = this;
|