@entity-access/entity-access 1.0.2 → 1.0.6
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/.github/workflows/node.yml +7 -2
- package/.vscode/settings.json +1 -0
- package/README.md +57 -27
- package/package.json +2 -2
- package/src/common/EntityAccessError.ts +10 -0
- package/src/common/IDisposable.ts +25 -0
- package/src/common/ImmutableObject.ts +53 -0
- package/src/common/Logger.ts +59 -0
- package/src/common/TypeInfo.ts +3 -0
- package/src/common/cache/TimedCache.ts +2 -2
- package/src/common/usingAsync.ts +42 -12
- package/src/compiler/QueryCompiler.ts +28 -30
- package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +23 -0
- package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +23 -0
- package/src/decorators/ForeignKey.ts +1 -1
- package/src/decorators/IClassOf.ts +2 -1
- package/src/decorators/IColumn.ts +2 -0
- package/src/decorators/parser/NameParser.ts +15 -0
- package/src/di/di.ts +224 -0
- package/src/drivers/base/BaseDriver.ts +28 -3
- package/src/drivers/sql-server/ExpressionToSqlServer.ts +34 -9
- package/src/drivers/sql-server/SqlServerDriver.ts +7 -16
- package/src/entity-query/EntityType.ts +45 -3
- package/src/model/EntityContext.ts +167 -22
- package/src/model/EntityQuery.ts +118 -59
- package/src/model/EntitySource.ts +38 -46
- package/src/model/IFilterWithParameter.ts +5 -0
- package/src/model/SourceExpression.ts +21 -25
- package/src/model/{ChangeEntry.ts → changes/ChangeEntry.ts} +35 -5
- package/src/model/{ChangeSet.ts → changes/ChangeSet.ts} +16 -11
- package/src/model/events/ContextEvents.ts +26 -0
- package/src/model/events/EntityEvents.ts +96 -0
- package/src/model/{IdentityService.ts → identity/IdentityService.ts} +9 -1
- package/src/model/identity/RelationMapper.ts +71 -0
- package/src/model/symbols.ts +1 -0
- package/src/model/verification/VerificationSession.ts +173 -0
- package/src/query/ast/DebugStringVisitor.ts +175 -0
- package/src/query/ast/ExpressionToSql.ts +277 -119
- package/src/query/ast/Expressions.ts +130 -13
- package/src/query/ast/IStringTransformer.ts +19 -5
- package/src/query/ast/ParameterScope.ts +97 -0
- package/src/query/ast/ReplaceParameter.ts +40 -0
- package/src/query/ast/Types.ts +0 -0
- package/src/query/ast/Visitor.ts +26 -5
- package/src/query/expander/QueryExpander.ts +147 -0
- package/src/query/parser/ArrowToExpression.ts +134 -19
- package/src/query/parser/BabelVisitor.ts +31 -43
- package/src/query/parser/NotSupportedError.ts +5 -0
- package/src/query/parser/Restructure.ts +66 -0
- package/src/query/parser/TransformVisitor.ts +83 -0
- package/src/sql/ISql.ts +10 -0
- package/src/tests/db-tests/tests/select-items.ts +12 -0
- package/src/tests/expressions/left-joins/child-joins.ts +54 -34
- package/src/tests/expressions/sanitize/sanitize-test.ts +17 -0
- package/src/tests/expressions/select/select.ts +24 -0
- package/src/tests/expressions/simple/parse-arrow.ts +10 -0
- package/src/tests/model/ShoppingContext.ts +7 -3
- package/src/tests/model/createContext.ts +68 -17
- package/src/tests/security/ShoppingContextEvents.ts +20 -0
- package/src/tests/security/events/OrderEvents.ts +72 -0
- package/src/tests/security/events/ProductEvents.ts +92 -0
- package/src/tests/security/events/UserEvents.ts +28 -0
- package/src/tests/security/events/UserInfo.ts +7 -0
- package/src/tests/security/tests/include-items.ts +19 -0
- package/src/tests/security/tests/place-order.ts +104 -0
- package/test.js +11 -4
- package/tsconfig.json +2 -0
- package/src/decorators/parser/MemberParser.ts +0 -8
- package/src/model/EntitySchema.ts +0 -21
|
@@ -1,48 +1,193 @@
|
|
|
1
1
|
import { BaseDriver } from "../drivers/base/BaseDriver.js";
|
|
2
|
-
import ChangeSet from "./ChangeSet.js";
|
|
2
|
+
import ChangeSet from "./changes/ChangeSet.js";
|
|
3
3
|
import EntityModel from "./EntityModel.js";
|
|
4
4
|
import { Expression } from "../query/ast/Expressions.js";
|
|
5
|
-
import
|
|
5
|
+
import { IClassOf } from "../decorators/IClassOf.js";
|
|
6
|
+
import VerificationSession from "./verification/VerificationSession.js";
|
|
7
|
+
import EntityType from "../entity-query/EntityType.js";
|
|
8
|
+
import EntityEvents from "./events/EntityEvents.js";
|
|
9
|
+
import ChangeEntry from "./changes/ChangeEntry.js";
|
|
10
|
+
import ContextEvents from "./events/ContextEvents.js";
|
|
11
|
+
import Inject, { ServiceProvider } from "../di/di.js";
|
|
12
|
+
import EntityAccessError from "../common/EntityAccessError.js";
|
|
13
|
+
import Logger from "../common/Logger.js";
|
|
14
|
+
|
|
15
|
+
const isChanging = Symbol("isChanging");
|
|
6
16
|
|
|
7
17
|
export default class EntityContext {
|
|
8
18
|
|
|
9
19
|
public readonly model = new EntityModel(this);
|
|
10
20
|
public readonly changeSet = new ChangeSet(this);
|
|
11
21
|
|
|
22
|
+
public raiseEvents: boolean;
|
|
23
|
+
|
|
24
|
+
public verifyFilters = false;
|
|
25
|
+
|
|
26
|
+
public get isChanging() {
|
|
27
|
+
return this[isChanging];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private postSaveChangesQueue: { task: () => any, order: number }[];
|
|
31
|
+
|
|
12
32
|
constructor(
|
|
13
|
-
|
|
33
|
+
@Inject
|
|
34
|
+
public driver: BaseDriver,
|
|
35
|
+
@Inject
|
|
36
|
+
private events?: ContextEvents,
|
|
37
|
+
@Inject
|
|
38
|
+
private logger?: Logger
|
|
14
39
|
) {
|
|
40
|
+
this.raiseEvents = !!events;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
eventsFor<T>(type: IClassOf<T>, fail = true): EntityEvents<T>{
|
|
44
|
+
const eventsClass = this.events?.for(type, fail);
|
|
45
|
+
if (!eventsClass) {
|
|
46
|
+
if (fail) {
|
|
47
|
+
EntityAccessError.throw(`No rules defined for ${type}`);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return ServiceProvider.create(this, eventsClass);
|
|
52
|
+
}
|
|
15
53
|
|
|
54
|
+
query<T>(type: IClassOf<T>) {
|
|
55
|
+
const query = this.model.register(type).asQuery();
|
|
56
|
+
return query;
|
|
16
57
|
}
|
|
17
58
|
|
|
18
|
-
public async saveChanges() {
|
|
59
|
+
public async saveChanges(signal?: AbortSignal) {
|
|
60
|
+
|
|
61
|
+
if (this[isChanging]) {
|
|
62
|
+
if (!this.raiseEvents) {
|
|
63
|
+
this.queuePostSaveTask(() => this.saveChangesWithoutEvents(signal));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
this.queuePostSaveTask(() => this.saveChanges(signal));
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
19
69
|
|
|
20
70
|
this.changeSet.detectChanges();
|
|
21
71
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
72
|
+
if(!this.raiseEvents) {
|
|
73
|
+
return this.saveChangesWithoutEvents(signal);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
this[isChanging] = true;
|
|
78
|
+
const r = await this.saveChangesInternal(signal);
|
|
79
|
+
const postSaveChanges = this.postSaveChangesQueue;
|
|
80
|
+
this.postSaveChangesQueue = void 0;
|
|
81
|
+
this[isChanging] = false;
|
|
82
|
+
if (postSaveChanges?.length) {
|
|
83
|
+
postSaveChanges.sort((a, b) => a.order - b.order);
|
|
84
|
+
for (const { task } of postSaveChanges) {
|
|
85
|
+
const p = task();
|
|
86
|
+
if (p?.then) {
|
|
87
|
+
await p;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return r;
|
|
92
|
+
} finally {
|
|
93
|
+
this[isChanging] = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public queuePostSaveTask(task: () => any, order = Number.MAX_SAFE_INTEGER) {
|
|
98
|
+
this.postSaveChangesQueue ??= [];
|
|
99
|
+
this.postSaveChangesQueue.push({ task, order });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async saveChangesInternal(signal?: AbortSignal) {
|
|
103
|
+
|
|
104
|
+
const verificationSession = new VerificationSession(this);
|
|
105
|
+
|
|
106
|
+
const pending = [] as { status: ChangeEntry["status"], change: ChangeEntry , events: EntityEvents<any> }[];
|
|
107
|
+
|
|
108
|
+
for (const iterator of this.changeSet.entries) {
|
|
109
|
+
|
|
110
|
+
const events = this.eventsFor(iterator.type.typeClass);
|
|
111
|
+
switch(iterator.status) {
|
|
112
|
+
case "inserted":
|
|
113
|
+
await events.beforeInsert(iterator.entity, iterator);
|
|
114
|
+
if (this.verifyFilters) {
|
|
115
|
+
verificationSession.queueVerification(iterator, events);
|
|
116
|
+
}
|
|
117
|
+
pending.push({ status: iterator.status, change: iterator, events });
|
|
118
|
+
continue;
|
|
119
|
+
case "deleted":
|
|
120
|
+
await events.beforeDelete(iterator.entity, iterator);
|
|
121
|
+
if (this.verifyFilters) {
|
|
122
|
+
verificationSession.queueVerification(iterator, events);
|
|
123
|
+
}
|
|
124
|
+
pending.push({ status: iterator.status, change: iterator, events });
|
|
125
|
+
continue;
|
|
126
|
+
case "modified":
|
|
127
|
+
await events.beforeUpdate(iterator.entity, iterator);
|
|
128
|
+
if (this.verifyFilters) {
|
|
129
|
+
verificationSession.queueVerification(iterator, events);
|
|
130
|
+
}
|
|
131
|
+
pending.push({ status: iterator.status, change: iterator, events });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.verifyFilters) {
|
|
137
|
+
await verificationSession.verifyAsync();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await this.driver.runInTransaction(() => this.saveChangesWithoutEvents(signal));
|
|
141
|
+
|
|
142
|
+
if (pending.length > 0) {
|
|
143
|
+
|
|
144
|
+
for (const { status, change, change: { entity}, events } of pending) {
|
|
145
|
+
switch(status) {
|
|
25
146
|
case "inserted":
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
147
|
+
await events.afterInsert(entity, entity);
|
|
148
|
+
continue;
|
|
149
|
+
case "deleted":
|
|
150
|
+
await events.afterDelete(entity, entity);
|
|
151
|
+
continue;
|
|
30
152
|
case "modified":
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await this.executeExpression(update);
|
|
34
|
-
}
|
|
35
|
-
iterator.apply({});
|
|
36
|
-
break;
|
|
153
|
+
await events.afterUpdate(entity, entity);
|
|
154
|
+
continue;
|
|
37
155
|
}
|
|
38
156
|
}
|
|
39
|
-
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected async saveChangesWithoutEvents(signal: AbortSignal) {
|
|
162
|
+
for (const iterator of this.changeSet.entries) {
|
|
163
|
+
switch (iterator.status) {
|
|
164
|
+
case "inserted":
|
|
165
|
+
const insert = this.driver.createInsertExpression(iterator.type, iterator.entity);
|
|
166
|
+
const r = await this.executeExpression(insert, signal);
|
|
167
|
+
iterator.apply(r);
|
|
168
|
+
break;
|
|
169
|
+
case "modified":
|
|
170
|
+
if (iterator.modified.size > 0) {
|
|
171
|
+
const update = this.driver.createUpdateExpression(iterator);
|
|
172
|
+
await this.executeExpression(update, signal);
|
|
173
|
+
}
|
|
174
|
+
iterator.apply({});
|
|
175
|
+
break;
|
|
176
|
+
case "deleted":
|
|
177
|
+
const deleteQuery = this.driver.createDeleteExpression(iterator.type, iterator.entity);
|
|
178
|
+
if (deleteQuery) {
|
|
179
|
+
await this.executeExpression(deleteQuery, signal);
|
|
180
|
+
}
|
|
181
|
+
iterator.apply({});
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
40
185
|
}
|
|
41
186
|
|
|
42
|
-
private async executeExpression(expression: Expression) {
|
|
43
|
-
const { text, values } = this.driver.compiler.compileExpression(expression);
|
|
44
|
-
const r = await this.driver.executeQuery({ text, values });
|
|
45
|
-
return r.rows[0];
|
|
187
|
+
private async executeExpression(expression: Expression, signal: AbortSignal) {
|
|
188
|
+
const { text, values } = this.driver.compiler.compileExpression(null, expression);
|
|
189
|
+
const r = await this.driver.executeQuery({ text, values }, signal);
|
|
190
|
+
return r.rows?.[0];
|
|
46
191
|
}
|
|
47
192
|
|
|
48
193
|
}
|
package/src/model/EntityQuery.ts
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import Logger from "../common/Logger.js";
|
|
2
|
+
import { DisposableScope } from "../common/usingAsync.js";
|
|
3
|
+
import { ServiceProvider } from "../di/di.js";
|
|
4
|
+
import EntityType from "../entity-query/EntityType.js";
|
|
5
|
+
import { CallExpression, Expression, ExpressionAs, Identifier, OrderByExpression, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
|
|
6
|
+
import { QueryExpander } from "../query/expander/QueryExpander.js";
|
|
7
|
+
import EntityContext from "./EntityContext.js";
|
|
8
|
+
import { IOrderedEntityQuery, IEntityQuery } from "./IFilterWithParameter.js";
|
|
9
|
+
import RelationMapper from "./identity/RelationMapper.js";
|
|
5
10
|
|
|
6
11
|
export default class EntityQuery<T = any>
|
|
7
12
|
implements IOrderedEntityQuery<T>, IEntityQuery<T> {
|
|
8
|
-
|
|
13
|
+
|
|
14
|
+
public context: EntityContext;
|
|
15
|
+
public type: EntityType;
|
|
16
|
+
public selectStatement: SelectStatement;
|
|
17
|
+
public signal?: AbortSignal;
|
|
18
|
+
constructor (p: Partial<EntityQuery<any>>
|
|
19
|
+
) {
|
|
20
|
+
// lets clone select...
|
|
21
|
+
Object.setPrototypeOf(p, EntityQuery.prototype);
|
|
22
|
+
return p as EntityQuery;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
select(p: any, fx: any): any {
|
|
26
|
+
return this.map(p, fx);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
map(p: any, fx: any): any {
|
|
30
|
+
// const source = this.source.copy();
|
|
31
|
+
// const { select } = source;
|
|
32
|
+
// const exp = this.source.context.driver.compiler.compileToExpression(source, p, fx);
|
|
9
33
|
}
|
|
10
34
|
|
|
11
35
|
withSignal(signal: AbortSignal): any {
|
|
12
|
-
|
|
13
|
-
source.signal = signal;
|
|
14
|
-
return new EntityQuery(source);
|
|
36
|
+
return new EntityQuery({ ... this, signal });
|
|
15
37
|
}
|
|
16
38
|
|
|
17
39
|
thenBy(parameters: any, fx: any): any {
|
|
@@ -20,21 +42,21 @@ export default class EntityQuery<T = any>
|
|
|
20
42
|
thenByDescending(parameters: any, fx: any) {
|
|
21
43
|
return this.orderByDescending(parameters, fx);
|
|
22
44
|
}
|
|
45
|
+
|
|
23
46
|
where<P>(parameters: P, fx: (p: P) => (x: T) => boolean): any {
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return new EntityQuery(source);
|
|
48
|
+
return this.extend(parameters, fx, (select, body) => ({
|
|
49
|
+
... select,
|
|
50
|
+
where: select.where ? Expression.logicalAnd(select.where, body): body
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
include(p: any): any {
|
|
55
|
+
const selectStatement = QueryExpander.expand(this.context, { ... this.selectStatement }, p);
|
|
56
|
+
return new EntityQuery({
|
|
57
|
+
... this,
|
|
58
|
+
selectStatement
|
|
59
|
+
});
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
async toArray(): Promise<T[]> {
|
|
@@ -46,21 +68,63 @@ export default class EntityQuery<T = any>
|
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
async *enumerate(): AsyncGenerator<T, any, unknown> {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
71
|
+
const logger = ServiceProvider.resolve(this.context, Logger);
|
|
72
|
+
const scope = new DisposableScope();
|
|
73
|
+
const session = logger.newSession();
|
|
74
|
+
let query: { text: string, values: any[]};
|
|
53
75
|
try {
|
|
76
|
+
scope.register(session);
|
|
77
|
+
const type = this.type;
|
|
78
|
+
const signal = this.signal;
|
|
79
|
+
|
|
80
|
+
const relationMapper = new RelationMapper(this.context.changeSet);
|
|
81
|
+
|
|
82
|
+
const include = this.selectStatement.include;
|
|
83
|
+
if (include?.length > 0) {
|
|
84
|
+
// since we will be streaming results...
|
|
85
|
+
// it is important that we load all the
|
|
86
|
+
// included entities first...
|
|
87
|
+
const loaders = include.map((x) => this.load(relationMapper, session, x, signal));
|
|
88
|
+
await Promise.all(loaders);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
|
|
93
|
+
query = this.context.driver.compiler.compileExpression(this, this.selectStatement);
|
|
94
|
+
const reader = await this.context.driver.executeReader(query, signal);
|
|
95
|
+
scope.register(reader);
|
|
54
96
|
for await (const iterator of reader.next(10, signal)) {
|
|
55
97
|
if (type) {
|
|
56
|
-
Object.setPrototypeOf(iterator, type.prototype);
|
|
98
|
+
Object.setPrototypeOf(iterator, type.typeClass.prototype);
|
|
57
99
|
// set identity...
|
|
58
|
-
const entry = this.
|
|
100
|
+
const entry = this.context.changeSet.getEntry(iterator, iterator);
|
|
101
|
+
relationMapper.fix(entry);
|
|
59
102
|
yield entry.entity;
|
|
60
103
|
continue;
|
|
61
104
|
}
|
|
62
105
|
yield iterator as T;
|
|
63
106
|
}
|
|
107
|
+
|
|
108
|
+
} catch(error) {
|
|
109
|
+
session.error(`Failed executing ${query?.text}\n${error.stack ?? error}`);
|
|
110
|
+
throw error;
|
|
111
|
+
} finally {
|
|
112
|
+
await scope.dispose();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async load(relationMapper: RelationMapper, session: Logger, select: SelectStatement, signal: AbortSignal) {
|
|
117
|
+
const query = this.context.driver.compiler.compileExpression(this, select);
|
|
118
|
+
const reader = await this.context.driver.executeReader(query, signal);
|
|
119
|
+
try {
|
|
120
|
+
for await (const iterator of reader.next(10, signal)) {
|
|
121
|
+
Object.setPrototypeOf(iterator, select.model.typeClass.prototype);
|
|
122
|
+
const entry = this.context.changeSet.getEntry(iterator, iterator);
|
|
123
|
+
relationMapper.fix(entry);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
session.error(`Failed loading ${query.text}\n${error.stack ?? error}`);
|
|
127
|
+
throw error;
|
|
64
128
|
} finally {
|
|
65
129
|
await reader.dispose();
|
|
66
130
|
}
|
|
@@ -70,7 +134,7 @@ export default class EntityQuery<T = any>
|
|
|
70
134
|
for await(const iterator of this.limit(1).enumerate()) {
|
|
71
135
|
return iterator;
|
|
72
136
|
}
|
|
73
|
-
throw new Error(`No records found for ${this.
|
|
137
|
+
throw new Error(`No records found for ${this.type?.name || "Table"}`);
|
|
74
138
|
}
|
|
75
139
|
|
|
76
140
|
async first(): Promise<T> {
|
|
@@ -80,42 +144,32 @@ export default class EntityQuery<T = any>
|
|
|
80
144
|
return null;
|
|
81
145
|
}
|
|
82
146
|
toQuery(): { text: string; values: any[]; } {
|
|
83
|
-
return this.
|
|
147
|
+
return this.context.driver.compiler.compileExpression(this, this.selectStatement);
|
|
84
148
|
}
|
|
85
149
|
orderBy(parameters: any, fx: any): any {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
target: exp
|
|
150
|
+
return this.extend(parameters, fx, (select, target) => ({
|
|
151
|
+
... select,
|
|
152
|
+
orderBy: select.orderBy
|
|
153
|
+
? [ ... select.orderBy, OrderByExpression.create({ target})]
|
|
154
|
+
: [OrderByExpression.create({ target})]
|
|
92
155
|
}));
|
|
93
|
-
return new EntityQuery(source);
|
|
94
156
|
}
|
|
95
157
|
orderByDescending(parameters: any, fx: any): any {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
descending: true
|
|
158
|
+
const descending = true;
|
|
159
|
+
return this.extend(parameters, fx, (select, target) => ({
|
|
160
|
+
... select,
|
|
161
|
+
orderBy: select.orderBy
|
|
162
|
+
? [ ... select.orderBy, OrderByExpression.create({ target, descending })]
|
|
163
|
+
: [OrderByExpression.create({ target, descending })]
|
|
103
164
|
}));
|
|
104
|
-
return new EntityQuery(source);
|
|
105
165
|
}
|
|
106
166
|
|
|
107
167
|
limit(n: number): any {
|
|
108
|
-
|
|
109
|
-
const { select } = source;
|
|
110
|
-
select.limit = n;
|
|
111
|
-
return new EntityQuery(source);
|
|
168
|
+
return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, limit: n} });
|
|
112
169
|
}
|
|
113
170
|
|
|
114
171
|
offset(n: number): any {
|
|
115
|
-
|
|
116
|
-
const { select } = source;
|
|
117
|
-
select.offset = n;
|
|
118
|
-
return new EntityQuery(source);
|
|
172
|
+
return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, offset: n} });
|
|
119
173
|
}
|
|
120
174
|
|
|
121
175
|
async count(parameters?:any, fx?: any): Promise<number> {
|
|
@@ -123,10 +177,7 @@ export default class EntityQuery<T = any>
|
|
|
123
177
|
return this.where(parameters, fx).count();
|
|
124
178
|
}
|
|
125
179
|
|
|
126
|
-
const
|
|
127
|
-
const { select } = source;
|
|
128
|
-
|
|
129
|
-
select.fields = [
|
|
180
|
+
const select = { ... this.selectStatement, fields: [
|
|
130
181
|
ExpressionAs.create({
|
|
131
182
|
expression: CallExpression.create({
|
|
132
183
|
callee: Identifier.create({ value: "COUNT"}),
|
|
@@ -134,10 +185,12 @@ export default class EntityQuery<T = any>
|
|
|
134
185
|
}),
|
|
135
186
|
alias: QuotedLiteral.create({ literal: "count" })
|
|
136
187
|
})
|
|
137
|
-
];
|
|
188
|
+
] };
|
|
138
189
|
|
|
139
|
-
const
|
|
140
|
-
|
|
190
|
+
const nq = new EntityQuery({ ... this, selectStatement: select });
|
|
191
|
+
|
|
192
|
+
const query = this.context.driver.compiler.compileExpression(nq, select);
|
|
193
|
+
const reader = await this.context.driver.executeReader(query);
|
|
141
194
|
|
|
142
195
|
try {
|
|
143
196
|
for await (const iterator of reader.next()) {
|
|
@@ -149,4 +202,10 @@ export default class EntityQuery<T = any>
|
|
|
149
202
|
|
|
150
203
|
}
|
|
151
204
|
|
|
205
|
+
private extend(parameters: any, fx: any, map: (select: SelectStatement, exp: Expression) => SelectStatement) {
|
|
206
|
+
const exp = this.context.driver.compiler.compile(this, fx);
|
|
207
|
+
exp.params[0].value = parameters;
|
|
208
|
+
return new EntityQuery({ ... this, selectStatement: map(this.selectStatement, exp.body)});
|
|
209
|
+
}
|
|
210
|
+
|
|
152
211
|
}
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import type EntityContext from "./EntityContext.js";
|
|
2
2
|
import type EntityType from "../entity-query/EntityType.js";
|
|
3
3
|
import type { IEntityQuery, IFilterExpression } from "./IFilterWithParameter.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Expression } from "../query/ast/Expressions.js";
|
|
5
5
|
import EntityQuery from "./EntityQuery.js";
|
|
6
|
-
import TimedCache from "../common/cache/TimedCache.js";
|
|
7
6
|
import { contextSymbol, modelSymbol } from "../common/symbols/symbols.js";
|
|
8
|
-
import { SourceExpression } from "./SourceExpression.js";
|
|
9
|
-
|
|
10
|
-
const modelCache = new TimedCache<any, SelectStatement>();
|
|
11
7
|
|
|
12
8
|
export class EntitySource<T = any> {
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
public readonly filters: {
|
|
16
|
-
read?: () => IFilterExpression;
|
|
17
|
-
modify?: () => IFilterExpression;
|
|
18
|
-
delete?: () => IFilterExpression;
|
|
19
|
-
include?: () => IFilterExpression;
|
|
20
|
-
} = {};
|
|
21
|
-
|
|
22
|
-
|
|
23
10
|
get [modelSymbol]() {
|
|
24
11
|
return this.model;
|
|
25
12
|
}
|
|
@@ -37,7 +24,7 @@ export class EntitySource<T = any> {
|
|
|
37
24
|
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
public add(item: Partial<T>) {
|
|
27
|
+
public add(item: Partial<T>): T {
|
|
41
28
|
const p = Object.getPrototypeOf(item).constructor;
|
|
42
29
|
if (!p || p === Object) {
|
|
43
30
|
Object.setPrototypeOf(item, this.model.typeClass.prototype);
|
|
@@ -47,45 +34,50 @@ export class EntitySource<T = any> {
|
|
|
47
34
|
throw new Error("Entity is already attached to the context");
|
|
48
35
|
}
|
|
49
36
|
entry.status = "inserted";
|
|
50
|
-
return
|
|
37
|
+
return entry.entity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Entity can only be deleted if all primary keys are present
|
|
42
|
+
* @param item entity to delete
|
|
43
|
+
*/
|
|
44
|
+
public delete(item: Partial<T>) {
|
|
45
|
+
const p = Object.getPrototypeOf(item).constructor;
|
|
46
|
+
if (!p || p === Object) {
|
|
47
|
+
Object.setPrototypeOf(item, this.model.typeClass.prototype);
|
|
48
|
+
}
|
|
49
|
+
const entry = this.context.changeSet.getEntry(item);
|
|
50
|
+
if (entry.status !== "detached" && entry.status !== "unchanged") {
|
|
51
|
+
throw new Error("Entity is already attached to the context");
|
|
52
|
+
}
|
|
53
|
+
entry.status = "deleted";
|
|
54
|
+
return entry.entity;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
public all(): IEntityQuery<T> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})) as IEntityQuery<T>;
|
|
58
|
+
return this.asQuery();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public filtered(mode: "read" | "modify" = "read"): IEntityQuery<T> {
|
|
62
|
+
const query = this.asQuery();
|
|
63
|
+
const events = this.context.eventsFor(this.model.typeClass, true);
|
|
64
|
+
return mode === "modify" ? events.modify(query) : events.filter(query);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
|
|
68
|
+
return this.asQuery().where(parameter, fx);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public asQuery() {
|
|
65
72
|
const { model, context } = this;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
const selectStatement = this.model.selectAllFields();
|
|
74
|
+
selectStatement.model = model;
|
|
75
|
+
return new EntityQuery<T>({
|
|
69
76
|
context,
|
|
70
|
-
model,
|
|
71
|
-
|
|
72
|
-
})
|
|
73
|
-
}
|
|
77
|
+
type: model,
|
|
78
|
+
selectStatement
|
|
79
|
+
}) as any as IEntityQuery<T>;
|
|
74
80
|
|
|
75
|
-
generateModel(): SelectStatement {
|
|
76
|
-
const source = this.model.fullyQualifiedName;
|
|
77
|
-
const as = QuotedLiteral.create({ literal: this.model.name[0] + "1" });
|
|
78
|
-
const fields = this.model.columns.map((c) => c.name !== c.columnName
|
|
79
|
-
? ExpressionAs.create({
|
|
80
|
-
expression: QuotedLiteral.propertyChain(as.literal, c.columnName),
|
|
81
|
-
alias: QuotedLiteral.create({ literal: c.name })
|
|
82
|
-
})
|
|
83
|
-
: QuotedLiteral.propertyChain(as.literal, c.columnName));
|
|
84
|
-
return SelectStatement.create({
|
|
85
|
-
source,
|
|
86
|
-
as,
|
|
87
|
-
fields,
|
|
88
|
-
});
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
public toQuery() {
|
|
@@ -93,6 +85,6 @@ export class EntitySource<T = any> {
|
|
|
93
85
|
if(!filter) {
|
|
94
86
|
return "";
|
|
95
87
|
}
|
|
96
|
-
return this.context.driver.compiler.compileExpression(filter);
|
|
88
|
+
return this.context.driver.compiler.compileExpression( this.asQuery() as any, filter);
|
|
97
89
|
}
|
|
98
90
|
}
|
|
@@ -11,6 +11,9 @@ export interface IBaseQuery<T> {
|
|
|
11
11
|
firstOrFail(): Promise<T>;
|
|
12
12
|
first(): Promise<T>;
|
|
13
13
|
|
|
14
|
+
select<P, TR>(parameters: P, fx: (p: P) => (x: T) => TR): IBaseQuery<TR>;
|
|
15
|
+
map<P, TR>(parameters: P, fx: (p: P) => (x: T) => TR): IBaseQuery<TR>;
|
|
16
|
+
|
|
14
17
|
toArray(): Promise<T[]>;
|
|
15
18
|
|
|
16
19
|
toQuery(): { text: string, values: any[]};
|
|
@@ -23,6 +26,8 @@ export interface IBaseQuery<T> {
|
|
|
23
26
|
count<P>(parameters: P, fx: (p: P) => (x: T) => boolean): Promise<number>;
|
|
24
27
|
|
|
25
28
|
withSignal<DT>(this:DT, signal: AbortSignal): DT;
|
|
29
|
+
|
|
30
|
+
include<TR>(fx: (x: T) => TR | TR[]): IBaseQuery<T>;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export interface IOrderedEntityQuery<T> extends IBaseQuery<T> {
|