remult 3.4.0-next.9 → 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 });
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,19 +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
350
  if (options && !options.orderBy)
357
351
  options.orderBy = Sort.createUniqueSort(this.entity, new Sort());
358
- const { select, toResult } = await buildFindSql(this.entity, options, e, r, this.sql._getSourceSql().orderByNullsFirst, (limit, offset) => this.strategy.getLimitSqlSyntax(limit, offset));
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 });
359
353
  const result = await r.execute(select);
360
354
  return result.rows.map((y) => toResult((i) => y[result.getColumnKeyInResultForIndexInSelect(i)]));
361
355
  }
362
356
  buildResultRow(colKeys, y, r) {
363
- return resultRow(colKeys, (i) => y[r.getColumnKeyInResultForIndexInSelect(i)]);
357
+ return resultRow(colKeys, (i) => y[r.getColumnKeyInResultForIndexInSelect(i)], this.sql.jsonColumnStorage);
364
358
  }
365
359
  async buildSelect(e, r, selectedFields, args) {
366
360
  return selectColumns(this.entity, e, r, selectedFields, args, false);
@@ -374,7 +368,7 @@ class ActualSQLEntityDataProvider {
374
368
  if (isDbReadonly(x, e)) {
375
369
  }
376
370
  else if (data[x.key] !== undefined) {
377
- let v = x.valueConverter.toDb(data[x.key]);
371
+ let v = toDbColumn(x, data[x.key], this.sql.jsonColumnStorage);
378
372
  if (v !== undefined) {
379
373
  if (!added)
380
374
  added = true;
@@ -385,7 +379,7 @@ class ActualSQLEntityDataProvider {
385
379
  }
386
380
  }
387
381
  const idFilter = this.entity.idMetadata.getIdFilter(id);
388
- let f = new FilterConsumerBridgeToSqlRequest(r, e);
382
+ let f = new FilterConsumerBridgeToSqlRequest(r, e, this.sql.jsonColumnStorage);
389
383
  Filter.fromEntityFilter(this.entity, idFilter).__applyToConsumer(f);
390
384
  statement += await f.resolveWhere();
391
385
  let { colKeys, select } = await this.buildSelect(e, r, undefined, undefined);
@@ -419,7 +413,7 @@ class ActualSQLEntityDataProvider {
419
413
  return;
420
414
  let e = await this.init();
421
415
  let r = this.sql.createCommand();
422
- let f = new FilterConsumerBridgeToSqlRequest(r, e);
416
+ let f = new FilterConsumerBridgeToSqlRequest(r, e, this.sql.jsonColumnStorage);
423
417
  Filter.fromEntityFilter(this.entity, this.entity.idMetadata.getIdFilter(...ids)).__applyToConsumer(f);
424
418
  let statement = 'delete from ' + e.$entityName;
425
419
  statement += await f.resolveWhere();
@@ -433,7 +427,7 @@ class ActualSQLEntityDataProvider {
433
427
  let e = await this.init();
434
428
  const maxParams = this.strategy.maxParametersInOneSqlStatement ??
435
429
  defaultMaxParametersInOneSqlStatement;
436
- 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);
437
431
  const result = [];
438
432
  for (const batch of batches)
439
433
  result.push(...(await this.insertBatch(batch, e, options)));
@@ -449,7 +443,7 @@ class ActualSQLEntityDataProvider {
449
443
  for (const x of this.entity.fields) {
450
444
  if (isDbReadonly(x, e))
451
445
  continue;
452
- 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))
453
447
  cols.push(x);
454
448
  }
455
449
  let colSql = '';
@@ -467,7 +461,7 @@ class ActualSQLEntityDataProvider {
467
461
  if (j)
468
462
  vals += ', ';
469
463
  const x = cols[j];
470
- const v = x.valueConverter.toDb(batch[i][x.key]);
464
+ const v = toDbColumn(x, batch[i][x.key], this.sql.jsonColumnStorage);
471
465
  if (v != undefined)
472
466
  vals += toDbSql(r, x, v);
473
467
  else if (x.allowNull)
@@ -522,6 +516,17 @@ function wrapIdentifierOf(r) {
522
516
  return dp.wrapIdentifier?.bind(dp);
523
517
  return undefined;
524
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
+ }
525
530
  function limitSyntaxRequired() {
526
531
  throw new Error('A raw select with a limit needs limitSyntax, for example SqlDatabase.getDb()._getSourceSql().getLimitSqlSyntax');
527
532
  }
@@ -562,12 +567,12 @@ export function getRowAfterUpdate(meta, dataProvider, data, id, operation) {
562
567
  });
563
568
  }
564
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. */
565
- export async function buildFindSql(entity, options, e, r, orderByNullsFirst, limitSyntax, { aliasColumns = false } = {}) {
570
+ export async function buildFindSql(entity, options, e, r, orderByNullsFirst, limitSyntax, { aliasColumns = false, jsonColumnStorage = 'native', } = {}) {
566
571
  const { colKeys, select: columns } = await selectColumns(entity, e, r, options?.select, options?.args, aliasColumns);
567
572
  let select = 'select ' + columns + '\n from ' + e.$entityName;
568
573
  if (options) {
569
574
  if (options.where) {
570
- let where = new FilterConsumerBridgeToSqlRequest(r, e);
575
+ let where = new FilterConsumerBridgeToSqlRequest(r, e, jsonColumnStorage);
571
576
  options.where.__applyToConsumer(where);
572
577
  select += await where.resolveWhere();
573
578
  }
@@ -610,7 +615,7 @@ export async function buildFindSql(entity, options, e, r, orderByNullsFirst, lim
610
615
  colKeys,
611
616
  /** `valueAt` reads one column of a result row, by its position in the select or by its alias (the field key). */
612
617
  toResult(valueAt) {
613
- return resultRow(colKeys, valueAt);
618
+ return resultRow(colKeys, valueAt, jsonColumnStorage);
614
619
  },
615
620
  };
616
621
  }
@@ -643,11 +648,11 @@ async function selectColumns(entity, e, r, selectedFields, args, aliasColumns) {
643
648
  }
644
649
  return { colKeys, select };
645
650
  }
646
- function resultRow(colKeys, valueAt) {
651
+ function resultRow(colKeys, valueAt, storage) {
647
652
  let result = {};
648
653
  colKeys.forEach((col, index) => {
649
654
  try {
650
- result[col.key] = col.valueConverter.fromDb(valueAt(index, col.key));
655
+ result[col.key] = fromDbColumn(col, valueAt(index, col.key), storage);
651
656
  }
652
657
  catch (err) {
653
658
  throw new Error('Failed to load from db:' + col.key + '\r\n' + err);
@@ -655,13 +660,13 @@ function resultRow(colKeys, valueAt) {
655
660
  });
656
661
  return result;
657
662
  }
658
- export async function groupByImpl(options, e, r, orderByNullFirst, limitSyntax) {
659
- 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);
660
665
  const result = await r.execute(select);
661
666
  return result.rows.map((sql) => toResult((i) => sql[result.getColumnKeyInResultForIndexInSelect(i)]));
662
667
  }
663
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. */
664
- export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax) {
669
+ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSyntax, jsonColumnStorage = 'native') {
665
670
  const aliases = [];
666
671
  function alias(name) {
667
672
  aliases.push(name);
@@ -688,7 +693,7 @@ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSynt
688
693
  groupByCols.push(e.$dbNameOf(x));
689
694
  }
690
695
  processResultRow.push((sqlResult, theResult) => {
691
- theResult[x.key] = x.valueConverter.fromDb(sqlResult);
696
+ theResult[x.key] = fromDbColumn(x, sqlResult, jsonColumnStorage);
692
697
  });
693
698
  }
694
699
  for (const operator of GroupByOperators) {
@@ -706,14 +711,14 @@ export async function buildGroupBySql(options, e, r, orderByNullFirst, limitSynt
706
711
  if (turnToNumber)
707
712
  sqlResult = Number(sqlResult);
708
713
  if (operator === 'max' || operator === 'min')
709
- sqlResult = x.valueConverter.fromDb(sqlResult);
714
+ sqlResult = fromDbColumn(x, sqlResult, jsonColumnStorage);
710
715
  theResult[x.key] = { ...theResult[x.key], [operator]: sqlResult };
711
716
  });
712
717
  }
713
718
  }
714
719
  select += '\n from ' + e.$entityName;
715
720
  if (options?.where) {
716
- let where = new FilterConsumerBridgeToSqlRequest(r, e);
721
+ let where = new FilterConsumerBridgeToSqlRequest(r, e, jsonColumnStorage);
717
722
  options?.where.__applyToConsumer(where);
718
723
  select += await where.resolveWhere();
719
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;
@@ -732,10 +744,12 @@ export class RepositoryImplementation {
732
744
  for (const row of result) {
733
745
  let { findOptions, returnNull, returnUndefined } = this._findOptionsBasedOnRelation(rel, col, incl, row, otherRepo);
734
746
  const colKey = col.key;
735
- if (returnNull)
736
- row[colKey] = null;
737
- else if (returnUndefined)
738
- 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
+ }
739
753
  else {
740
754
  const entityType = rel.toEntity;
741
755
  const toRepo = otherRepo;
@@ -746,14 +760,12 @@ export class RepositoryImplementation {
746
760
  metadata: toRepo.metadata,
747
761
  }, findOptions)
748
762
  .then((result) => {
749
- if (result.length == 0 && rel.type == 'toOne')
750
- return;
751
- row[colKey] =
752
- rel.type !== 'toMany'
753
- ? result.length == 0
754
- ? null
755
- : result[0]
756
- : 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];
757
769
  });
758
770
  }
759
771
  }
@@ -1189,7 +1201,7 @@ class rowHelperBase {
1189
1201
  },
1190
1202
  set: (newVal) => {
1191
1203
  val = newVal;
1192
- if (newVal === undefined)
1204
+ if (newVal === undefined || this._skipRelationFieldSync)
1193
1205
  return;
1194
1206
  const op = col.options;
1195
1207
  if (op.field) {
@@ -1214,6 +1226,7 @@ class rowHelperBase {
1214
1226
  }
1215
1227
  }
1216
1228
  }
1229
+ _skipRelationFieldSync = false;
1217
1230
  _subscribers;
1218
1231
  subscribe(listener) {
1219
1232
  this.initSubscribers();
@@ -1672,7 +1685,9 @@ export class rowHelperImplementation extends rowHelperBase {
1672
1685
  await listener.saved?.(this.instance, true);
1673
1686
  }
1674
1687
  }
1675
- 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
+ ]);
1676
1691
  this.saveOriginalData();
1677
1692
  this._isNew = false;
1678
1693
  return this.instance;
@@ -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;
package/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { ClassType } from './classType.js';
2
2
  import type { DataProvider } from './src/data-interfaces.js';
3
3
  export { ArrayEntityDataProvider } from './src/data-providers/array-entity-data-provider.js';
4
4
  export type { ClassType } from './classType.js';
5
- export type { ValidateFieldEvent, MembersOnly, NumericKeys, FieldsMetadata, FieldRef, IdFieldRef, FieldsRef, EntityMetadata, EntityOrderBy, EntityFilter, FindOptions, FindOptionsBase, QueryResult, QueryOptions, UpsertOptions, Repository, GroupByOptions, GroupByResult, FindFirstOptions, ComparisonValueFilter, ValueFilter, IdFilter, ContainsStringValueFilter, EntityRef, SortSegments, Paginator, LiveQuery, LiveQueryChangeInfo, Subscribable, RefSubscriber, RefSubscriberBase, RelationOptions, ObjectMembersOnly, MembersToInclude, RepositoryRelations, EntityIdFields, EntitySelectFields, InsertOrUpdateOptions, ClassFieldDecorator, ClassFieldDecoratorContextStub, //n1 consider removing in ts5
5
+ export type { ValidateFieldEvent, MembersOnly, NumericKeys, FieldsMetadata, FieldRef, IdFieldRef, FieldsRef, EntityMetadata, EntityOrderBy, EntityFilter, FindOptions, FindOptionsBase, QueryResult, QueryOptions, UpsertOptions, Repository, GroupByOptions, GroupByResult, FindFirstOptions, ComparisonValueFilter, ValueFilter, IdFilter, ContainsStringValueFilter, EntityRef, SortSegments, Paginator, LiveQuery, LiveQueryChangeInfo, Subscribable, RefSubscriber, RefSubscriberBase, RelationOptions, ObjectMembersOnly, MembersToInclude, Loaded, RepositoryRelations, EntityIdFields, EntitySelectFields, InsertOrUpdateOptions, ClassFieldDecorator, ClassFieldDecoratorContextStub, //n1 consider removing in ts5
6
6
  LifecycleEvent, FieldsRefBase, FieldsRefForEntityBase, RepositoryRelationsForEntityBase, ControllerRefForControllerBase, ControllerRefBase, ControllerRef, EntityRefForEntityBase, IdMetadata, FindFirstOptionsBase, } from './src/remult3/remult3.js';
7
7
  export { standardSchema } from './src/standard-schema/index.js';
8
8
  export { EntityBase, ControllerBase, FieldType, getFields, ValueListFieldType, getValueList, ValueListInfo, } from './src/remult3/RepositoryImplementation.js';
package/internals.d.ts CHANGED
@@ -3,6 +3,7 @@ export { getRelationInfo, getRelationFieldInfo, type RelationFieldInfo, type Rel
3
3
  export { decorateColumnSettings, controllerRefImpl, getControllerRef, fieldOptionsEnricher, } from './src/remult3/RepositoryImplementation.js';
4
4
  export { getEntitySettings } from './src/remult3/getEntityRef.js';
5
5
  export { __updateEntityBasedOnWhere } from './src/filter/filter-interfaces.js';
6
+ export { omitFilterField, walkFilter } from './src/filter/walk-filter.js';
6
7
  export declare const actionInfo: {
7
8
  allActions: any[];
8
9
  runningOnServer: boolean;
package/internals.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pagedQueryResult = exports.DataProviderPromiseWrapper = exports.SseSubscriptionClient = exports.flags = exports.isOfType = exports.actionInfo = exports.__updateEntityBasedOnWhere = exports.getEntitySettings = exports.fieldOptionsEnricher = exports.getControllerRef = exports.controllerRefImpl = exports.decorateColumnSettings = exports.getRelationFieldInfo = exports.getRelationInfo = exports.sqlRelationsFilter = exports.sqlRelations = void 0;
3
+ exports.pagedQueryResult = exports.DataProviderPromiseWrapper = exports.SseSubscriptionClient = exports.flags = exports.isOfType = exports.actionInfo = exports.walkFilter = exports.omitFilterField = exports.__updateEntityBasedOnWhere = exports.getEntitySettings = exports.fieldOptionsEnricher = exports.getControllerRef = exports.controllerRefImpl = exports.decorateColumnSettings = exports.getRelationFieldInfo = exports.getRelationInfo = exports.sqlRelationsFilter = exports.sqlRelations = void 0;
4
4
  var remult_static_js_1 = require("./src/remult-static.js");
5
5
  var sql_relations_js_1 = require("./src/data-providers/sql-relations.js");
6
6
  Object.defineProperty(exports, "sqlRelations", { enumerable: true, get: function () { return sql_relations_js_1.sqlRelations; } });
@@ -17,6 +17,9 @@ var getEntityRef_js_1 = require("./src/remult3/getEntityRef.js");
17
17
  Object.defineProperty(exports, "getEntitySettings", { enumerable: true, get: function () { return getEntityRef_js_1.getEntitySettings; } });
18
18
  var filter_interfaces_js_1 = require("./src/filter/filter-interfaces.js");
19
19
  Object.defineProperty(exports, "__updateEntityBasedOnWhere", { enumerable: true, get: function () { return filter_interfaces_js_1.__updateEntityBasedOnWhere; } });
20
+ var walk_filter_js_1 = require("./src/filter/walk-filter.js");
21
+ Object.defineProperty(exports, "omitFilterField", { enumerable: true, get: function () { return walk_filter_js_1.omitFilterField; } });
22
+ Object.defineProperty(exports, "walkFilter", { enumerable: true, get: function () { return walk_filter_js_1.walkFilter; } });
20
23
  exports.actionInfo = remult_static_js_1.remultStatic.actionInfo;
21
24
  var isOfType_js_1 = require("./src/isOfType.js");
22
25
  Object.defineProperty(exports, "isOfType", { enumerable: true, get: function () { return isOfType_js_1.isOfType; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remult",
3
- "version": "3.4.0-next.9",
3
+ "version": "3.4.0",
4
4
  "description": "A CRUD framework for full-stack TypeScript",
5
5
  "homepage": "https://remult.dev",
6
6
  "repository": {
@@ -4,9 +4,9 @@ import type { FieldMetadata } from '../src/column-interfaces.js';
4
4
  import type { DataProvider, EntityDataProvider } from '../src/data-interfaces.js';
5
5
  import type { EntityFilter, EntityMetadata } from '../src/remult3/remult3.js';
6
6
  import type { RepositoryOverloads } from '../src/remult3/RepositoryImplementation.js';
7
- import type { HasWrapIdentifier, SqlCommand, SqlCommandFactory, SqlResult } from '../src/sql-command.js';
7
+ import type { HasJsonColumnStorage, HasWrapIdentifier, JsonColumnStorage, SqlCommand, SqlCommandFactory, SqlResult } from '../src/sql-command.js';
8
8
  import type { CanBuildMigrations, MigrationBuilder, MigrationCode } from '../migrations/migration-types.js';
9
- export declare class KnexDataProvider implements DataProvider, HasWrapIdentifier, SqlCommandFactory, CanBuildMigrations {
9
+ export declare class KnexDataProvider implements DataProvider, HasWrapIdentifier, HasJsonColumnStorage, SqlCommandFactory, CanBuildMigrations {
10
10
  knex: Knex;
11
11
  constructor(knex: Knex);
12
12
  end(): Promise<void>;
@@ -15,6 +15,7 @@ export declare class KnexDataProvider implements DataProvider, HasWrapIdentifier
15
15
  execute(sql: string): Promise<SqlResult>;
16
16
  static getDb(dataProvider?: DataProvider): Knex<any, any[]>;
17
17
  wrapIdentifier: (name: string) => string;
18
+ get jsonColumnStorage(): JsonColumnStorage;
18
19
  getEntityDataProvider(entity: EntityMetadata<any>): EntityDataProvider;
19
20
  transaction(action: (dataProvider: DataProvider) => Promise<void>): Promise<void>;
20
21
  static rawFilter(build: CustomKnexFilterBuilderFunction): EntityFilter<any>;