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.
@@ -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})`
@@ -190,7 +190,12 @@ export class Fields {
190
190
  */
191
191
  static literal(optionalValues, ...options) {
192
192
  return Fields.string({
193
- validate: (entity, event) => Validators.in(optionalValues())(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) => Validators.enum(enumType())(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?.getValues) {
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
- return this.byId(val);
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';