remult 3.4.0-next.7 → 3.4.0

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.
@@ -4,7 +4,7 @@ import { remult as defaultRemult } from '../remult-proxy.js';
4
4
  import { GroupByCountMember, GroupByOperators, } from '../remult3/remult3.js';
5
5
  import { getRepository, isAutoIncrement, } from '../remult3/RepositoryImplementation.js';
6
6
  import { Sort } from '../sort.js';
7
- import { ValueConverters } from '../valueConverters.js';
7
+ import { fromDbColumn, toDbColumn } from '../valueConverters.js';
8
8
  import { getRepositoryInternals } from '../remult3/repository-internals.js';
9
9
  import { isOfType } from '../isOfType.js';
10
10
  import { originalSqlExpressionKey } from '../filter/fieldDbName.js';
@@ -51,6 +51,9 @@ export class SqlDatabase {
51
51
  * Wraps an identifier with the database's identifier syntax.
52
52
  */
53
53
  wrapIdentifier = (x) => x;
54
+ get jsonColumnStorage() {
55
+ return this.sql.supportsJsonColumnType ? 'native' : 'text';
56
+ }
54
57
  /* @internal*/
55
58
  _getSourceSql() {
56
59
  return this.sql;
@@ -65,18 +68,6 @@ export class SqlDatabase {
65
68
  * @returns The entity data provider.
66
69
  */
67
70
  getEntityDataProvider(entity) {
68
- if (!this.sql.supportsJsonColumnType) {
69
- for (const f of entity.fields.toArray()) {
70
- if (f.valueConverter.fieldTypeInDb === 'json') {
71
- //@ts-ignore
72
- f.valueConverter = {
73
- ...f.valueConverter,
74
- toDb: ValueConverters.JsonString.toDb,
75
- fromDb: ValueConverters.JsonString.fromDb,
76
- };
77
- }
78
- }
79
- }
80
71
  return new ActualSQLEntityDataProvider(entity, this, async (dbName) => {
81
72
  if (this.createdEntities.indexOf(dbName.$entityName) < 0) {
82
73
  this.createdEntities.push(dbName.$entityName);
@@ -154,7 +145,7 @@ export class SqlDatabase {
154
145
  }
155
146
  const r = getRepository(repo);
156
147
  var b = new FilterConsumerBridgeToSqlRequest(sqlCommand, dbNames ||
157
- (await dbNamesOfWithForceSqlExpression(r.metadata, wrapIdentifier)));
148
+ (await dbNamesOfWithForceSqlExpression(r.metadata, wrapIdentifier)), jsonColumnStorageOf(r));
158
149
  b._addWhere = false;
159
150
  await (await getRepositoryInternals(r)._translateWhereToFilter(condition)).__applyToConsumer(b);
160
151
  return await b.resolveWhere();
@@ -167,7 +158,7 @@ export class SqlDatabase {
167
158
  static async groupByToRaw(repo, options, sql) {
168
159
  const r = getRepository(repo);
169
160
  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);
161
+ (await dbNamesOfWithForceSqlExpression(r.metadata, sql?.wrapIdentifier ?? wrapIdentifierOf(r))), sql?.sqlCommand ?? new myDummySQLCommand(), false, sql?.limitSyntax ?? limitSyntaxRequired, jsonColumnStorageForRaw(r, sql?.jsonAsString));
171
162
  return {
172
163
  sql: built.select,
173
164
  toResult: (row) => built.toResult((_, alias) => row[alias]),
@@ -181,7 +172,10 @@ export class SqlDatabase {
181
172
  static async selectToRaw(repo, options, sql) {
182
173
  const r = getRepository(repo);
183
174
  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 });
175
+ (await dbNamesOfWithForceSqlExpression(r.metadata, sql?.wrapIdentifier ?? wrapIdentifierOf(r))), sql?.sqlCommand ?? new myDummySQLCommand(), false, sql?.limitSyntax ?? limitSyntaxRequired, {
176
+ aliasColumns: true,
177
+ jsonColumnStorage: jsonColumnStorageForRaw(r, sql?.jsonAsString),
178
+ });
185
179
  return {
186
180
  sql: built.select,
187
181
  toResult: (row) => built.toResult((_, alias) => row[alias]),
@@ -242,12 +236,12 @@ function splitByMaxParameters(items, parametersInItem, maxParameters) {
242
236
  batches.push(batch);
243
237
  return batches;
244
238
  }
245
- function countInsertBindParameters(entity, e, row) {
239
+ function countInsertBindParameters(entity, e, row, storage) {
246
240
  let n = 0;
247
241
  for (const x of entity.fields) {
248
242
  if (isDbReadonly(x, e))
249
243
  continue;
250
- if (x.valueConverter.toDb(row[x.key]) != undefined)
244
+ if (toDbColumn(x, row[x.key], storage) != undefined)
251
245
  n++;
252
246
  }
253
247
  return n;
@@ -339,7 +333,7 @@ class ActualSQLEntityDataProvider {
339
333
  let select = 'select count(*) count from ' + e.$entityName;
340
334
  let r = this.sql.createCommand();
341
335
  if (where) {
342
- let wc = new FilterConsumerBridgeToSqlRequest(r, e);
336
+ let wc = new FilterConsumerBridgeToSqlRequest(r, e, this.sql.jsonColumnStorage);
343
337
  where.__applyToConsumer(wc);
344
338
  select += await wc.resolveWhere();
345
339
  }
@@ -348,17 +342,19 @@ class ActualSQLEntityDataProvider {
348
342
  });
349
343
  }
350
344
  async groupBy(options) {
351
- return await groupByImpl(options, await this.init(), this.sql.createCommand(), this.sql._getSourceSql().orderByNullsFirst, this.sql._getSourceSql().getLimitSqlSyntax);
345
+ return await groupByImpl(options, await this.init(), this.sql.createCommand(), this.sql._getSourceSql().orderByNullsFirst, this.sql._getSourceSql().getLimitSqlSyntax, this.sql.jsonColumnStorage);
352
346
  }
353
347
  async find(options) {
354
348
  const e = await this.init();
355
349
  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));
350
+ if (options && !options.orderBy)
351
+ options.orderBy = Sort.createUniqueSort(this.entity, new Sort());
352
+ const { select, toResult } = await buildFindSql(this.entity, options, e, r, this.sql._getSourceSql().orderByNullsFirst, (limit, offset) => this.strategy.getLimitSqlSyntax(limit, offset), { jsonColumnStorage: this.sql.jsonColumnStorage });
357
353
  const result = await r.execute(select);
358
354
  return result.rows.map((y) => toResult((i) => y[result.getColumnKeyInResultForIndexInSelect(i)]));
359
355
  }
360
356
  buildResultRow(colKeys, y, r) {
361
- return resultRow(colKeys, (i) => y[r.getColumnKeyInResultForIndexInSelect(i)]);
357
+ return resultRow(colKeys, (i) => y[r.getColumnKeyInResultForIndexInSelect(i)], this.sql.jsonColumnStorage);
362
358
  }
363
359
  async buildSelect(e, r, selectedFields, args) {
364
360
  return selectColumns(this.entity, e, r, selectedFields, args, false);
@@ -372,7 +368,7 @@ class ActualSQLEntityDataProvider {
372
368
  if (isDbReadonly(x, e)) {
373
369
  }
374
370
  else if (data[x.key] !== undefined) {
375
- let v = x.valueConverter.toDb(data[x.key]);
371
+ let v = toDbColumn(x, data[x.key], this.sql.jsonColumnStorage);
376
372
  if (v !== undefined) {
377
373
  if (!added)
378
374
  added = true;
@@ -383,7 +379,7 @@ class ActualSQLEntityDataProvider {
383
379
  }
384
380
  }
385
381
  const idFilter = this.entity.idMetadata.getIdFilter(id);
386
- let f = new FilterConsumerBridgeToSqlRequest(r, e);
382
+ let f = new FilterConsumerBridgeToSqlRequest(r, e, this.sql.jsonColumnStorage);
387
383
  Filter.fromEntityFilter(this.entity, idFilter).__applyToConsumer(f);
388
384
  statement += await f.resolveWhere();
389
385
  let { colKeys, select } = await this.buildSelect(e, r, undefined, undefined);
@@ -417,7 +413,7 @@ class ActualSQLEntityDataProvider {
417
413
  return;
418
414
  let e = await this.init();
419
415
  let r = this.sql.createCommand();
420
- let f = new FilterConsumerBridgeToSqlRequest(r, e);
416
+ let f = new FilterConsumerBridgeToSqlRequest(r, e, this.sql.jsonColumnStorage);
421
417
  Filter.fromEntityFilter(this.entity, this.entity.idMetadata.getIdFilter(...ids)).__applyToConsumer(f);
422
418
  let statement = 'delete from ' + e.$entityName;
423
419
  statement += await f.resolveWhere();
@@ -431,7 +427,7 @@ class ActualSQLEntityDataProvider {
431
427
  let e = await this.init();
432
428
  const maxParams = this.strategy.maxParametersInOneSqlStatement ??
433
429
  defaultMaxParametersInOneSqlStatement;
434
- const batches = splitByMaxParameters(data, (row) => countInsertBindParameters(this.entity, e, row), maxParams);
430
+ const batches = splitByMaxParameters(data, (row) => countInsertBindParameters(this.entity, e, row, this.sql.jsonColumnStorage), maxParams);
435
431
  const result = [];
436
432
  for (const batch of batches)
437
433
  result.push(...(await this.insertBatch(batch, e, options)));
@@ -447,7 +443,7 @@ class ActualSQLEntityDataProvider {
447
443
  for (const x of this.entity.fields) {
448
444
  if (isDbReadonly(x, e))
449
445
  continue;
450
- if (batch.some((row) => x.valueConverter.toDb(row[x.key]) != undefined))
446
+ if (batch.some((row) => toDbColumn(x, row[x.key], this.sql.jsonColumnStorage) != undefined))
451
447
  cols.push(x);
452
448
  }
453
449
  let colSql = '';
@@ -465,7 +461,7 @@ class ActualSQLEntityDataProvider {
465
461
  if (j)
466
462
  vals += ', ';
467
463
  const x = cols[j];
468
- const v = x.valueConverter.toDb(batch[i][x.key]);
464
+ const v = toDbColumn(x, batch[i][x.key], this.sql.jsonColumnStorage);
469
465
  if (v != undefined)
470
466
  vals += toDbSql(r, x, v);
471
467
  else if (x.allowNull)
@@ -520,6 +516,17 @@ function wrapIdentifierOf(r) {
520
516
  return dp.wrapIdentifier?.bind(dp);
521
517
  return undefined;
522
518
  }
519
+ export function jsonColumnStorageOf(r) {
520
+ const dp = getRepositoryInternals(r)._dataProvider;
521
+ if (isOfType(dp, 'jsonColumnStorage'))
522
+ return dp.jsonColumnStorage;
523
+ return 'native';
524
+ }
525
+ function jsonColumnStorageForRaw(r, jsonAsString) {
526
+ if (jsonAsString === undefined)
527
+ return jsonColumnStorageOf(r);
528
+ return jsonAsString ? 'text' : 'native';
529
+ }
523
530
  function limitSyntaxRequired() {
524
531
  throw new Error('A raw select with a limit needs limitSyntax, for example SqlDatabase.getDb()._getSourceSql().getLimitSqlSyntax');
525
532
  }
@@ -560,21 +567,18 @@ export function getRowAfterUpdate(meta, dataProvider, data, id, operation) {
560
567
  });
561
568
  }
562
569
  /** 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 } = {}) {
570
+ export async function buildFindSql(entity, options, e, r, orderByNullsFirst, limitSyntax, { aliasColumns = false, jsonColumnStorage = 'native', } = {}) {
564
571
  const { colKeys, select: columns } = await selectColumns(entity, e, r, options?.select, options?.args, aliasColumns);
565
572
  let select = 'select ' + columns + '\n from ' + e.$entityName;
566
573
  if (options) {
567
574
  if (options.where) {
568
- let where = new FilterConsumerBridgeToSqlRequest(r, e);
575
+ let where = new FilterConsumerBridgeToSqlRequest(r, e, jsonColumnStorage);
569
576
  options.where.__applyToConsumer(where);
570
577
  select += await where.resolveWhere();
571
578
  }
572
579
  if (options.limit) {
573
580
  options.orderBy = Sort.createUniqueSort(entity, options.orderBy);
574
581
  }
575
- if (!options.orderBy && defaultOrderBy) {
576
- options.orderBy = Sort.createUniqueSort(entity, new Sort());
577
- }
578
582
  if (options.orderBy) {
579
583
  let first = true;
580
584
  for (const c of options.orderBy.Segments) {
@@ -611,7 +615,7 @@ export async function buildFindSql(entity, options, e, r, orderByNullsFirst, lim
611
615
  colKeys,
612
616
  /** `valueAt` reads one column of a result row, by its position in the select or by its alias (the field key). */
613
617
  toResult(valueAt) {
614
- return resultRow(colKeys, valueAt);
618
+ return resultRow(colKeys, valueAt, jsonColumnStorage);
615
619
  },
616
620
  };
617
621
  }
@@ -644,11 +648,11 @@ async function selectColumns(entity, e, r, selectedFields, args, aliasColumns) {
644
648
  }
645
649
  return { colKeys, select };
646
650
  }
647
- function resultRow(colKeys, valueAt) {
651
+ function resultRow(colKeys, valueAt, storage) {
648
652
  let result = {};
649
653
  colKeys.forEach((col, index) => {
650
654
  try {
651
- result[col.key] = col.valueConverter.fromDb(valueAt(index, col.key));
655
+ result[col.key] = fromDbColumn(col, valueAt(index, col.key), storage);
652
656
  }
653
657
  catch (err) {
654
658
  throw new Error('Failed to load from db:' + col.key + '\r\n' + err);
@@ -656,13 +660,13 @@ function resultRow(colKeys, valueAt) {
656
660
  });
657
661
  return result;
658
662
  }
659
- export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax) {
660
- const { select, toResult } = await buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax);
663
+ export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax, jsonColumnStorage = 'native') {
664
+ const { select, toResult } = await buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax, jsonColumnStorage);
661
665
  const result = await r.execute(select);
662
666
  return result.rows.map((sql) => toResult((i) => sql[result.getColumnKeyInResultForIndexInSelect(i)]));
663
667
  }
664
668
  /** 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) {
669
+ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax, jsonColumnStorage = 'native') {
666
670
  const aliases = [];
667
671
  function alias(name) {
668
672
  aliases.push(name);
@@ -689,7 +693,7 @@ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSynt
689
693
  groupByCols.push(e.$dbNameOf(x));
690
694
  }
691
695
  processResultRow.push((sqlResult, theResult) => {
692
- theResult[x.key] = x.valueConverter.fromDb(sqlResult);
696
+ theResult[x.key] = fromDbColumn(x, sqlResult, jsonColumnStorage);
693
697
  });
694
698
  }
695
699
  for (const operator of GroupByOperators) {
@@ -707,14 +711,14 @@ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSynt
707
711
  if (turnToNumber)
708
712
  sqlResult = Number(sqlResult);
709
713
  if (operator === 'max' || operator === 'min')
710
- sqlResult = x.valueConverter.fromDb(sqlResult);
714
+ sqlResult = fromDbColumn(x, sqlResult, jsonColumnStorage);
711
715
  theResult[x.key] = { ...theResult[x.key], [operator]: sqlResult };
712
716
  });
713
717
  }
714
718
  }
715
719
  select += '\n from ' + e.$entityName;
716
720
  if (options?.where) {
717
- let where = new FilterConsumerBridgeToSqlRequest(r, e);
721
+ let where = new FilterConsumerBridgeToSqlRequest(r, e, jsonColumnStorage);
718
722
  options?.where.__applyToConsumer(where);
719
723
  select += await where.resolveWhere();
720
724
  }
@@ -1,11 +1,22 @@
1
1
  import { SqlDatabase } from '../data-providers/sql-database.js';
2
2
  import { remult } from '../remult-proxy.js';
3
3
  import { getEntityMetadata } from '../remult3/RepositoryImplementation.js';
4
+ import { toDbColumn } from '../valueConverters.js';
4
5
  import { entityDbName } from './entityDbName.js';
5
6
  import { fieldDbName } from './fieldDbName.js';
7
+ // `[` starts a character class in sql server's like. `]` on its own is a literal
8
+ // everywhere, so escaping the opening bracket is enough. The other engines treat
9
+ // an escaped non-wildcard as that literal, so one escape set works for all of them.
10
+ const REGEX_LIKE_SPECIAL = /[!%_\[]/g;
11
+ // `!` not `\`: a backslash in a string literal is an escape in mysql and in pg without standard_conforming_strings
12
+ export const LIKE_ESCAPE_SQL = " escape '!'";
13
+ export function escapeLike(val) {
14
+ return val.replace(REGEX_LIKE_SPECIAL, '!$&');
15
+ }
6
16
  export class FilterConsumerBridgeToSqlRequest {
7
17
  r;
8
18
  nameProvider;
19
+ jsonColumnStorage;
9
20
  where = '';
10
21
  _addWhere = true;
11
22
  promises = [];
@@ -19,9 +30,10 @@ export class FilterConsumerBridgeToSqlRequest {
19
30
  }
20
31
  return this.where;
21
32
  }
22
- constructor(r, nameProvider) {
33
+ constructor(r, nameProvider, jsonColumnStorage = 'native') {
23
34
  this.r = r;
24
35
  this.nameProvider = nameProvider;
36
+ this.jsonColumnStorage = jsonColumnStorage;
25
37
  }
26
38
  custom(key, customItem) {
27
39
  throw new Error('Custom filter should be translated before it gets here');
@@ -30,7 +42,7 @@ export class FilterConsumerBridgeToSqlRequest {
30
42
  let statement = '';
31
43
  this.promises.push((async () => {
32
44
  for (const element of orElements) {
33
- let f = new FilterConsumerBridgeToSqlRequest(this.r, this.nameProvider);
45
+ let f = new FilterConsumerBridgeToSqlRequest(this.r, this.nameProvider, this.jsonColumnStorage);
34
46
  f._addWhere = false;
35
47
  element.__applyToConsumer(f);
36
48
  let where = await f.resolveWhere();
@@ -52,7 +64,7 @@ export class FilterConsumerBridgeToSqlRequest {
52
64
  }
53
65
  not(element) {
54
66
  this.promises.push((async () => {
55
- let f = new FilterConsumerBridgeToSqlRequest(this.r, this.nameProvider);
67
+ let f = new FilterConsumerBridgeToSqlRequest(this.r, this.nameProvider, this.jsonColumnStorage);
56
68
  f._addWhere = false;
57
69
  element.__applyToConsumer(f);
58
70
  let where = await f.resolveWhere();
@@ -73,7 +85,7 @@ export class FilterConsumerBridgeToSqlRequest {
73
85
  this.addToWhere(this.nameProvider.$dbNameOf(col) +
74
86
  ' in (' +
75
87
  val
76
- .map((x) => toDbSql(this.r, col, col.valueConverter.toDb(x)))
88
+ .map((x) => toDbSql(this.r, col, toDbColumn(col, x, this.jsonColumnStorage)))
77
89
  .join(',') +
78
90
  ')');
79
91
  else
@@ -99,39 +111,26 @@ export class FilterConsumerBridgeToSqlRequest {
99
111
  this.add(col, val, '<');
100
112
  }
101
113
  containsCaseInsensitive(col, val) {
102
- this.promises.push((async () => {
103
- this.addToWhere('lower (' +
104
- this.nameProvider.$dbNameOf(col) +
105
- ") like lower ('%" +
106
- val.replace(/'/g, "''") +
107
- "%')");
108
- })());
114
+ this.like(col, '%' + escapeLike(val) + '%');
109
115
  }
110
116
  notContainsCaseInsensitive(col, val) {
111
- this.promises.push((async () => {
112
- this.addToWhere('not lower (' +
113
- this.nameProvider.$dbNameOf(col) +
114
- ") like lower ('%" +
115
- val.replace(/'/g, "''") +
116
- "%')");
117
- })());
117
+ this.like(col, '%' + escapeLike(val) + '%', 'not ');
118
118
  }
119
119
  startsWithCaseInsensitive(col, val) {
120
- this.promises.push((async () => {
121
- this.addToWhere('lower (' +
122
- this.nameProvider.$dbNameOf(col) +
123
- ") like lower ('" +
124
- val.replace(/'/g, "''") +
125
- "%')");
126
- })());
120
+ this.like(col, escapeLike(val) + '%');
127
121
  }
128
122
  endsWithCaseInsensitive(col, val) {
123
+ this.like(col, '%' + escapeLike(val));
124
+ }
125
+ like(col, pattern, not = '') {
129
126
  this.promises.push((async () => {
130
- this.addToWhere('lower (' +
127
+ this.addToWhere(not +
128
+ 'lower (' +
131
129
  this.nameProvider.$dbNameOf(col) +
132
- ") like lower ('%" +
133
- val.replace(/'/g, "''") +
134
- "')");
130
+ ') like lower (' +
131
+ this.r.param(pattern) +
132
+ ')' +
133
+ LIKE_ESCAPE_SQL);
135
134
  })());
136
135
  }
137
136
  add(col, val, operator) {
@@ -140,7 +139,7 @@ export class FilterConsumerBridgeToSqlRequest {
140
139
  ' ' +
141
140
  operator +
142
141
  ' ' +
143
- toDbSql(this.r, col, col.valueConverter.toDb(val));
142
+ toDbSql(this.r, col, toDbColumn(col, val, this.jsonColumnStorage));
144
143
  this.addToWhere(x);
145
144
  })());
146
145
  }
@@ -156,7 +155,7 @@ export class FilterConsumerBridgeToSqlRequest {
156
155
  databaseCustom(databaseCustom) {
157
156
  this.promises.push((async () => {
158
157
  if (databaseCustom?.buildSql) {
159
- let item = new CustomSqlFilterBuilder(this.r, this.nameProvider.wrapIdentifier);
158
+ let item = new CustomSqlFilterBuilder(this.r, this.nameProvider.wrapIdentifier, this.jsonColumnStorage);
160
159
  let sql = await databaseCustom.buildSql(item);
161
160
  if (typeof sql !== 'string')
162
161
  sql = item.sql;
@@ -173,9 +172,11 @@ export class FilterConsumerBridgeToSqlRequest {
173
172
  export class CustomSqlFilterBuilder {
174
173
  r;
175
174
  wrapIdentifier;
176
- constructor(r, wrapIdentifier) {
175
+ jsonColumnStorage;
176
+ constructor(r, wrapIdentifier, jsonColumnStorage = 'native') {
177
177
  this.r = r;
178
178
  this.wrapIdentifier = wrapIdentifier;
179
+ this.jsonColumnStorage = jsonColumnStorage;
179
180
  this.param.bind(this);
180
181
  this.filterToRaw.bind(this);
181
182
  }
@@ -191,8 +192,8 @@ export class CustomSqlFilterBuilder {
191
192
  * @returns {string} - The SQL token.
192
193
  */
193
194
  param = (val, field) => {
194
- if (typeof field === 'object' && field.valueConverter.toDb) {
195
- val = field.valueConverter.toDb(val);
195
+ if (field) {
196
+ val = toDbColumn(field, val, this.jsonColumnStorage);
196
197
  }
197
198
  return this.r.param(val);
198
199
  };
@@ -0,0 +1,29 @@
1
+ /** Return this from {@link walkFilter}'s visitor to drop that field key. */
2
+ export const omitFilterField = Symbol('omitFilterField');
3
+ function isCompoundFilterKey(key) {
4
+ return key === '$and' || key === '$or' || key === '$not';
5
+ }
6
+ // `$and`, `$or`, and `$not` are entered. Every other key is a field, passed to `visit`.
7
+ // `visit` returns the value to keep, or `omitFilterField` to drop the key.
8
+ export function walkFilter(filter, visit) {
9
+ if (Array.isArray(filter)) {
10
+ const result = [];
11
+ for (const item of filter)
12
+ result.push(walkFilter(item, visit));
13
+ return result;
14
+ }
15
+ if (!filter || typeof filter !== 'object')
16
+ return filter;
17
+ const result = {};
18
+ for (const key of Object.keys(filter)) {
19
+ const value = filter[key];
20
+ if (isCompoundFilterKey(key)) {
21
+ result[key] = walkFilter(value, visit);
22
+ continue;
23
+ }
24
+ const next = visit(key, value);
25
+ if (next !== omitFilterField)
26
+ result[key] = next;
27
+ }
28
+ return result;
29
+ }
@@ -124,10 +124,10 @@ export class RemultProxy {
124
124
  .remultFactory()
125
125
  .repo(...args)
126
126
  .updateMany(...args2),
127
- find: (...args2) => self
127
+ find: (options) => self
128
128
  .remultFactory()
129
129
  .repo(...args)
130
- .find(...args2),
130
+ .find(options),
131
131
  groupBy: (...args2) => self
132
132
  .remultFactory()
133
133
  .repo(...args)
@@ -138,14 +138,14 @@ export class RemultProxy {
138
138
  .repo(...args)
139
139
  //@ts-ignore
140
140
  .aggregate(...args2),
141
- findFirst: (...args2) => self
141
+ findFirst: (where, options) => self
142
142
  .remultFactory()
143
143
  .repo(...args)
144
- .findFirst(...args2),
145
- findOne: (...args2) => self
144
+ .findFirst(where, options),
145
+ findOne: (options) => self
146
146
  .remultFactory()
147
147
  .repo(...args)
148
- .findOne(...args2),
148
+ .findOne(options),
149
149
  findId: (a, b) => self
150
150
  .remultFactory()
151
151
  .repo(...args)
@@ -41,6 +41,18 @@ let classValidatorValidate = undefined;
41
41
  // })
42
42
  // .catch(() => {
43
43
  // });
44
+ // Assigning a `toOne` also writes its `field`/`fields`, which would wipe the FK of a row whose
45
+ // target is missing.
46
+ function setMissingRelationToNull(row, key) {
47
+ const ref = getEntityRef(row);
48
+ ref._skipRelationFieldSync = true;
49
+ try {
50
+ row[key] = null;
51
+ }
52
+ finally {
53
+ ref._skipRelationFieldSync = false;
54
+ }
55
+ }
44
56
  export class RepositoryImplementation {
45
57
  _entity;
46
58
  _remult;
@@ -597,7 +609,16 @@ export class RepositoryImplementation {
597
609
  if (this._defaultFindOptions) {
598
610
  options = { ...this._defaultFindOptions, ...options };
599
611
  }
600
- let opt = await this._buildEntityDataProviderFindOptions(options);
612
+ let findOptions = options;
613
+ if ((!findOptions.orderBy || Object.keys(findOptions.orderBy).length === 0) &&
614
+ !this._dataProvider.isProxy &&
615
+ this._info.entityInfo.defaultOrderBy) {
616
+ findOptions = {
617
+ ...findOptions,
618
+ orderBy: this._info.entityInfo.defaultOrderBy,
619
+ };
620
+ }
621
+ let opt = await this._buildEntityDataProviderFindOptions(findOptions);
601
622
  if (skipOrderByAndLimit) {
602
623
  delete opt.orderBy;
603
624
  delete opt.limit;
@@ -633,10 +654,6 @@ export class RepositoryImplementation {
633
654
  return f.key;
634
655
  });
635
656
  }
636
- if (!options.orderBy || Object.keys(options.orderBy).length === 0) {
637
- if (!this._dataProvider.isProxy)
638
- options.orderBy = this._info.entityInfo.defaultOrderBy;
639
- }
640
657
  opt.where = await this._translateWhereToFilter(options.where);
641
658
  if (options.orderBy !== undefined)
642
659
  opt.orderBy = Sort.translateOrderByToSort(this.metadata, options.orderBy);
@@ -727,10 +744,12 @@ export class RepositoryImplementation {
727
744
  for (const row of result) {
728
745
  let { findOptions, returnNull, returnUndefined } = this._findOptionsBasedOnRelation(rel, col, incl, row, otherRepo);
729
746
  const colKey = col.key;
730
- if (returnNull)
731
- row[colKey] = null;
732
- else if (returnUndefined)
733
- row[colKey] = undefined;
747
+ if (returnNull || returnUndefined) {
748
+ if (rel.type === 'toMany')
749
+ row[colKey] = (returnNull ? null : undefined);
750
+ else
751
+ setMissingRelationToNull(row, colKey);
752
+ }
734
753
  else {
735
754
  const entityType = rel.toEntity;
736
755
  const toRepo = otherRepo;
@@ -741,14 +760,12 @@ export class RepositoryImplementation {
741
760
  metadata: toRepo.metadata,
742
761
  }, findOptions)
743
762
  .then((result) => {
744
- if (result.length == 0 && rel.type == 'toOne')
745
- return;
746
- row[colKey] =
747
- rel.type !== 'toMany'
748
- ? result.length == 0
749
- ? null
750
- : result[0]
751
- : result;
763
+ if (rel.type === 'toMany')
764
+ row[colKey] = result;
765
+ else if (result.length == 0)
766
+ setMissingRelationToNull(row, colKey);
767
+ else
768
+ row[colKey] = result[0];
752
769
  });
753
770
  }
754
771
  }
@@ -1184,7 +1201,7 @@ class rowHelperBase {
1184
1201
  },
1185
1202
  set: (newVal) => {
1186
1203
  val = newVal;
1187
- if (newVal === undefined)
1204
+ if (newVal === undefined || this._skipRelationFieldSync)
1188
1205
  return;
1189
1206
  const op = col.options;
1190
1207
  if (op.field) {
@@ -1209,6 +1226,7 @@ class rowHelperBase {
1209
1226
  }
1210
1227
  }
1211
1228
  }
1229
+ _skipRelationFieldSync = false;
1212
1230
  _subscribers;
1213
1231
  subscribe(listener) {
1214
1232
  this.initSubscribers();
@@ -1667,7 +1685,9 @@ export class rowHelperImplementation extends rowHelperBase {
1667
1685
  await listener.saved?.(this.instance, true);
1668
1686
  }
1669
1687
  }
1670
- this.repo._remult.liveQueryPublisher.itemChanged(this.repo.metadata.key, [{ id: this.getId(), oldId: this.getOriginalId(), deleted: false }]);
1688
+ this.repo._remult.liveQueryPublisher.itemChanged(this.repo.metadata.key, [
1689
+ { id: this.getId(), oldId: this.getOriginalId(), deleted: false },
1690
+ ]);
1671
1691
  this.saveOriginalData();
1672
1692
  this._isNew = false;
1673
1693
  return this.instance;
@@ -2454,21 +2474,21 @@ export class ValueListInfo {
2454
2474
  getValues() {
2455
2475
  return this.values;
2456
2476
  }
2457
- byId(key) {
2458
- if (this.isNumeric)
2459
- key = +key;
2460
- return this.byIdMap.get(key);
2461
- }
2462
- fromJson(val) {
2463
- const known = this.byId(val);
2464
- if (known !== undefined)
2477
+ /** Declared id only. Pass `useUnknownIdValue` to also apply `unknownIdValue` (`null` and `undefined` still return `undefined`). */
2478
+ byId(key, useUnknownIdValue = false) {
2479
+ const lookupKey = this.isNumeric ? +key : key;
2480
+ const known = this.byIdMap.get(lookupKey);
2481
+ if (known !== undefined || !useUnknownIdValue)
2465
2482
  return known;
2466
- if (val == null)
2483
+ if (key == null)
2467
2484
  return undefined;
2468
2485
  const fallback = this.unknownIdValue;
2469
2486
  if (typeof fallback !== 'function')
2470
2487
  return fallback;
2471
- return fallback(this.isNumeric ? +val : val);
2488
+ return fallback(lookupKey);
2489
+ }
2490
+ fromJson(val) {
2491
+ return this.byId(val, true);
2472
2492
  }
2473
2493
  toJson(val) {
2474
2494
  if (!val)
package/esm/src/sort.js CHANGED
@@ -128,9 +128,6 @@ export class Sort {
128
128
  * @returns {Sort} A `Sort` instance representing the unique sort criteria.
129
129
  */
130
130
  static createUniqueSort(entityMetadata, orderBy) {
131
- if ((!orderBy || Object.keys(orderBy).length === 0) &&
132
- entityMetadata.options.defaultOrderBy)
133
- orderBy = Sort.translateOrderByToSort(entityMetadata, entityMetadata.options.defaultOrderBy);
134
131
  if (!orderBy)
135
132
  orderBy = new Sort();
136
133
  for (const field of entityMetadata.idMetadata.fields) {
@@ -211,6 +211,17 @@ export class ValueConverters {
211
211
  fieldTypeInDb: 'json',
212
212
  };
213
213
  }
214
+ export function fromDbColumn(field, value, storage) {
215
+ if (storage === 'text' && field.valueConverter.fieldTypeInDb === 'json')
216
+ return ValueConverters.JsonString.fromDb(value);
217
+ return field.valueConverter.fromDb(value);
218
+ }
219
+ export function toDbColumn(field, value, storage) {
220
+ if ((storage === 'text' || storage === 'textOnWrite') &&
221
+ field.valueConverter.fieldTypeInDb === 'json')
222
+ return ValueConverters.JsonString.toDb(value);
223
+ return field.valueConverter.toDb(value);
224
+ }
214
225
  function enforceString(value) {
215
226
  if (value === null || value === undefined)
216
227
  return value;