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 CHANGED
@@ -4,6 +4,7 @@ export { getRelationInfo, getRelationFieldInfo, } from './src/remult3/relationIn
4
4
  export { decorateColumnSettings, controllerRefImpl, getControllerRef, fieldOptionsEnricher, } from './src/remult3/RepositoryImplementation.js';
5
5
  export { getEntitySettings } from './src/remult3/getEntityRef.js';
6
6
  export { __updateEntityBasedOnWhere } from './src/filter/filter-interfaces.js';
7
+ export { omitFilterField, walkFilter } from './src/filter/walk-filter.js';
7
8
  export const actionInfo = remultStatic.actionInfo;
8
9
  export { isOfType } from './src/isOfType.js';
9
10
  export { flags } from './src/remult3/remult3.js';
@@ -1,12 +1,12 @@
1
- import { CustomSqlFilterBuilder, dbNamesOfWithForceSqlExpression, } from '../src/filter/filter-consumer-bridge-to-sql-request.js';
1
+ import { CustomSqlFilterBuilder, dbNamesOfWithForceSqlExpression, escapeLike, LIKE_ESCAPE_SQL, } from '../src/filter/filter-consumer-bridge-to-sql-request.js';
2
2
  import { dbNamesOf, isDbReadonly, shouldCreateEntity, shouldNotCreateField, } from '../src/filter/filter-consumer-bridge-to-sql-request.js';
3
3
  import { customDatabaseFilterToken, Filter, } from '../src/filter/filter-interfaces.js';
4
4
  import { remult as remultContext } from '../src/remult-proxy.js';
5
5
  import { getRepository, isAutoIncrement, } from '../src/remult3/RepositoryImplementation.js';
6
6
  import { Sort } from '../src/sort.js';
7
- import { ValueConverters } from '../src/valueConverters.js';
7
+ import { fromDbColumn, toDbColumn, ValueConverters, } from '../src/valueConverters.js';
8
8
  import { getRepositoryInternals } from '../src/remult3/repository-internals.js';
9
- import { groupByImpl, getRowAfterUpdate, } from '../src/data-providers/sql-database.js';
9
+ import { groupByImpl, getRowAfterUpdate, jsonColumnStorageOf, } from '../src/data-providers/sql-database.js';
10
10
  export class KnexDataProvider {
11
11
  knex;
12
12
  constructor(knex) {
@@ -46,29 +46,14 @@ export class KnexDataProvider {
46
46
  return r.knex;
47
47
  }
48
48
  wrapIdentifier = (x) => this.knex.ref(x) + '';
49
- getEntityDataProvider(entity) {
49
+ get jsonColumnStorage() {
50
50
  if (!supportsJsonLoadingAndSaving(this.knex))
51
- for (const f of entity.fields.toArray()) {
52
- if (f.valueConverter.fieldTypeInDb === 'json') {
53
- //@ts-ignore
54
- f.valueConverter = {
55
- ...f.valueConverter,
56
- toDb: ValueConverters.JsonString.toDb,
57
- fromDb: ValueConverters.JsonString.fromDb,
58
- };
59
- }
60
- }
61
- else if (this.knex.client.config.client === 'mysql2') {
62
- for (const f of entity.fields.toArray()) {
63
- if (f.valueConverter.fieldTypeInDb === 'json') {
64
- //@ts-ignore
65
- f.valueConverter = {
66
- ...f.valueConverter,
67
- toDb: ValueConverters.JsonString.toDb,
68
- };
69
- }
70
- }
71
- }
51
+ return 'text';
52
+ if (this.knex.client.config.client === 'mysql2')
53
+ return 'textOnWrite';
54
+ return 'native';
55
+ }
56
+ getEntityDataProvider(entity) {
72
57
  return new KnexEntityDataProvider(entity, this.knex, this.wrapIdentifier, this);
73
58
  }
74
59
  async transaction(action) {
@@ -93,7 +78,7 @@ export class KnexDataProvider {
93
78
  if (!wrapIdentifier)
94
79
  wrapIdentifier = (x) => x;
95
80
  const repo = getRepository(entity);
96
- var b = new FilterConsumerBridgeToKnexRequest(await dbNamesOfWithForceSqlExpression(repo.metadata, wrapIdentifier), wrapIdentifier ?? wrapIdentifier);
81
+ var b = new FilterConsumerBridgeToKnexRequest(await dbNamesOfWithForceSqlExpression(repo.metadata, wrapIdentifier), wrapIdentifier ?? wrapIdentifier, jsonColumnStorageOf(repo));
97
82
  b._addWhere = false;
98
83
  await (await getRepositoryInternals(repo)._translateWhereToFilter(condition)).__applyToConsumer(b);
99
84
  let r = await b.resolveWhere();
@@ -119,7 +104,7 @@ class KnexEntityDataProvider {
119
104
  async groupBy(options) {
120
105
  return groupByImpl(options, await dbNamesOf(this.entity, this.dp.wrapIdentifier), this.dp.createCommand(), false, (limit, offset) => this.knex.client.config.client?.includes('mssql')
121
106
  ? ' offset ' + offset + ' rows fetch next ' + limit + ' rows only'
122
- : ' limit ' + limit + ' offset ' + offset);
107
+ : ' limit ' + limit + ' offset ' + offset, this.dp.jsonColumnStorage);
123
108
  }
124
109
  getEntityFrom(e) {
125
110
  if (this.entity.options.sqlExpression)
@@ -128,7 +113,7 @@ class KnexEntityDataProvider {
128
113
  }
129
114
  async count(where) {
130
115
  const e = await this.init();
131
- const br = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier);
116
+ const br = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier, this.dp.jsonColumnStorage);
132
117
  where.__applyToConsumer(br);
133
118
  let r = await br.resolveWhere();
134
119
  const result = await this.getEntityFrom(e)
@@ -162,7 +147,7 @@ class KnexEntityDataProvider {
162
147
  }
163
148
  let query = this.getEntityFrom(e).select(cols);
164
149
  if (options?.where) {
165
- const br = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier);
150
+ const br = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier, this.dp.jsonColumnStorage);
166
151
  options.where.__applyToConsumer(br);
167
152
  let r = await br.resolveWhere();
168
153
  query.where((b) => r.forEach((y) => y(b)));
@@ -188,7 +173,7 @@ class KnexEntityDataProvider {
188
173
  for (let m in y) {
189
174
  let field = colKeys[i++];
190
175
  try {
191
- result[field.key] = field.valueConverter.fromDb(y[m]);
176
+ result[field.key] = fromDbColumn(field, y[m], this.dp.jsonColumnStorage);
192
177
  }
193
178
  catch (err) {
194
179
  throw new Error('Failed to load from db:' + field.key + '\r\n' + err);
@@ -212,14 +197,14 @@ class KnexEntityDataProvider {
212
197
  }
213
198
  async update(id, data, options) {
214
199
  const e = await this.init();
215
- let f = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier);
200
+ let f = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier, this.dp.jsonColumnStorage);
216
201
  Filter.fromEntityFilter(this.entity, this.entity.idMetadata.getIdFilter(id)).__applyToConsumer(f);
217
202
  let updateObject = {};
218
203
  for (const x of this.entity.fields) {
219
204
  if (isDbReadonly(x, e)) {
220
205
  }
221
206
  else if (data[x.key] !== undefined) {
222
- let v = translateValueAndHandleArrayAndHandleArray(x, data[x.key]);
207
+ let v = translateValueAndHandleArrayAndHandleArray(x, data[x.key], this.dp.jsonColumnStorage);
223
208
  if (v !== undefined) {
224
209
  let key = await e.$dbNameOf(x);
225
210
  updateObject[key] = v;
@@ -238,7 +223,7 @@ class KnexEntityDataProvider {
238
223
  if (ids.length === 0)
239
224
  return;
240
225
  const e = await this.init();
241
- let f = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier);
226
+ let f = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier, this.dp.jsonColumnStorage);
242
227
  Filter.fromEntityFilter(this.entity, this.entity.idMetadata.getIdFilter(...ids)).__applyToConsumer(f);
243
228
  let where = await f.resolveWhere();
244
229
  await this.getEntityFrom(e)
@@ -257,7 +242,7 @@ class KnexEntityDataProvider {
257
242
  return result;
258
243
  }
259
244
  const e = await this.init();
260
- const batches = splitByMaxParameters(data, (row) => countKnexInsertBindParameters(this.entity, e, row), knexMaxParametersInOneSqlStatement(this.knex));
245
+ const batches = splitByMaxParameters(data, (row) => countKnexInsertBindParameters(this.entity, e, row, this.dp.jsonColumnStorage), knexMaxParametersInOneSqlStatement(this.knex));
261
246
  const result = [];
262
247
  for (const batch of batches)
263
248
  result.push(...(await this.insertBatch(batch, e, options)));
@@ -268,15 +253,14 @@ class KnexEntityDataProvider {
268
253
  for (const x of this.entity.fields) {
269
254
  if (isDbReadonly(x, e))
270
255
  continue;
271
- if (batch.some((row) => translateValueAndHandleArrayAndHandleArray(x, row[x.key]) !=
272
- undefined))
256
+ if (batch.some((row) => translateValueAndHandleArrayAndHandleArray(x, row[x.key], this.dp.jsonColumnStorage) != undefined))
273
257
  cols.push(x);
274
258
  }
275
259
  const insertObjects = [];
276
260
  for (const row of batch) {
277
261
  const insertObject = {};
278
262
  for (const x of cols) {
279
- const v = translateValueAndHandleArrayAndHandleArray(x, row[x.key]);
263
+ const v = translateValueAndHandleArrayAndHandleArray(x, row[x.key], this.dp.jsonColumnStorage);
280
264
  const key = await e.$dbNameOf(x);
281
265
  if (v != undefined)
282
266
  insertObject[key] = v;
@@ -291,9 +275,7 @@ class KnexEntityDataProvider {
291
275
  const wantRows = options?.select !== 'none';
292
276
  if (isAutoIncrement(this.entity.idMetadata.field) && wantRows) {
293
277
  const result = await insert.returning(this.entity.idMetadata.field.dbName);
294
- const ids = result.map((r) => r && typeof r === 'object'
295
- ? r[this.entity.idMetadata.field.dbName]
296
- : r);
278
+ const ids = result.map((r) => r && typeof r === 'object' ? r[this.entity.idMetadata.field.dbName] : r);
297
279
  return this.loadRowsByIds(ids);
298
280
  }
299
281
  await insert;
@@ -321,7 +303,7 @@ class KnexEntityDataProvider {
321
303
  if (isDbReadonly(x, e)) {
322
304
  }
323
305
  else {
324
- let v = translateValueAndHandleArrayAndHandleArray(x, data[x.key]);
306
+ let v = translateValueAndHandleArrayAndHandleArray(x, data[x.key], this.dp.jsonColumnStorage);
325
307
  if (v != undefined) {
326
308
  let key = await e.$dbNameOf(x);
327
309
  insertObject[key] = v;
@@ -357,6 +339,7 @@ class KnexEntityDataProvider {
357
339
  class FilterConsumerBridgeToKnexRequest {
358
340
  innerNameProvider;
359
341
  rawSqlWrapIdentifier;
342
+ jsonColumnStorage;
360
343
  _addWhere = true;
361
344
  promises = [];
362
345
  result = [];
@@ -370,9 +353,10 @@ class FilterConsumerBridgeToKnexRequest {
370
353
  }
371
354
  return this.result;
372
355
  }
373
- constructor(innerNameProvider, rawSqlWrapIdentifier) {
356
+ constructor(innerNameProvider, rawSqlWrapIdentifier, jsonColumnStorage) {
374
357
  this.innerNameProvider = innerNameProvider;
375
358
  this.rawSqlWrapIdentifier = rawSqlWrapIdentifier;
359
+ this.jsonColumnStorage = jsonColumnStorage;
376
360
  }
377
361
  custom(key, customItem) {
378
362
  throw new Error('Custom filter should be translated before it gets here');
@@ -381,7 +365,7 @@ class FilterConsumerBridgeToKnexRequest {
381
365
  this.promises.push((async () => {
382
366
  const result = [];
383
367
  for (const element of orElements) {
384
- let f = new FilterConsumerBridgeToKnexRequest(this.innerNameProvider, this.rawSqlWrapIdentifier);
368
+ let f = new FilterConsumerBridgeToKnexRequest(this.innerNameProvider, this.rawSqlWrapIdentifier, this.jsonColumnStorage);
385
369
  f._addWhere = false;
386
370
  element.__applyToConsumer(f);
387
371
  let where = await f.resolveWhere();
@@ -402,7 +386,7 @@ class FilterConsumerBridgeToKnexRequest {
402
386
  }
403
387
  not(element) {
404
388
  this.promises.push((async () => {
405
- let f = new FilterConsumerBridgeToKnexRequest(this.innerNameProvider, this.rawSqlWrapIdentifier);
389
+ let f = new FilterConsumerBridgeToKnexRequest(this.innerNameProvider, this.rawSqlWrapIdentifier, this.jsonColumnStorage);
406
390
  f._addWhere = false;
407
391
  element.__applyToConsumer(f);
408
392
  let where = await f.resolveWhere();
@@ -424,7 +408,7 @@ class FilterConsumerBridgeToKnexRequest {
424
408
  this.result.push((b) => b.whereNotNull(this.innerNameProvider.$dbNameOf(col)));
425
409
  }
426
410
  isIn(col, val) {
427
- this.result.push((knex) => knex.whereIn(this.innerNameProvider.$dbNameOf(col), val.map((x) => translateValueAndHandleArrayAndHandleArray(col, x))));
411
+ this.result.push((knex) => knex.whereIn(this.innerNameProvider.$dbNameOf(col), val.map((x) => translateValueAndHandleArrayAndHandleArray(col, x, this.jsonColumnStorage))));
428
412
  }
429
413
  isEqualTo(col, val) {
430
414
  this.add(col, val, '=');
@@ -445,39 +429,27 @@ class FilterConsumerBridgeToKnexRequest {
445
429
  this.add(col, val, '<');
446
430
  }
447
431
  containsCaseInsensitive(col, val) {
448
- this.result.push((b) => b.whereRaw('lower (' +
449
- b.client.ref(this.innerNameProvider.$dbNameOf(col)) +
450
- ") like lower ('%" +
451
- val.replace(/'/g, "''") +
452
- "%')"));
453
- this.promises.push((async () => { })());
432
+ this.like(col, '%' + escapeLike(val) + '%');
454
433
  }
455
434
  notContainsCaseInsensitive(col, val) {
456
- this.result.push((b) => b.whereRaw('not lower (' +
457
- b.client.ref(this.innerNameProvider.$dbNameOf(col)) +
458
- ") like lower ('%" +
459
- val.replace(/'/g, "''") +
460
- "%')"));
461
- this.promises.push((async () => { })());
435
+ this.like(col, '%' + escapeLike(val) + '%', 'not ');
462
436
  }
463
437
  startsWithCaseInsensitive(col, val) {
464
- this.result.push((b) => b.whereRaw('lower (' +
465
- b.client.ref(this.innerNameProvider.$dbNameOf(col)) +
466
- ") like lower ('" +
467
- val.replace(/'/g, "''") +
468
- "%')"));
469
- this.promises.push((async () => { })());
438
+ this.like(col, escapeLike(val) + '%');
470
439
  }
471
440
  endsWithCaseInsensitive(col, val) {
472
- this.result.push((b) => b.whereRaw('lower (' +
441
+ this.like(col, '%' + escapeLike(val));
442
+ }
443
+ like(col, pattern, not = '') {
444
+ this.result.push((b) => b.whereRaw(not +
445
+ 'lower (' +
473
446
  b.client.ref(this.innerNameProvider.$dbNameOf(col)) +
474
- ") like lower ('%" +
475
- val.replace(/'/g, "''") +
476
- "')"));
447
+ ') like lower (?)' +
448
+ LIKE_ESCAPE_SQL, [pattern]));
477
449
  this.promises.push((async () => { })());
478
450
  }
479
451
  add(col, val, operator) {
480
- this.result.push((b) => b.where(this.innerNameProvider.$dbNameOf(col), operator, translateValueAndHandleArrayAndHandleArray(col, val)));
452
+ this.result.push((b) => b.where(this.innerNameProvider.$dbNameOf(col), operator, translateValueAndHandleArrayAndHandleArray(col, val, this.jsonColumnStorage)));
481
453
  }
482
454
  databaseCustom(databaseCustom) {
483
455
  this.promises.push((async () => {
@@ -486,7 +458,7 @@ class FilterConsumerBridgeToKnexRequest {
486
458
  }
487
459
  if (databaseCustom?.buildSql) {
488
460
  let r = new KnexCommandHelper();
489
- const item = new CustomSqlFilterBuilder(r, this.rawSqlWrapIdentifier);
461
+ const item = new CustomSqlFilterBuilder(r, this.rawSqlWrapIdentifier, this.jsonColumnStorage);
490
462
  let sql = await databaseCustom.buildSql(item);
491
463
  if (typeof sql !== 'string')
492
464
  sql = item.sql;
@@ -691,18 +663,19 @@ function splitByMaxParameters(items, parametersInItem, maxParameters) {
691
663
  batches.push(batch);
692
664
  return batches;
693
665
  }
694
- function countKnexInsertBindParameters(entity, e, row) {
666
+ function countKnexInsertBindParameters(entity, e, row, storage) {
695
667
  let n = 0;
696
668
  for (const x of entity.fields) {
697
669
  if (isDbReadonly(x, e))
698
670
  continue;
699
- if (translateValueAndHandleArrayAndHandleArray(x, row[x.key]) != undefined)
671
+ if (translateValueAndHandleArrayAndHandleArray(x, row[x.key], storage) !=
672
+ undefined)
700
673
  n++;
701
674
  }
702
675
  return n;
703
676
  }
704
- function translateValueAndHandleArrayAndHandleArray(field, val) {
705
- let result = field.valueConverter.toDb(val);
677
+ function translateValueAndHandleArrayAndHandleArray(field, val, storage) {
678
+ let result = toDbColumn(field, val, storage);
706
679
  if (Array.isArray(result))
707
680
  return JSON.stringify(result);
708
681
  return result;
@@ -2,6 +2,7 @@ import { doTransaction } from './context.js';
2
2
  import { EntityError } from './data-interfaces.js';
3
3
  import { findOptionsToJson, liveQueryAction, } from './data-providers/rest-data-provider.js';
4
4
  import { Filter, buildFilterFromRequestParameters, customUrlToken, } from './filter/filter-interfaces.js';
5
+ import { omitFilterField, walkFilter } from './filter/walk-filter.js';
5
6
  import { getRelationFieldInfo } from './remult3/relationInfoMember.js';
6
7
  import { GroupByCountMember, GroupByForApiKey, } from './remult3/remult3.js';
7
8
  import { ForbiddenError } from './server-action.js';
@@ -27,10 +28,7 @@ export class DataApi {
27
28
  return this.getArray(res, req, undefined);
28
29
  }
29
30
  catch (err) {
30
- if (err.isForbiddenError)
31
- res.forbidden(err.message);
32
- else
33
- res.error(err, this.repository.metadata);
31
+ this.handleError(res, err);
34
32
  }
35
33
  }
36
34
  async httpPost(res, req, body, serializeContext) {
@@ -76,10 +74,7 @@ export class DataApi {
76
74
  }
77
75
  }
78
76
  catch (err) {
79
- if (err.isForbiddenError)
80
- res.forbidden(err.message);
81
- else
82
- res.error(err, this.repository.metadata);
77
+ this.handleError(res, err);
83
78
  }
84
79
  }
85
80
  async upsertMany(response, request, body) {
@@ -130,10 +125,7 @@ export class DataApi {
130
125
  });
131
126
  }
132
127
  catch (err) {
133
- if (err.isForbiddenError)
134
- response.forbidden();
135
- else
136
- response.error(err, this.repository.metadata);
128
+ this.handleError(response, err);
137
129
  }
138
130
  }
139
131
  static defaultGetLimit = 0;
@@ -155,7 +147,7 @@ export class DataApi {
155
147
  });
156
148
  }
157
149
  catch (err) {
158
- response.error(err, this.repository.metadata);
150
+ this.handleError(response, err);
159
151
  }
160
152
  }
161
153
  async deleteMany(response, request, body) {
@@ -175,7 +167,7 @@ export class DataApi {
175
167
  });
176
168
  }
177
169
  catch (err) {
178
- response.error(err, this.repository.metadata);
170
+ this.handleError(response, err);
179
171
  }
180
172
  }
181
173
  async prepareWhereForManyOperation(body, request) {
@@ -196,7 +188,7 @@ export class DataApi {
196
188
  for (const element of body?.orderBy) {
197
189
  const direction = element.isDescending ? 'desc' : 'asc';
198
190
  if (element.operation !== 'count' &&
199
- !this.repository.fields.find(element.field)?.includedInApi())
191
+ !this.fieldIsInApi(element.field))
200
192
  continue;
201
193
  switch (element.operation) {
202
194
  case undefined:
@@ -214,9 +206,9 @@ export class DataApi {
214
206
  }
215
207
  }
216
208
  }
217
- const group = body?.groupBy?.filter((x) => this.repository.fields.find(x).includedInApi());
218
- const min = body?.min?.filter((x) => this.repository.fields.find(x).includedInApi());
219
- const max = body?.max?.filter((x) => this.repository.fields.find(x).includedInApi());
209
+ const group = this.fieldsInApi(body?.groupBy);
210
+ const min = this.fieldsInApi(body?.min);
211
+ const max = this.fieldsInApi(body?.max);
220
212
  let result = await this.repository.groupBy({
221
213
  where: findOptions.where,
222
214
  limit: findOptions.limit,
@@ -224,11 +216,11 @@ export class DataApi {
224
216
  //@ts-expect-error internal key
225
217
  [GroupByForApiKey]: true,
226
218
  group,
227
- sum: body?.sum?.filter((x) => this.repository.fields.find(x).includedInApi()),
228
- avg: body?.avg?.filter((x) => this.repository.fields.find(x).includedInApi()),
219
+ sum: this.fieldsInApi(body?.sum),
220
+ avg: this.fieldsInApi(body?.avg),
229
221
  min,
230
222
  max,
231
- distinctCount: body?.distinctCount?.filter((x) => this.repository.fields.find(x).includedInApi()),
223
+ distinctCount: this.fieldsInApi(body?.distinctCount),
232
224
  orderBy: orderBy,
233
225
  });
234
226
  if (group)
@@ -269,7 +261,7 @@ export class DataApi {
269
261
  let sort = request.get('_sort');
270
262
  if (sort != undefined) {
271
263
  let dir = request.get('_order');
272
- findOptions.orderBy = determineSort(sort, dir);
264
+ findOptions.orderBy = this.omitFieldsHiddenFromApi(determineSort(sort, dir));
273
265
  }
274
266
  let limit = +request.get('_limit');
275
267
  if (!limit && DataApi.defaultGetLimit)
@@ -341,10 +333,7 @@ export class DataApi {
341
333
  response.success(r);
342
334
  }
343
335
  catch (err) {
344
- if (err.isForbiddenError)
345
- response.forbidden(err.message);
346
- else
347
- response.error(err, this.repository.metadata);
336
+ this.handleError(response, err);
348
337
  }
349
338
  }
350
339
  async liveQuery(response, request, body, serializeContext, queryChannel) {
@@ -368,10 +357,7 @@ export class DataApi {
368
357
  response.success(r.r);
369
358
  }
370
359
  catch (err) {
371
- if (err.isForbiddenError)
372
- response.forbidden();
373
- else
374
- response.error(err, this.repository.metadata);
360
+ this.handleError(response, err);
375
361
  }
376
362
  }
377
363
  async buildWhere(request, body) {
@@ -396,6 +382,7 @@ export class DataApi {
396
382
  return result;
397
383
  },
398
384
  });
385
+ f = this.omitFieldsHiddenFromApi(f);
399
386
  if (this.repository.metadata.options.apiPreprocessFilter) {
400
387
  f = await this.repository.metadata.options.apiPreprocessFilter(f, {
401
388
  metadata: this.repository.metadata,
@@ -408,6 +395,30 @@ export class DataApi {
408
395
  }
409
396
  return { $and: where };
410
397
  }
398
+ // A hidden field is not on the api, same as a field that is not on the entity.
399
+ fieldIsInApi(key) {
400
+ return this.repository.fields.find(key)?.includedInApi() === true;
401
+ }
402
+ fieldsInApi(keys) {
403
+ return keys?.filter((key) => this.fieldIsInApi(key));
404
+ }
405
+ // Drop a field the api hides. Same as a field that is not on the entity:
406
+ // refusing it would confirm the name exists.
407
+ omitFieldsHiddenFromApi(filterOrSort) {
408
+ const fields = this.repository.metadata.fields;
409
+ return walkFilter(filterOrSort, function (key, value) {
410
+ if (!key.startsWith(customUrlToken) &&
411
+ fields.find(key)?.includedInApi() === false)
412
+ return omitFilterField;
413
+ return value;
414
+ });
415
+ }
416
+ handleError(response, err) {
417
+ if (err.isForbiddenError)
418
+ response.forbidden(err.message);
419
+ else
420
+ response.error(err, this.repository.metadata);
421
+ }
411
422
  async doOnId(response, id, what) {
412
423
  try {
413
424
  var where = [
@@ -467,7 +478,7 @@ export class DataApi {
467
478
  });
468
479
  }
469
480
  catch (err) {
470
- response.error(err, this.repository.metadata);
481
+ this.handleError(response, err);
471
482
  }
472
483
  }
473
484
  async actualUpdate(row, body, req, doNotSelect) {