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.
@@ -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
  }
@@ -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) {
@@ -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.key] = toDb(f, data[f.key]);
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.add(col, val, '$regex', { $options: 'i' });
391
+ this.addRegex(col, val);
390
392
  }
391
393
  notContainsCaseInsensitive(col, val) {
392
- this.result.push(() => ({
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.add(col, `^${val}`, '$regex', { $options: 'i' });
397
+ this.addRegex(col, val, '^');
403
398
  }
404
399
  endsWithCaseInsensitive(col, val) {
405
- this.add(col, `${val}$`, '$regex', { $options: 'i' });
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, streamUrl, } from '../src/live-query/SubscriptionChannel.js';
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
- if (this.options.subscriptionServer instanceof SseSubscriptionServer) {
270
- const streamPath = this.options.rootPath + '/' + streamUrl;
271
- r.route(streamPath).get(this.process(async (remult, req, res, origReq, origRes) => {
272
- ;
273
- remult.subscriptionServer.openHttpServerStream(origReq, origRes);
274
- }));
275
- r.route(streamPath + '/subscribe').post(this.process(async (remult, _2, res, _, _1, origReq) => {
276
- const body = remult.subscriptionServer.subscribeToChannel(await this.coreOptions.getRequestBody(origReq), res, remult);
277
- }));
278
- r.route(streamPath + '/unsubscribe').post(this.process(async (remult, req, res, reqInfo, origRes, origReq) => {
279
- ;
280
- remult.subscriptionServer.subscribeToChannel(await this.coreOptions.getRequestBody(origReq), res, remult, true);
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);
@@ -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 res.success(await this.query(res, req, body));
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
- let e = await this.init();
327
- let r = this.sql.createCommand();
328
- let { colKeys, select } = await this.buildSelect(e, r, options?.select, options?.args);
329
- select = 'select ' + select;
330
- select += '\n from ' + e.$entityName;
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
- let result = {};
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
- let select = '';
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
- let select = 'select count(*) as count';
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}_${operator}`;
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
- const result = await r.execute(select);
712
- return result.rows.map((sql) => {
713
- let theResult = {};
714
- processResultRow.forEach((x, i) => x(sql[result.getColumnKeyInResultForIndexInSelect(i)], theResult));
715
- return theResult;
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})`
@@ -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) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remult",
3
- "version": "3.4.0-next.5",
3
+ "version": "3.4.0-next.6",
4
4
  "description": "A CRUD framework for full-stack TypeScript",
5
5
  "homepage": "https://remult.dev",
6
6
  "repository": {
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.key] = toDb(f_1, data[f_1.key]);
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.add(col, val, '$regex', { $options: 'i' });
802
+ this.addRegex(col, val);
801
803
  };
802
804
  FilterConsumerBridgeToMongo.prototype.notContainsCaseInsensitive = function (col, val) {
803
- var _this = this;
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.add(col, "^".concat(val), '$regex', { $options: 'i' });
808
+ this.addRegex(col, val, '^');
818
809
  };
819
810
  FilterConsumerBridgeToMongo.prototype.endsWithCaseInsensitive = function (col, val) {
820
- this.add(col, "".concat(val, "$"), '$regex', { $options: 'i' });
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;