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.
- 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 +44 -39
- 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 +29 -14
- 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 -7
- package/src/data-providers/sql-database.js +100 -90
- 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 +1 -0
- package/src/remult3/RepositoryImplementation.js +29 -14
- package/src/remult3/remult3.d.ts +37 -0
- 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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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;
|
|
@@ -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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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 (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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, [
|
|
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
package/remult-knex/index.d.ts
CHANGED
|
@@ -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>;
|