remult 0.27.7 → 0.27.8-next.1
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/remult-knex/index.js +8 -3
- package/esm/remult-mongo.js +93 -0
- package/esm/src/data-api.js +55 -5
- package/esm/src/data-providers/array-entity-data-provider.js +122 -0
- package/esm/src/data-providers/json-data-provider.js +3 -0
- package/esm/src/data-providers/rest-data-provider.js +23 -1
- package/esm/src/data-providers/sql-database.js +108 -0
- package/esm/src/remult-proxy.js +5 -0
- package/esm/src/remult3/RepositoryImplementation.js +57 -1
- package/esm/src/remult3/remult3.js +8 -2
- package/esm/src/sort.js +18 -11
- package/package.json +1 -1
- package/remult-knex/index.js +19 -2
- package/remult-mongo.js +179 -17
- package/src/data-api.d.ts +3 -1
- package/src/data-api.js +123 -38
- package/src/data-interfaces.d.ts +12 -0
- package/src/data-providers/array-entity-data-provider.d.ts +3 -2
- package/src/data-providers/array-entity-data-provider.js +252 -25
- package/src/data-providers/json-data-provider.js +6 -0
- package/src/data-providers/rest-data-provider.d.ts +2 -1
- package/src/data-providers/rest-data-provider.js +57 -13
- package/src/data-providers/sql-database.d.ts +3 -2
- package/src/data-providers/sql-database.js +207 -0
- package/src/remult-proxy.js +11 -0
- package/src/remult3/RepositoryImplementation.d.ts +6 -1
- package/src/remult3/RepositoryImplementation.js +68 -0
- package/src/remult3/remult3.d.ts +130 -0
- package/src/remult3/remult3.js +9 -3
- package/src/sort.d.ts +2 -0
- package/src/sort.js +19 -10
package/esm/remult-knex/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Sort } from '../src/sort.js';
|
|
|
7
7
|
import { ValueConverters } from '../src/valueConverters.js';
|
|
8
8
|
import { getRepositoryInternals } from '../src/remult3/repository-internals.js';
|
|
9
9
|
import { remultStatic } from '../src/remult-static.js';
|
|
10
|
-
import { getRowAfterUpdate } from '../src/data-providers/sql-database.js';
|
|
10
|
+
import { aggregateImpl, getRowAfterUpdate, } from '../src/data-providers/sql-database.js';
|
|
11
11
|
export class KnexDataProvider {
|
|
12
12
|
knex;
|
|
13
13
|
constructor(knex) {
|
|
@@ -70,7 +70,7 @@ export class KnexDataProvider {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
return new KnexEntityDataProvider(entity, this.knex, this.wrapIdentifier);
|
|
73
|
+
return new KnexEntityDataProvider(entity, this.knex, this.wrapIdentifier, this);
|
|
74
74
|
}
|
|
75
75
|
async transaction(action) {
|
|
76
76
|
let t = await this.knex.transaction();
|
|
@@ -110,10 +110,15 @@ class KnexEntityDataProvider {
|
|
|
110
110
|
entity;
|
|
111
111
|
knex;
|
|
112
112
|
rawSqlWrapIdentifier;
|
|
113
|
-
|
|
113
|
+
dp;
|
|
114
|
+
constructor(entity, knex, rawSqlWrapIdentifier, dp) {
|
|
114
115
|
this.entity = entity;
|
|
115
116
|
this.knex = knex;
|
|
116
117
|
this.rawSqlWrapIdentifier = rawSqlWrapIdentifier;
|
|
118
|
+
this.dp = dp;
|
|
119
|
+
}
|
|
120
|
+
async aggregate(options) {
|
|
121
|
+
return aggregateImpl(options, await this.init(), this.dp.createCommand(), false, (limit, offset) => ' limit ' + limit + ' offset ' + offset);
|
|
117
122
|
}
|
|
118
123
|
async count(where) {
|
|
119
124
|
const e = await this.init();
|
package/esm/remult-mongo.js
CHANGED
|
@@ -5,6 +5,7 @@ import { remult as remultContext } from './src/remult-proxy.js';
|
|
|
5
5
|
import { getRepository } from './src/remult3/RepositoryImplementation.js';
|
|
6
6
|
import { getRepositoryInternals } from './src/remult3/repository-internals.js';
|
|
7
7
|
import { getRowAfterUpdate } from './src/data-providers/sql-database.js';
|
|
8
|
+
import { AggregateCountMember } from './src/remult3/remult3.js';
|
|
8
9
|
export class MongoDataProvider {
|
|
9
10
|
db;
|
|
10
11
|
client;
|
|
@@ -97,6 +98,98 @@ class MongoEntityDataProvider {
|
|
|
97
98
|
let w = await x.resolveWhere();
|
|
98
99
|
return await collection.countDocuments(w, { session: this.session });
|
|
99
100
|
}
|
|
101
|
+
async aggregate(options) {
|
|
102
|
+
let { collection, e } = await this.collection();
|
|
103
|
+
let x = new FilterConsumerBridgeToMongo(e);
|
|
104
|
+
const pipeLine = [];
|
|
105
|
+
if (options?.where) {
|
|
106
|
+
options.where.__applyToConsumer(x);
|
|
107
|
+
let where = await x.resolveWhere();
|
|
108
|
+
pipeLine.push({ $match: where });
|
|
109
|
+
}
|
|
110
|
+
const processResultRow = [];
|
|
111
|
+
const $group = {
|
|
112
|
+
__count: { $sum: 1 },
|
|
113
|
+
};
|
|
114
|
+
processResultRow.push((mongoRow, resultRow) => {
|
|
115
|
+
resultRow[AggregateCountMember] = mongoRow.__count;
|
|
116
|
+
});
|
|
117
|
+
pipeLine.push({ $group });
|
|
118
|
+
if (options?.groupBy) {
|
|
119
|
+
$group._id = {};
|
|
120
|
+
for (const element of options.groupBy) {
|
|
121
|
+
const name = e.$dbNameOf(element);
|
|
122
|
+
$group._id[name] = `$${name}`;
|
|
123
|
+
processResultRow.push((mongoRow, resultRow) => {
|
|
124
|
+
resultRow[element.key] = element.valueConverter.fromDb(mongoRow._id[name]);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
$group._id = null;
|
|
130
|
+
}
|
|
131
|
+
if (options?.sum) {
|
|
132
|
+
for (const element of options.sum) {
|
|
133
|
+
const name = e.$dbNameOf(element) + '_sum';
|
|
134
|
+
$group[name] = { $sum: `$${e.$dbNameOf(element)}` };
|
|
135
|
+
processResultRow.push((mongoRow, resultRow) => {
|
|
136
|
+
resultRow[element.key] = {
|
|
137
|
+
...resultRow[element.key],
|
|
138
|
+
sum: element.valueConverter.fromDb(mongoRow[name]),
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (options?.avg) {
|
|
144
|
+
for (const element of options.avg) {
|
|
145
|
+
const name = e.$dbNameOf(element) + '_avg';
|
|
146
|
+
$group[name] = { $avg: `$${e.$dbNameOf(element)}` };
|
|
147
|
+
processResultRow.push((mongoRow, resultRow) => {
|
|
148
|
+
resultRow[element.key] = {
|
|
149
|
+
...resultRow[element.key],
|
|
150
|
+
avg: element.valueConverter.fromDb(mongoRow[name]),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (options?.orderBy) {
|
|
156
|
+
let sort = {};
|
|
157
|
+
for (const x of options.orderBy) {
|
|
158
|
+
const direction = x.isDescending ? -1 : 1;
|
|
159
|
+
switch (x.operation) {
|
|
160
|
+
case 'sum':
|
|
161
|
+
sort[e.$dbNameOf(x.field) + '_sum'] = direction;
|
|
162
|
+
break;
|
|
163
|
+
case 'avg':
|
|
164
|
+
sort[e.$dbNameOf(x.field) + '_avg'] = direction;
|
|
165
|
+
break;
|
|
166
|
+
case 'count':
|
|
167
|
+
sort['__count'] = direction;
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
sort['_id.' + e.$dbNameOf(x.field)] = direction;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
pipeLine.push({ $sort: sort });
|
|
174
|
+
}
|
|
175
|
+
if (options?.limit) {
|
|
176
|
+
pipeLine.push({ $limit: options.limit });
|
|
177
|
+
if (options.page) {
|
|
178
|
+
pipeLine.push({ $skip: (options.page - 1) * options.limit });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const r = await collection
|
|
182
|
+
.aggregate(pipeLine, { session: this.session })
|
|
183
|
+
.toArray();
|
|
184
|
+
const r2 = r.map((x) => {
|
|
185
|
+
let resultRow = {};
|
|
186
|
+
for (const f of processResultRow) {
|
|
187
|
+
f(x, resultRow);
|
|
188
|
+
}
|
|
189
|
+
return resultRow;
|
|
190
|
+
});
|
|
191
|
+
return r2;
|
|
192
|
+
}
|
|
100
193
|
async find(options) {
|
|
101
194
|
let { collection, e } = await this.collection();
|
|
102
195
|
let x = new FilterConsumerBridgeToMongo(e);
|
package/esm/src/data-api.js
CHANGED
|
@@ -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,56 @@ 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 'avg':
|
|
120
|
+
orderBy[element.field] = {
|
|
121
|
+
...orderBy[element.field],
|
|
122
|
+
avg: direction,
|
|
123
|
+
};
|
|
124
|
+
break;
|
|
125
|
+
case 'count':
|
|
126
|
+
orderBy[AggregateCountMember] = direction;
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
orderBy[element.field] = direction;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let result = await this.repository.aggregate({
|
|
135
|
+
where: findOptions.where,
|
|
136
|
+
limit: findOptions.limit,
|
|
137
|
+
page: findOptions.page,
|
|
138
|
+
groupBy: body?.groupBy?.filter((x) => this.repository.fields.find(x).includedInApi()),
|
|
139
|
+
sum: body?.sum?.filter((x) => this.repository.fields.find(x).includedInApi()),
|
|
140
|
+
avg: body?.avg?.filter((x) => this.repository.fields.find(x).includedInApi()),
|
|
141
|
+
orderBy: orderBy,
|
|
142
|
+
});
|
|
143
|
+
if (!Array.isArray(result)) {
|
|
144
|
+
result = [result];
|
|
145
|
+
}
|
|
146
|
+
response.success(result);
|
|
147
|
+
}
|
|
101
148
|
async getArrayImpl(response, request, body) {
|
|
149
|
+
let findOptions = await this.findOptionsFromRequest(request, body);
|
|
150
|
+
const r = await this.repository.find(findOptions).then(async (r) => {
|
|
151
|
+
return await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson()));
|
|
152
|
+
});
|
|
153
|
+
return { r, findOptions };
|
|
154
|
+
}
|
|
155
|
+
async findOptionsFromRequest(request, body) {
|
|
102
156
|
let findOptions = {
|
|
103
157
|
load: () => [],
|
|
104
158
|
include: this.includeNone(),
|
|
@@ -147,14 +201,10 @@ export class DataApi {
|
|
|
147
201
|
});
|
|
148
202
|
}
|
|
149
203
|
if (!hasId) {
|
|
150
|
-
response.forbidden();
|
|
151
204
|
throw new ForbiddenError();
|
|
152
205
|
}
|
|
153
206
|
}
|
|
154
|
-
|
|
155
|
-
return await Promise.all(r.map(async (y) => this.repository.getEntityRef(y).toApiJson()));
|
|
156
|
-
});
|
|
157
|
-
return { r, findOptions };
|
|
207
|
+
return findOptions;
|
|
158
208
|
}
|
|
159
209
|
includeNone() {
|
|
160
210
|
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?.avg) {
|
|
54
|
+
class Avg {
|
|
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
|
+
avg: this.sum / this.count,
|
|
72
|
+
};
|
|
73
|
+
this.sum = 0;
|
|
74
|
+
this.count = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const element of options.avg) {
|
|
78
|
+
aggregates.push(new Avg(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 'avg':
|
|
126
|
+
return row[x.field.key].avg;
|
|
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
|
+
avg: options?.avg?.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,9 @@ class ActualSQLEntityDataProvider {
|
|
|
286
287
|
return Number(r.rows[0].count);
|
|
287
288
|
});
|
|
288
289
|
}
|
|
290
|
+
async aggregate(options) {
|
|
291
|
+
return await aggregateImpl(options, await this.init(), this.sql.createCommand(), this.sql._getSourceSql().orderByNullsFirst, this.sql._getSourceSql().getLimitSqlSyntax);
|
|
292
|
+
}
|
|
289
293
|
async find(options) {
|
|
290
294
|
let e = await this.init();
|
|
291
295
|
let { colKeys, select } = this.buildSelect(e);
|
|
@@ -537,3 +541,107 @@ export function getRowAfterUpdate(meta, dataProvider, data, id, operation) {
|
|
|
537
541
|
return r[0];
|
|
538
542
|
});
|
|
539
543
|
}
|
|
544
|
+
export async function aggregateImpl(options, e, r, orderByNullFirst, limitSyntax) {
|
|
545
|
+
let select = 'select count(*) as count';
|
|
546
|
+
let groupBy = '';
|
|
547
|
+
const processResultRow = [];
|
|
548
|
+
processResultRow.push((sqlVal, theResult) => {
|
|
549
|
+
theResult[AggregateCountMember] = Number(sqlVal);
|
|
550
|
+
});
|
|
551
|
+
if (options?.groupBy)
|
|
552
|
+
for (const x of options?.groupBy) {
|
|
553
|
+
if (x.isServerExpression) {
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
select += ', ' + e.$dbNameOf(x);
|
|
557
|
+
if (x.options.sqlExpression)
|
|
558
|
+
select += ' as ' + x.key;
|
|
559
|
+
if (groupBy == '')
|
|
560
|
+
groupBy = ' group by ';
|
|
561
|
+
else
|
|
562
|
+
groupBy += ', ';
|
|
563
|
+
groupBy += e.$dbNameOf(x);
|
|
564
|
+
}
|
|
565
|
+
processResultRow.push((sqlResult, theResult) => {
|
|
566
|
+
theResult[x.key] = x.valueConverter.fromDb(sqlResult);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (options?.sum)
|
|
570
|
+
for (const x of options?.sum) {
|
|
571
|
+
if (x.isServerExpression) {
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
select += ', sum(' + e.$dbNameOf(x) + ') as ' + x.key + '_sum';
|
|
575
|
+
}
|
|
576
|
+
processResultRow.push((sqlResult, theResult) => {
|
|
577
|
+
theResult[x.key] = { ...theResult[x.key], sum: sqlResult };
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (options?.avg)
|
|
581
|
+
for (const x of options?.avg) {
|
|
582
|
+
if (x.isServerExpression) {
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
select += ', avg(' + e.$dbNameOf(x) + ') as ' + x.key + '_avg';
|
|
586
|
+
}
|
|
587
|
+
processResultRow.push((sqlResult, theResult) => {
|
|
588
|
+
theResult[x.key] = { ...theResult[x.key], avg: sqlResult };
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
select += '\n from ' + e.$entityName;
|
|
592
|
+
if (options?.where) {
|
|
593
|
+
let where = new FilterConsumerBridgeToSqlRequest(r, e);
|
|
594
|
+
options?.where.__applyToConsumer(where);
|
|
595
|
+
select += await where.resolveWhere();
|
|
596
|
+
}
|
|
597
|
+
if (groupBy)
|
|
598
|
+
select += groupBy;
|
|
599
|
+
let orderBy = '';
|
|
600
|
+
if (options?.orderBy) {
|
|
601
|
+
for (const x of options?.orderBy) {
|
|
602
|
+
if (orderBy == '')
|
|
603
|
+
orderBy = ' order by ';
|
|
604
|
+
else
|
|
605
|
+
orderBy += ', ';
|
|
606
|
+
if (!x.field && x.operation == 'count') {
|
|
607
|
+
orderBy += x.operation + '(*)';
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
let field = await e.$dbNameOf(x.field);
|
|
611
|
+
switch (x.operation) {
|
|
612
|
+
case 'sum':
|
|
613
|
+
field = 'sum(' + field + ')';
|
|
614
|
+
break;
|
|
615
|
+
case 'avg':
|
|
616
|
+
field = 'avg(' + field + ')';
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
orderBy += field;
|
|
620
|
+
}
|
|
621
|
+
if (x.isDescending)
|
|
622
|
+
orderBy += ' desc';
|
|
623
|
+
if (orderByNullFirst) {
|
|
624
|
+
if (x.isDescending)
|
|
625
|
+
select += ' nulls last';
|
|
626
|
+
else
|
|
627
|
+
select += ' nulls first';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (orderBy)
|
|
631
|
+
select += orderBy;
|
|
632
|
+
}
|
|
633
|
+
if (options?.limit) {
|
|
634
|
+
let page = 1;
|
|
635
|
+
if (options.page)
|
|
636
|
+
page = options.page;
|
|
637
|
+
if (page < 1)
|
|
638
|
+
page = 1;
|
|
639
|
+
select += ' ' + limitSyntax(options.limit, (page - 1) * options.limit);
|
|
640
|
+
}
|
|
641
|
+
const result = await r.execute(select);
|
|
642
|
+
return result.rows.map((sql) => {
|
|
643
|
+
let theResult = {};
|
|
644
|
+
processResultRow.forEach((x, i) => x(sql[result.getColumnKeyInResultForIndexInSelect(i)], theResult));
|
|
645
|
+
return theResult;
|
|
646
|
+
});
|
|
647
|
+
}
|
package/esm/src/remult-proxy.js
CHANGED
|
@@ -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
|
|
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,62 @@ 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
|
+
const getField = (key) => {
|
|
135
|
+
const r = this.metadata.fields.find(key);
|
|
136
|
+
if (r === undefined)
|
|
137
|
+
throw Error(`key "${key}" not found in entity`);
|
|
138
|
+
return r;
|
|
139
|
+
};
|
|
140
|
+
const getFieldNotInGroupBy = (key) => {
|
|
141
|
+
if (options?.groupBy?.includes(key))
|
|
142
|
+
throw Error(`field "${key}" cannot be used both in an aggregate and in group by`);
|
|
143
|
+
return getField(key);
|
|
144
|
+
};
|
|
145
|
+
var dpOptions = {
|
|
146
|
+
where: findOptions.where,
|
|
147
|
+
limit: findOptions.limit,
|
|
148
|
+
page: findOptions.page,
|
|
149
|
+
groupBy: options?.groupBy?.map(getField),
|
|
150
|
+
sum: options?.sum?.map(getFieldNotInGroupBy),
|
|
151
|
+
avg: options?.avg?.map(getFieldNotInGroupBy),
|
|
152
|
+
};
|
|
153
|
+
if (options?.orderBy) {
|
|
154
|
+
dpOptions.orderBy = [];
|
|
155
|
+
for (const key in options.orderBy) {
|
|
156
|
+
if (Object.prototype.hasOwnProperty.call(options?.orderBy, key)) {
|
|
157
|
+
const element = options.orderBy[key];
|
|
158
|
+
if (element)
|
|
159
|
+
if (typeof element === 'string') {
|
|
160
|
+
dpOptions.orderBy.push({
|
|
161
|
+
field: key === '$count' ? undefined : getField(key),
|
|
162
|
+
isDescending: element === 'desc',
|
|
163
|
+
operation: key === '$count' ? 'count' : undefined,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const operation in element) {
|
|
168
|
+
if (Object.prototype.hasOwnProperty.call(element, operation)) {
|
|
169
|
+
const direction = element[operation];
|
|
170
|
+
dpOptions.orderBy.push({
|
|
171
|
+
field: this.metadata.fields.find(key),
|
|
172
|
+
isDescending: direction === 'desc',
|
|
173
|
+
operation: operation,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const result = await this._edp.aggregate(dpOptions);
|
|
182
|
+
if (!options?.groupBy)
|
|
183
|
+
return result[0];
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
130
186
|
_idCache = new Map();
|
|
131
187
|
_getCachedById(id, doNotLoadIfNotFound) {
|
|
132
188
|
id = id + '';
|
|
@@ -1,10 +1,16 @@
|
|
|
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 -
|
|
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 - order by $count
|
|
10
|
+
p1 - handle relations
|
|
11
|
+
p1 - mongodb
|
|
12
|
+
p1 - knex
|
|
13
|
+
*/
|
|
8
14
|
/*p1 - add id and use uuid by default, but allow changes with Fields.id.defaultIdProvider NO but defaultProvider yes???
|
|
9
15
|
//p1 - replace uuid with crypto.randomUUID and allow custom fallback NO
|
|
10
16
|
//p1 - Add example for luid
|