remult 0.27.7 → 0.27.8-next.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.
@@ -115,6 +115,9 @@ class KnexEntityDataProvider {
115
115
  this.knex = knex;
116
116
  this.rawSqlWrapIdentifier = rawSqlWrapIdentifier;
117
117
  }
118
+ aggregate(options) {
119
+ throw new Error('Method not implemented.');
120
+ }
118
121
  async count(where) {
119
122
  const e = await this.init();
120
123
  const br = new FilterConsumerBridgeToKnexRequest(e, this.rawSqlWrapIdentifier);
@@ -70,6 +70,9 @@ class MongoEntityDataProvider {
70
70
  this.entity = entity;
71
71
  this.session = session;
72
72
  }
73
+ aggregate(options) {
74
+ throw new Error('Method not implemented.');
75
+ }
73
76
  translateFromDb(row, nameProvider) {
74
77
  let result = {};
75
78
  for (const col of this.entity.fields) {
@@ -2,6 +2,7 @@ import { doTransaction } from './context.js';
2
2
  import { findOptionsToJson, liveQueryAction, } from './data-providers/rest-data-provider.js';
3
3
  import { Filter, buildFilterFromRequestParameters, customUrlToken, } from './filter/filter-interfaces.js';
4
4
  import { getRelationFieldInfo } from './remult3/relationInfoMember.js';
5
+ import { AggregateCountMember, } from './remult3/remult3.js';
5
6
  import { ForbiddenError } from './server-action.js';
6
7
  export class DataApi {
7
8
  repository;
@@ -18,6 +19,8 @@ export class DataApi {
18
19
  case 'get':
19
20
  case 'count':
20
21
  return this.count(res, req, undefined);
22
+ case 'aggregate':
23
+ return this.aggregate(res, req, undefined);
21
24
  }
22
25
  return this.getArray(res, req, undefined);
23
26
  }
@@ -42,6 +45,8 @@ export class DataApi {
42
45
  case 'count':
43
46
  validateWhereInBody();
44
47
  return this.count(res, req, body);
48
+ case 'aggregate':
49
+ return this.aggregate(res, req, body);
45
50
  case 'deleteMany':
46
51
  validateWhereInBody();
47
52
  return this.deleteMany(res, req, body);
@@ -98,7 +103,55 @@ export class DataApi {
98
103
  response.error(err, this.repository.metadata);
99
104
  }
100
105
  }
106
+ async aggregate(response, request, body) {
107
+ let findOptions = await this.findOptionsFromRequest(request, body);
108
+ let orderBy = {};
109
+ if (body?.orderBy) {
110
+ for (const element of body?.orderBy) {
111
+ const direction = element.isDescending ? 'desc' : 'asc';
112
+ switch (element.operation) {
113
+ case 'sum':
114
+ orderBy[element.field] = {
115
+ ...orderBy[element.field],
116
+ sum: direction,
117
+ };
118
+ break;
119
+ case 'average':
120
+ orderBy[element.field] = {
121
+ ...orderBy[element.field],
122
+ average: direction,
123
+ };
124
+ break;
125
+ case 'count':
126
+ orderBy[AggregateCountMember] = direction;
127
+ default:
128
+ orderBy[element.field] = direction;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ let result = await this.repository.aggregate({
134
+ where: findOptions.where,
135
+ limit: findOptions.limit,
136
+ page: findOptions.page,
137
+ groupBy: body?.groupBy?.filter((x) => this.repository.fields.find(x).includedInApi()),
138
+ sum: body?.sum?.filter((x) => this.repository.fields.find(x).includedInApi()),
139
+ average: body?.average?.filter((x) => this.repository.fields.find(x).includedInApi()),
140
+ orderBy: orderBy,
141
+ });
142
+ if (!Array.isArray(result)) {
143
+ result = [result];
144
+ }
145
+ response.success(result);
146
+ }
101
147
  async getArrayImpl(response, request, body) {
148
+ let findOptions = await this.findOptionsFromRequest(request, body);
149
+ const r = await this.repository.find(findOptions).then(async (r) => {
150
+ return await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson()));
151
+ });
152
+ return { r, findOptions };
153
+ }
154
+ async findOptionsFromRequest(request, body) {
102
155
  let findOptions = {
103
156
  load: () => [],
104
157
  include: this.includeNone(),
@@ -147,14 +200,10 @@ export class DataApi {
147
200
  });
148
201
  }
149
202
  if (!hasId) {
150
- response.forbidden();
151
203
  throw new ForbiddenError();
152
204
  }
153
205
  }
154
- const r = await this.repository.find(findOptions).then(async (r) => {
155
- return await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson()));
156
- });
157
- return { r, findOptions };
206
+ return findOptions;
158
207
  }
159
208
  includeNone() {
160
209
  let include = {};
@@ -1,6 +1,8 @@
1
1
  import { CompoundIdField } from '../CompoundIdField.js';
2
2
  import { dbNamesOf, isDbReadonly, } from '../filter/filter-consumer-bridge-to-sql-request.js';
3
3
  import { Filter, customDatabaseFilterToken, } from '../filter/filter-interfaces.js';
4
+ import { AggregateCountMember, } from '../remult3/remult3.js';
5
+ import { compareForSort, Sort } from '../sort.js';
4
6
  export class ArrayEntityDataProvider {
5
7
  entity;
6
8
  rows;
@@ -15,6 +17,126 @@ export class ArrayEntityDataProvider {
15
17
  this.entity = entity;
16
18
  this.rows = rows;
17
19
  }
20
+ async aggregate(options) {
21
+ const sort = new Sort();
22
+ if (options?.groupBy)
23
+ for (const field of options?.groupBy) {
24
+ sort.Segments.push({ field: field });
25
+ }
26
+ const rows = await this.find({ orderBy: sort, where: options?.where });
27
+ let result = [];
28
+ let group = {};
29
+ let first = true;
30
+ let count = 0;
31
+ let aggregates = [];
32
+ if (options?.sum) {
33
+ class Sum {
34
+ key;
35
+ constructor(key) {
36
+ this.key = key;
37
+ }
38
+ sum = 0;
39
+ process(row) {
40
+ const val = row[this.key];
41
+ if (val !== undefined && val !== null)
42
+ this.sum += row[this.key];
43
+ }
44
+ finishGroup(result) {
45
+ result[this.key] = { ...result[this.key], sum: this.sum };
46
+ this.sum = 0;
47
+ }
48
+ }
49
+ for (const element of options.sum) {
50
+ aggregates.push(new Sum(element.key));
51
+ }
52
+ }
53
+ if (options?.average) {
54
+ class Average {
55
+ key;
56
+ constructor(key) {
57
+ this.key = key;
58
+ }
59
+ sum = 0;
60
+ count = 0;
61
+ process(row) {
62
+ const val = row[this.key];
63
+ if (val !== undefined && val !== null) {
64
+ this.sum += row[this.key];
65
+ this.count++;
66
+ }
67
+ }
68
+ finishGroup(result) {
69
+ result[this.key] = {
70
+ ...result[this.key],
71
+ average: this.sum / this.count,
72
+ };
73
+ this.sum = 0;
74
+ this.count = 0;
75
+ }
76
+ }
77
+ for (const element of options.average) {
78
+ aggregates.push(new Average(element.key));
79
+ }
80
+ }
81
+ function finishGroup() {
82
+ const r = { ...group, $count: count };
83
+ for (const a of aggregates) {
84
+ a.finishGroup(r);
85
+ }
86
+ r[AggregateCountMember] = count;
87
+ result.push(r);
88
+ first = true;
89
+ count = 0;
90
+ }
91
+ for (const row of rows) {
92
+ if (options?.groupBy) {
93
+ if (!first) {
94
+ for (const field of options?.groupBy) {
95
+ if (group[field.key] != row[field.key]) {
96
+ finishGroup();
97
+ break;
98
+ }
99
+ }
100
+ }
101
+ if (first) {
102
+ for (const field of options?.groupBy) {
103
+ group[field.key] = row[field.key];
104
+ }
105
+ }
106
+ }
107
+ for (const a of aggregates) {
108
+ a.process(row);
109
+ }
110
+ count++;
111
+ first = false;
112
+ }
113
+ finishGroup();
114
+ if (options?.orderBy) {
115
+ result.sort((a, b) => {
116
+ for (const x of options.orderBy) {
117
+ const getValue = (row) => {
118
+ if (!x.field && x.operation == 'count') {
119
+ return row[AggregateCountMember];
120
+ }
121
+ else {
122
+ switch (x.operation) {
123
+ case 'sum':
124
+ return row[x.field.key].sum;
125
+ case 'average':
126
+ return row[x.field.key].average;
127
+ }
128
+ }
129
+ return row[x.field.key];
130
+ };
131
+ let compare = compareForSort(getValue(a), getValue(b), x.isDescending);
132
+ if (compare != 0)
133
+ return compare;
134
+ }
135
+ return 0;
136
+ });
137
+ }
138
+ return pageArray(result, { page: options?.page, limit: options?.limit });
139
+ }
18
140
  //@internal
19
141
  __names;
20
142
  //@internal
@@ -22,6 +22,9 @@ class JsonEntityDataProvider {
22
22
  this.helper = helper;
23
23
  this.formatted = formatted;
24
24
  }
25
+ aggregate(options) {
26
+ return (this.p = this.p.then(() => this.loadEntityData((dp, save) => dp.aggregate(options))));
27
+ }
25
28
  async loadEntityData(what) {
26
29
  let data = [];
27
30
  let dbName = await this.entity.dbName;
@@ -101,6 +101,27 @@ export class RestEntityDataProvider {
101
101
  this.http = http;
102
102
  this.entity = entity;
103
103
  }
104
+ async aggregate(options) {
105
+ const { run } = this.buildFindRequest({
106
+ where: options?.where,
107
+ limit: options?.limit,
108
+ page: options?.page,
109
+ });
110
+ const body = {
111
+ groupBy: options?.groupBy?.map((x) => x.key),
112
+ sum: options?.sum?.map((x) => x.key),
113
+ average: options?.average?.map((x) => x.key),
114
+ orderBy: options?.orderBy?.map((x) => ({ ...x, field: x.field?.key })),
115
+ };
116
+ const result = await run('aggregate', Object.keys(body).length > 0 ? body : undefined);
117
+ if (options?.groupBy)
118
+ result.forEach((row) => {
119
+ for (const g of options.groupBy) {
120
+ row[g.key] = g.valueConverter.fromJson(row[g.key]);
121
+ }
122
+ });
123
+ return result;
124
+ }
104
125
  translateFromJson(row) {
105
126
  let result = {};
106
127
  for (const col of this.entity.fields) {
@@ -176,8 +197,9 @@ export class RestEntityDataProvider {
176
197
  u.add('__action', action);
177
198
  if (filterObject) {
178
199
  body = { set: body, where: filterObject };
179
- return this.http().post(u.url, body);
180
200
  }
201
+ if (body)
202
+ return this.http().post(u.url, body);
181
203
  else
182
204
  return this.http()[method](u.url, body);
183
205
  };
@@ -1,6 +1,7 @@
1
1
  import { FilterConsumerBridgeToSqlRequest, dbNamesOfWithForceSqlExpression, isDbReadonly, } from '../filter/filter-consumer-bridge-to-sql-request.js';
2
2
  import { Filter, customDatabaseFilterToken, } from '../filter/filter-interfaces.js';
3
3
  import { remult as defaultRemult } from '../remult-proxy.js';
4
+ import { AggregateCountMember, } from '../remult3/remult3.js';
4
5
  import { getRepository, isAutoIncrement, } from '../remult3/RepositoryImplementation.js';
5
6
  import { Sort } from '../sort.js';
6
7
  import { ValueConverters } from '../valueConverters.js';
@@ -286,6 +287,114 @@ class ActualSQLEntityDataProvider {
286
287
  return Number(r.rows[0].count);
287
288
  });
288
289
  }
290
+ async aggregate(options) {
291
+ let e = await this.init();
292
+ let select = 'select count(*) as count';
293
+ let groupBy = '';
294
+ const processResultRow = [];
295
+ processResultRow.push((sqlVal, theResult) => {
296
+ theResult[AggregateCountMember] = Number(sqlVal);
297
+ });
298
+ if (options?.groupBy)
299
+ for (const x of options?.groupBy) {
300
+ if (x.isServerExpression) {
301
+ }
302
+ else {
303
+ select += ', ' + e.$dbNameOf(x);
304
+ if (x.options.sqlExpression)
305
+ select += ' as ' + x.key;
306
+ if (groupBy == '')
307
+ groupBy = ' group by ';
308
+ else
309
+ groupBy += ', ';
310
+ groupBy += e.$dbNameOf(x);
311
+ }
312
+ processResultRow.push((sqlResult, theResult) => {
313
+ theResult[x.key] = x.valueConverter.fromDb(sqlResult);
314
+ });
315
+ }
316
+ if (options?.sum)
317
+ for (const x of options?.sum) {
318
+ if (x.isServerExpression) {
319
+ }
320
+ else {
321
+ select += ', sum(' + e.$dbNameOf(x) + ') as ' + x.key + '_sum';
322
+ }
323
+ processResultRow.push((sqlResult, theResult) => {
324
+ theResult[x.key] = { ...theResult[x.key], sum: sqlResult };
325
+ });
326
+ }
327
+ if (options?.average)
328
+ for (const x of options?.average) {
329
+ if (x.isServerExpression) {
330
+ }
331
+ else {
332
+ select += ', avg(' + e.$dbNameOf(x) + ') as ' + x.key + '_average';
333
+ }
334
+ processResultRow.push((sqlResult, theResult) => {
335
+ theResult[x.key] = { ...theResult[x.key], average: sqlResult };
336
+ });
337
+ }
338
+ select += '\n from ' + e.$entityName;
339
+ let r = this.sql.createCommand();
340
+ if (options?.where) {
341
+ let where = new FilterConsumerBridgeToSqlRequest(r, e);
342
+ options?.where.__applyToConsumer(where);
343
+ select += await where.resolveWhere();
344
+ }
345
+ if (groupBy)
346
+ select += groupBy;
347
+ let orderBy = '';
348
+ if (options?.orderBy) {
349
+ for (const x of options?.orderBy) {
350
+ if (orderBy == '')
351
+ orderBy = ' order by ';
352
+ else
353
+ orderBy += ', ';
354
+ if (!x.field && x.operation == 'count') {
355
+ orderBy += x.operation + '(*)';
356
+ }
357
+ else {
358
+ let field = await e.$dbNameOf(x.field);
359
+ switch (x.operation) {
360
+ case 'sum':
361
+ field = 'sum(' + field + ')';
362
+ break;
363
+ case 'average':
364
+ field = 'avg(' + field + ')';
365
+ break;
366
+ }
367
+ orderBy += field;
368
+ }
369
+ if (x.isDescending)
370
+ orderBy += ' desc';
371
+ if (this.sql._getSourceSql().orderByNullsFirst) {
372
+ if (x.isDescending)
373
+ select += ' nulls last';
374
+ else
375
+ select += ' nulls first';
376
+ }
377
+ }
378
+ if (orderBy)
379
+ select += orderBy;
380
+ }
381
+ if (options?.limit) {
382
+ let page = 1;
383
+ if (options.page)
384
+ page = options.page;
385
+ if (page < 1)
386
+ page = 1;
387
+ select +=
388
+ ' ' +
389
+ this.strategy.getLimitSqlSyntax(options.limit, (page - 1) * options.limit);
390
+ }
391
+ const result = await r.execute(select);
392
+ return result.rows.map((sql) => {
393
+ let theResult = {};
394
+ processResultRow.forEach((x, i) => x(sql[result.getColumnKeyInResultForIndexInSelect(i)], theResult));
395
+ return theResult;
396
+ });
397
+ }
289
398
  async find(options) {
290
399
  let e = await this.init();
291
400
  let { colKeys, select } = this.buildSelect(e);
@@ -112,6 +112,11 @@ export class RemultProxy {
112
112
  .remultFactory()
113
113
  .repo(...args)
114
114
  .find(...args2),
115
+ aggregate: (...args2) => self
116
+ .remultFactory()
117
+ .repo(...args)
118
+ //@ts-ignore
119
+ .aggregate(...args2),
115
120
  findFirst: (...args2) => self
116
121
  .remultFactory()
117
122
  .repo(...args)
@@ -1,6 +1,6 @@
1
1
  import { LookupColumn, makeTitle } from '../column.js';
2
2
  import { CompoundIdField } from '../CompoundIdField.js';
3
- import { Remult, isBackend, queryConfig, } from '../context.js';
3
+ import { Remult, isBackend, queryConfig } from '../context.js';
4
4
  import { Filter } from '../filter/filter-interfaces.js';
5
5
  import { Sort } from '../sort.js';
6
6
  import { assign } from '../../assign.js';
@@ -127,6 +127,53 @@ export class RepositoryImplementation {
127
127
  this._info = _info;
128
128
  this._defaultFindOptions = _defaultFindOptions;
129
129
  }
130
+ async aggregate(options) {
131
+ let findOptions = await this._buildEntityDataProviderFindOptions({
132
+ ...options,
133
+ });
134
+ var dpOptions = {
135
+ where: findOptions.where,
136
+ limit: findOptions.limit,
137
+ page: findOptions.page,
138
+ groupBy: options?.groupBy?.map((f) => this.metadata.fields.find(f)),
139
+ sum: options?.sum?.map((f) => this.metadata.fields.find(f)),
140
+ average: options?.average?.map((f) => this.metadata.fields.find(f)),
141
+ };
142
+ if (options?.orderBy) {
143
+ dpOptions.orderBy = [];
144
+ for (const key in options.orderBy) {
145
+ if (Object.prototype.hasOwnProperty.call(options?.orderBy, key)) {
146
+ const element = options.orderBy[key];
147
+ if (element)
148
+ if (typeof element === 'string') {
149
+ dpOptions.orderBy.push({
150
+ field: key === '$count'
151
+ ? undefined
152
+ : this.metadata.fields.find(key),
153
+ isDescending: element === 'desc',
154
+ operation: key === '$count' ? 'count' : undefined,
155
+ });
156
+ }
157
+ else {
158
+ for (const operation in element) {
159
+ if (Object.prototype.hasOwnProperty.call(element, operation)) {
160
+ const direction = element[operation];
161
+ dpOptions.orderBy.push({
162
+ field: this.metadata.fields.find(key),
163
+ isDescending: direction === 'desc',
164
+ operation: operation,
165
+ });
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ const result = await this._edp.aggregate(dpOptions);
173
+ if (!options?.groupBy)
174
+ return result[0];
175
+ return result;
176
+ }
130
177
  _idCache = new Map();
131
178
  _getCachedById(id, doNotLoadIfNotFound) {
132
179
  id = id + '';
@@ -1,10 +1,15 @@
1
+ export const AggregateCountMember = '$count';
1
2
  export const flags = {
2
3
  error500RetryCount: 4,
3
4
  };
4
- //p1 - add transactional option to backend method
5
5
  //p1 - add parameter all to deleteMany, and updateMany
6
6
  //p1 filter.apply ApiPreFilter
7
- //p1 - min, max, avg, sum, countDistinct - noam come up with an api
7
+ //p1 - add not as an overload to not equal
8
+ /*p1 - min, max, avg, sum, countDistinct - noam come up with an api
9
+ p1 - handle relations
10
+ p1 - mongodb
11
+ p1 - knex
12
+ */
8
13
  /*p1 - add id and use uuid by default, but allow changes with Fields.id.defaultIdProvider NO but defaultProvider yes???
9
14
  //p1 - replace uuid with crypto.randomUUID and allow custom fallback NO
10
15
  //p1 - Add example for luid
package/esm/src/sort.js CHANGED
@@ -57,22 +57,17 @@ export class Sort {
57
57
  compare(a, b, getFieldKey) {
58
58
  if (!getFieldKey)
59
59
  getFieldKey = (x) => x.key;
60
- let r = 0;
61
60
  for (let i = 0; i < this.Segments.length; i++) {
62
61
  let seg = this.Segments[i];
63
- let left = fixValueForSort(a[getFieldKey(seg.field)]);
64
- let right = fixValueForSort(b[getFieldKey(seg.field)]);
65
- if (left > right)
66
- r = 1;
67
- else if (left < right)
68
- r = -1;
62
+ let left = a[getFieldKey(seg.field)];
63
+ let right = b[getFieldKey(seg.field)];
64
+ let descending = seg.isDescending;
65
+ let r = compareForSort(left, right, descending);
69
66
  if (r != 0) {
70
- if (seg.isDescending)
71
- r *= -1;
72
67
  return r;
73
68
  }
74
69
  }
75
- return r;
70
+ return 0;
76
71
  }
77
72
  /**
78
73
  * Translates an `EntityOrderBy` to a `Sort` instance.
@@ -169,7 +164,19 @@ export class Sort {
169
164
  return orderBy;
170
165
  }
171
166
  }
172
- function fixValueForSort(a) {
167
+ export function compareForSort(left, right, descending) {
168
+ left = fixValueForSort(left);
169
+ right = fixValueForSort(right);
170
+ let r = 0;
171
+ if (left > right)
172
+ r = 1;
173
+ else if (left < right)
174
+ r = -1;
175
+ if (descending)
176
+ r *= -1;
177
+ return r;
178
+ }
179
+ export function fixValueForSort(a) {
173
180
  if (a == undefined || a == null)
174
181
  return a;
175
182
  if (a.id !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remult",
3
- "version": "0.27.7",
3
+ "version": "0.27.8-next.0",
4
4
  "description": "A CRUD framework for full-stack TypeScript",
5
5
  "homepage": "https://remult.dev",
6
6
  "repository": {
@@ -201,6 +201,9 @@ var KnexEntityDataProvider = /** @class */ (function () {
201
201
  this.knex = knex;
202
202
  this.rawSqlWrapIdentifier = rawSqlWrapIdentifier;
203
203
  }
204
+ KnexEntityDataProvider.prototype.aggregate = function (options) {
205
+ throw new Error('Method not implemented.');
206
+ };
204
207
  KnexEntityDataProvider.prototype.count = function (where) {
205
208
  return tslib_1.__awaiter(this, void 0, void 0, function () {
206
209
  var e, br, r, result, row, key, element;
package/remult-mongo.js CHANGED
@@ -107,6 +107,9 @@ var MongoEntityDataProvider = /** @class */ (function () {
107
107
  this.entity = entity;
108
108
  this.session = session;
109
109
  }
110
+ MongoEntityDataProvider.prototype.aggregate = function (options) {
111
+ throw new Error('Method not implemented.');
112
+ };
110
113
  MongoEntityDataProvider.prototype.translateFromDb = function (row, nameProvider) {
111
114
  var e_1, _a;
112
115
  var result = {};
package/src/data-api.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Remult } from './context.js';
2
2
  import type { ErrorInfo } from './data-interfaces.js';
3
- import type { EntityMetadata, FindOptions, Repository } from './remult3/remult3.js';
3
+ import { type EntityMetadata, type FindOptions, type Repository } from './remult3/remult3.js';
4
4
  import type { rowHelperImplementation } from './remult3/RepositoryImplementation.js';
5
5
  export declare class DataApi<T = unknown> {
6
6
  private repository;
@@ -12,10 +12,12 @@ export declare class DataApi<T = unknown> {
12
12
  get(response: DataApiResponse, id: any): Promise<void>;
13
13
  count(response: DataApiResponse, request: DataApiRequest, body?: any): Promise<void>;
14
14
  deleteMany(response: DataApiResponse, request: DataApiRequest, body?: any): Promise<void>;
15
+ aggregate(response: DataApiResponse, request: DataApiRequest, body: any): Promise<void>;
15
16
  getArrayImpl(response: DataApiResponse, request: DataApiRequest, body: any): Promise<{
16
17
  r: any[];
17
18
  findOptions: FindOptions<T>;
18
19
  }>;
20
+ private findOptionsFromRequest;
19
21
  private includeNone;
20
22
  getArray(response: DataApiResponse, request: DataApiRequest, body?: any): Promise<void>;
21
23
  liveQuery(response: DataApiResponse, request: DataApiRequest, body: any, serializeContext: () => Promise<any>, queryChannel: string): Promise<void>;