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.
- package/esm/internals.js +1 -0
- package/esm/remult-knex/index.js +46 -73
- package/esm/src/data-api.js +42 -31
- package/esm/src/data-providers/sql-database.js +46 -42
- package/esm/src/filter/filter-consumer-bridge-to-sql-request.js +35 -34
- package/esm/src/filter/walk-filter.js +29 -0
- package/esm/src/remult-proxy.js +6 -6
- package/esm/src/remult3/RepositoryImplementation.js +49 -29
- package/esm/src/sort.js +0 -3
- package/esm/src/valueConverters.js +11 -0
- package/index.d.ts +1 -1
- package/internals.d.ts +1 -0
- package/internals.js +4 -1
- package/package.json +1 -1
- package/remult-knex/index.d.ts +3 -2
- package/remult-knex/index.js +118 -164
- package/src/data-api.d.ts +4 -0
- package/src/data-api.js +51 -48
- package/src/data-providers/sql-database.d.ts +16 -8
- package/src/data-providers/sql-database.js +90 -81
- package/src/filter/filter-consumer-bridge-to-sql-request.d.ts +8 -3
- package/src/filter/filter-consumer-bridge-to-sql-request.js +40 -47
- package/src/filter/walk-filter.d.ts +3 -0
- package/src/filter/walk-filter.js +56 -0
- package/src/remult-proxy.js +12 -27
- package/src/remult3/RepositoryImplementation.d.ts +4 -2
- package/src/remult3/RepositoryImplementation.js +48 -30
- package/src/remult3/remult3.d.ts +37 -0
- package/src/sort.js +0 -3
- package/src/sql-command.d.ts +5 -0
- package/src/valueConverters.d.ts +4 -1
- package/src/valueConverters.js +13 -0
|
@@ -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 {
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
127
|
+
this.addToWhere(not +
|
|
128
|
+
'lower (' +
|
|
131
129
|
this.nameProvider.$dbNameOf(col) +
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
195
|
-
val = field.
|
|
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
|
+
}
|
package/esm/src/remult-proxy.js
CHANGED
|
@@ -124,10 +124,10 @@ export class RemultProxy {
|
|
|
124
124
|
.remultFactory()
|
|
125
125
|
.repo(...args)
|
|
126
126
|
.updateMany(...args2),
|
|
127
|
-
find: (
|
|
127
|
+
find: (options) => self
|
|
128
128
|
.remultFactory()
|
|
129
129
|
.repo(...args)
|
|
130
|
-
.find(
|
|
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: (
|
|
141
|
+
findFirst: (where, options) => self
|
|
142
142
|
.remultFactory()
|
|
143
143
|
.repo(...args)
|
|
144
|
-
.findFirst(
|
|
145
|
-
findOne: (
|
|
144
|
+
.findFirst(where, options),
|
|
145
|
+
findOne: (options) => self
|
|
146
146
|
.remultFactory()
|
|
147
147
|
.repo(...args)
|
|
148
|
-
.findOne(
|
|
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
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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, [
|
|
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
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
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 (
|
|
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(
|
|
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;
|