@entity-access/entity-access 1.0.2 → 1.0.5
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/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/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 +48 -9
- package/src/drivers/sql-server/SqlServerDriver.ts +7 -16
- package/src/entity-query/EntityType.ts +1 -1
- package/src/model/EntityContext.ts +167 -22
- package/src/model/EntityQuery.ts +76 -60
- package/src/model/EntitySource.ts +39 -33
- package/src/model/IFilterWithParameter.ts +3 -0
- package/src/model/SourceExpression.ts +21 -25
- package/src/model/{ChangeEntry.ts → changes/ChangeEntry.ts} +33 -3
- package/src/model/{ChangeSet.ts → changes/ChangeSet.ts} +12 -11
- package/src/model/events/ContextEvents.ts +26 -0
- package/src/model/events/EntityEvents.ts +92 -0
- package/src/model/{IdentityService.ts → identity/IdentityService.ts} +1 -1
- package/src/model/symbols.ts +1 -0
- package/src/model/verification/VerificationSession.ts +173 -0
- package/src/query/ast/DebugStringVisitor.ts +128 -0
- package/src/query/ast/ExpressionToSql.ts +289 -119
- package/src/query/ast/Expressions.ts +111 -12
- package/src/query/ast/IStringTransformer.ts +16 -4
- package/src/query/ast/ReplaceParameter.ts +40 -0
- package/src/query/ast/Visitor.ts +20 -5
- package/src/query/parser/ArrowToExpression.ts +116 -16
- package/src/query/parser/BabelVisitor.ts +27 -44
- 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 +19 -26
- 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 +45 -13
- package/src/tests/security/ShoppingContextEvents.ts +20 -0
- package/src/tests/security/events/OrderEvents.ts +66 -0
- package/src/tests/security/events/ProductEvents.ts +92 -0
- package/src/tests/security/events/UserEvents.ts +18 -0
- package/src/tests/security/events/UserInfo.ts +7 -0
- package/src/tests/security/tests/place-order.ts +71 -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
package/src/di/di.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { IDisposable, disposeDisposable } from "../common/IDisposable.js";
|
|
2
|
+
import { IAbstractClassOf, IClassOf } from "../decorators/IClassOf.js";
|
|
3
|
+
|
|
4
|
+
import "reflect-metadata";
|
|
5
|
+
import EntityContext from "../model/EntityContext.js";
|
|
6
|
+
import { BaseDriver } from "../drivers/base/BaseDriver.js";
|
|
7
|
+
|
|
8
|
+
export type ServiceKind = "Singleton" | "Transient" | "Scoped";
|
|
9
|
+
|
|
10
|
+
const registrations = new Map<any,IServiceDescriptor>();
|
|
11
|
+
|
|
12
|
+
export const injectServiceTypesSymbol = Symbol("injectServiceTypes");
|
|
13
|
+
export const injectServiceKeysSymbol = Symbol("injectServiceKeys");
|
|
14
|
+
|
|
15
|
+
const registrationsSymbol = Symbol("registrations");
|
|
16
|
+
|
|
17
|
+
const serviceProvider = Symbol("serviceProvider");
|
|
18
|
+
|
|
19
|
+
const parentServiceProvider = Symbol("parentServiceProvider");
|
|
20
|
+
|
|
21
|
+
export class ServiceProvider implements IDisposable {
|
|
22
|
+
|
|
23
|
+
public static global = new ServiceProvider();
|
|
24
|
+
|
|
25
|
+
public static resolve<T>(serviceOwner: any, type: IClassOf<T>): T {
|
|
26
|
+
const sp = (serviceOwner[serviceProvider] ?? this.global) as ServiceProvider;
|
|
27
|
+
return sp.resolve(type);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static create<T>(serviceOwner, type: IClassOf<T>): T {
|
|
31
|
+
const sp = (serviceOwner[serviceProvider] ?? this.global) as ServiceProvider;
|
|
32
|
+
return sp.createFromType(type);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
private map: Map<any,any> = new Map();
|
|
37
|
+
private disposables: IDisposable[];
|
|
38
|
+
|
|
39
|
+
constructor(parent?: ServiceProvider) {
|
|
40
|
+
this[serviceProvider] = this;
|
|
41
|
+
this[parentServiceProvider] = parent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
add<T1, T extends T1>(type: IAbstractClassOf<T1> | IClassOf<T1>, instance: T) {
|
|
45
|
+
this.getRegistration(type, true);
|
|
46
|
+
this.map.set(type, instance);
|
|
47
|
+
instance[serviceProvider] = this;
|
|
48
|
+
return instance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
createScope() {
|
|
53
|
+
return new ServiceProvider(this);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
create<T>(type: IClassOf<T>): T {
|
|
57
|
+
return this.createFromType(type);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
resolve(type) {
|
|
62
|
+
let instance;
|
|
63
|
+
const sd = this.getRegistration(type);
|
|
64
|
+
switch(sd.kind) {
|
|
65
|
+
case "Scoped":
|
|
66
|
+
if (!this[parentServiceProvider]) {
|
|
67
|
+
throw new Error(`Unable to create scoped service ${type?.name ?? type} in global scope.`);
|
|
68
|
+
}
|
|
69
|
+
instance = this.map.get(type);
|
|
70
|
+
if (!instance) {
|
|
71
|
+
instance = this.createFromDescriptor(sd);
|
|
72
|
+
this.map.set(type, instance);
|
|
73
|
+
instance[serviceProvider] = this;
|
|
74
|
+
if (instance[Symbol.disposable] || instance[Symbol.asyncDisposable]) {
|
|
75
|
+
(this.disposables ??= []).push(instance);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return instance;
|
|
79
|
+
case "Singleton":
|
|
80
|
+
let sp = this;
|
|
81
|
+
while (sp[parentServiceProvider]) {
|
|
82
|
+
sp = sp[parentServiceProvider];
|
|
83
|
+
}
|
|
84
|
+
instance = sp.map.get(type);
|
|
85
|
+
if (!instance) {
|
|
86
|
+
instance = sp.createFromDescriptor(sd);
|
|
87
|
+
instance[serviceProvider] = sp;
|
|
88
|
+
sp.map.set(type, instance);
|
|
89
|
+
if (instance[Symbol.disposable] || instance[Symbol.asyncDisposable]) {
|
|
90
|
+
(sp.disposables ??= []).push(instance);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return instance;
|
|
94
|
+
case "Transient":
|
|
95
|
+
instance = sp.createFromDescriptor(sd);
|
|
96
|
+
instance[serviceProvider] = sp;
|
|
97
|
+
return instance;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dispose() {
|
|
102
|
+
this[Symbol.disposable]();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[Symbol.disposable]() {
|
|
106
|
+
const disposables = this.disposables;
|
|
107
|
+
if (!disposables) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const iterator of disposables) {
|
|
111
|
+
disposeDisposable(iterator);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private getRegistration(type: any, add = false) {
|
|
116
|
+
let sd = registrations.get(type);
|
|
117
|
+
if (!sd) {
|
|
118
|
+
|
|
119
|
+
if (add) {
|
|
120
|
+
const registration: IServiceDescriptor = { key: type, kind: "Scoped"};
|
|
121
|
+
registrations.set(type, registration);
|
|
122
|
+
return registration;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// we need to go through all services
|
|
126
|
+
// to find the derived type
|
|
127
|
+
for (const [key, value] of registrations.entries()) {
|
|
128
|
+
if (key instanceof type) {
|
|
129
|
+
// we found the match..
|
|
130
|
+
registrations.set(type, { ...value, key: type });
|
|
131
|
+
sd = value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!sd) {
|
|
135
|
+
throw new Error(`No service registered for ${type?.name ?? type}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return sd;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private createFromDescriptor(sd: IServiceDescriptor): any {
|
|
142
|
+
if(sd.factory) {
|
|
143
|
+
return sd.factory(this);
|
|
144
|
+
}
|
|
145
|
+
return this.createFromType(sd.key);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private createFromType(type): any {
|
|
149
|
+
const injectTypes = type[injectServiceTypesSymbol] as any[];
|
|
150
|
+
const injectServices = injectTypes
|
|
151
|
+
? injectTypes.map((x) => this.resolve(x))
|
|
152
|
+
: [];
|
|
153
|
+
const instance = new type(... injectServices);
|
|
154
|
+
instance[serviceProvider] = this;
|
|
155
|
+
// initialize properties...
|
|
156
|
+
const keys = type.prototype[injectServiceKeysSymbol];
|
|
157
|
+
if (keys) {
|
|
158
|
+
for (const key in keys) {
|
|
159
|
+
if (Object.prototype.hasOwnProperty.call(keys, key)) {
|
|
160
|
+
const element = keys[key];
|
|
161
|
+
instance[key] = this.resolve(element);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return instance;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface IServiceDescriptor {
|
|
171
|
+
|
|
172
|
+
key: any;
|
|
173
|
+
kind: ServiceKind;
|
|
174
|
+
instance?: any;
|
|
175
|
+
factory?: (sp: ServiceProvider) => any;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
export const ServiceCollection = {
|
|
180
|
+
register(kind: ServiceKind, key, factory?: (sp: ServiceProvider) => any) {
|
|
181
|
+
registrations.set(key, { kind, key, factory});
|
|
182
|
+
},
|
|
183
|
+
[registrationsSymbol]: registrations
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export default function Inject(target, key, index?: number) {
|
|
187
|
+
|
|
188
|
+
if (index !== void 0) {
|
|
189
|
+
const plist = (Reflect as any).getMetadata("design:paramtypes", target, key);
|
|
190
|
+
const serviceTypes = target[injectServiceTypesSymbol] ??= [];
|
|
191
|
+
serviceTypes[index] = plist[index];
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const pType = (Reflect as any).getMetadata("design:type", target, key);
|
|
196
|
+
(target[injectServiceKeysSymbol] ??= {})[key] = pType;
|
|
197
|
+
Object.defineProperty(target, key, {
|
|
198
|
+
get() {
|
|
199
|
+
const result = ServiceProvider.resolve(this, pType);
|
|
200
|
+
// get is compatible with AtomWatcher
|
|
201
|
+
// as it will ignore getter and it will
|
|
202
|
+
// not try to set a binding refresher
|
|
203
|
+
Object.defineProperty(target, key, {
|
|
204
|
+
get: () => result
|
|
205
|
+
});
|
|
206
|
+
return result;
|
|
207
|
+
},
|
|
208
|
+
configurable: true
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function Register(kind: ServiceKind, factory?: (sp: ServiceProvider) => any) {
|
|
215
|
+
return function(target) {
|
|
216
|
+
ServiceCollection.register(kind, target, factory);
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const RegisterSingleton = Register("Singleton");
|
|
221
|
+
|
|
222
|
+
export const RegisterScoped = Register("Scoped");
|
|
223
|
+
|
|
224
|
+
export const RegisterTransient = Register("Transient");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import QueryCompiler from "../../compiler/QueryCompiler.js";
|
|
2
2
|
import EntityType from "../../entity-query/EntityType.js";
|
|
3
3
|
import Migrations from "../../migrations/Migrations.js";
|
|
4
|
-
import ChangeEntry from "../../model/ChangeEntry.js";
|
|
5
|
-
import { BinaryExpression, Constant, Expression, InsertStatement, QuotedLiteral, ReturnUpdated, TableLiteral, UpdateStatement, ValuesStatement } from "../../query/ast/Expressions.js";
|
|
4
|
+
import ChangeEntry from "../../model/changes/ChangeEntry.js";
|
|
5
|
+
import { BinaryExpression, Constant, DeleteStatement, Expression, InsertStatement, QuotedLiteral, ReturnUpdated, TableLiteral, UpdateStatement, ValuesStatement } from "../../query/ast/Expressions.js";
|
|
6
6
|
|
|
7
7
|
export const disposableSymbol: unique symbol = (Symbol as any).dispose ??= Symbol("disposable");
|
|
8
8
|
|
|
@@ -47,7 +47,6 @@ export interface IQueryResult {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export abstract class BaseDriver {
|
|
50
|
-
|
|
51
50
|
abstract get compiler(): QueryCompiler;
|
|
52
51
|
|
|
53
52
|
constructor(public readonly connectionString: IDbConnectionString) {}
|
|
@@ -131,4 +130,30 @@ export abstract class BaseDriver {
|
|
|
131
130
|
});
|
|
132
131
|
}
|
|
133
132
|
|
|
133
|
+
createDeleteExpression(type: EntityType, entity: any) {
|
|
134
|
+
let where: Expression;
|
|
135
|
+
for (const iterator of type.keys) {
|
|
136
|
+
const key = entity[iterator.name];
|
|
137
|
+
if (!key) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const compare = BinaryExpression.create({
|
|
141
|
+
left: QuotedLiteral.create({ literal: iterator.columnName }),
|
|
142
|
+
operator: "=",
|
|
143
|
+
right: Constant.create({ value: key })
|
|
144
|
+
});
|
|
145
|
+
where = where
|
|
146
|
+
? BinaryExpression.create({ left: where, operator: "AND", right: compare })
|
|
147
|
+
: compare;
|
|
148
|
+
}
|
|
149
|
+
if (!where) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return DeleteStatement.create({
|
|
153
|
+
table: type.fullyQualifiedName,
|
|
154
|
+
where
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
134
159
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import ExpressionToSql from "../../query/ast/ExpressionToSql.js";
|
|
2
2
|
import { Identifier, InsertStatement, OrderByExpression, ReturnUpdated, SelectStatement, ValuesStatement } from "../../query/ast/Expressions.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ITextQuery, prepare } from "../../query/ast/IStringTransformer.js";
|
|
4
4
|
|
|
5
5
|
export default class ExpressionToSqlServer extends ExpressionToSql {
|
|
6
6
|
|
|
7
|
-
visitReturnUpdated(e: ReturnUpdated):
|
|
7
|
+
visitReturnUpdated(e: ReturnUpdated): ITextQuery {
|
|
8
8
|
if (!e) {
|
|
9
9
|
return [];
|
|
10
10
|
}
|
|
@@ -22,7 +22,7 @@ export default class ExpressionToSqlServer extends ExpressionToSql {
|
|
|
22
22
|
return prepare ` OUTPUT ${fields}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
visitInsertStatement(e: InsertStatement):
|
|
25
|
+
visitInsertStatement(e: InsertStatement): ITextQuery {
|
|
26
26
|
const returnValues = this.visit(e.returnValues);
|
|
27
27
|
if (e.values instanceof ValuesStatement) {
|
|
28
28
|
|
|
@@ -45,7 +45,28 @@ export default class ExpressionToSqlServer extends ExpressionToSql {
|
|
|
45
45
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
visitSelectStatement(e: SelectStatement):
|
|
48
|
+
visitSelectStatement(e: SelectStatement): ITextQuery {
|
|
49
|
+
|
|
50
|
+
if (e.as && e.model) {
|
|
51
|
+
const scope = this.targets.get(e.as);
|
|
52
|
+
if (!scope) {
|
|
53
|
+
this.targets.set(e.as, {
|
|
54
|
+
parameter: e.as,
|
|
55
|
+
model:
|
|
56
|
+
e.model,
|
|
57
|
+
replace: e.as,
|
|
58
|
+
selectStatement: e
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
scope.selectStatement = e;
|
|
62
|
+
scope.model = e.model;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const orderBy = e.orderBy?.length > 0 ? prepare `\n\t\tORDER BY ${this.visitArray(e.orderBy)}` : "";
|
|
67
|
+
const where = e.where ? prepare `\n\tWHERE ${this.visit(e.where)}` : "";
|
|
68
|
+
const joins = e.joins?.length > 0 ? prepare `\n\t\t${this.visitArray(e.joins)}` : [];
|
|
69
|
+
|
|
49
70
|
const fields = this.visitArray(e.fields, ",\n\t\t");
|
|
50
71
|
|
|
51
72
|
const showTop = e.limit && !e.offset;
|
|
@@ -64,15 +85,33 @@ export default class ExpressionToSqlServer extends ExpressionToSql {
|
|
|
64
85
|
const topValue = Number(e.limit);
|
|
65
86
|
const top = showTop ? prepare ` TOP (${() => topValue}) ` : "";
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
let source: ITextQuery;
|
|
89
|
+
let as: ITextQuery | "";
|
|
90
|
+
if (e.source.type === "ValuesStatement") {
|
|
91
|
+
const v = e.source as ValuesStatement;
|
|
92
|
+
const rows = v.values.map((x) => prepare `(${this.visitArray(x)})`);
|
|
93
|
+
source = prepare `(VALUES ${rows}) as ${this.visit(e.as)}(${this.visitArray(v.fields)})`;
|
|
94
|
+
as = [];
|
|
95
|
+
} else {
|
|
96
|
+
source = this.visit(e.source);
|
|
97
|
+
as = e.as ? prepare ` AS ${this.visit(e.as)}` : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// const as = e.as ? prepare ` AS ${this.visit(e.as)}` : "";
|
|
72
101
|
const offset = showFetch ? prepare ` OFFSET ${Number(e.offset).toString()} ROWS ` : "";
|
|
73
102
|
const next = showFetch ? prepare ` FETCH NEXT ${Number(e.limit).toString()} ROWS ONLY` : "";
|
|
74
103
|
return prepare `SELECT ${top}
|
|
75
104
|
${fields}
|
|
76
105
|
FROM ${source}${as}${joins}${where}${orderBy}${offset}${next}`;
|
|
77
106
|
}
|
|
107
|
+
|
|
108
|
+
visitValuesStatement(e: ValuesStatement): ITextQuery {
|
|
109
|
+
const rows = [];
|
|
110
|
+
for (const rowValues of e.values) {
|
|
111
|
+
rows.push(prepare `(${ this.visitArray(rowValues) })`);
|
|
112
|
+
}
|
|
113
|
+
const fields = e.fields ? prepare ` as x11(${this.visitArray(e.fields)})` : "";
|
|
114
|
+
return prepare `(VALUES ${rows}) ${fields}`;
|
|
115
|
+
|
|
116
|
+
}
|
|
78
117
|
}
|
|
@@ -4,13 +4,11 @@ import Migrations from "../../migrations/Migrations.js";
|
|
|
4
4
|
import { BaseDriver, IDbConnectionString, IDbReader, IQuery, IRecord, disposableSymbol, toQuery } from "../base/BaseDriver.js";
|
|
5
5
|
import sql from "mssql";
|
|
6
6
|
import SqlServerQueryCompiler from "./SqlServerQueryCompiler.js";
|
|
7
|
-
import SqlServerSqlMethodTransformer from "../../compiler/sql-server/SqlServerSqlMethodTransformer.js";
|
|
8
7
|
import SqlServerAutomaticMigrations from "../../migrations/sql-server/SqlServerAutomaticMigrations.js";
|
|
9
8
|
import { SqlServerLiteral } from "./SqlServerLiteral.js";
|
|
10
|
-
import usingAsync from "../../common/usingAsync.js";
|
|
11
9
|
import TimedCache from "../../common/cache/TimedCache.js";
|
|
12
10
|
|
|
13
|
-
export type ISqlServerConnectionString = sql.config;
|
|
11
|
+
export type ISqlServerConnectionString = IDbConnectionString & sql.config;
|
|
14
12
|
|
|
15
13
|
const namedPool = new TimedCache<string, sql.ConnectionPool>();
|
|
16
14
|
|
|
@@ -24,14 +22,8 @@ export default class SqlServerDriver extends BaseDriver {
|
|
|
24
22
|
private transaction: sql.Transaction;
|
|
25
23
|
|
|
26
24
|
constructor(private readonly config: ISqlServerConnectionString) {
|
|
27
|
-
super(
|
|
28
|
-
|
|
29
|
-
host: config.server ??= (config as any).host,
|
|
30
|
-
port: config.port,
|
|
31
|
-
password: config.password,
|
|
32
|
-
user: config.user,
|
|
33
|
-
... config,
|
|
34
|
-
});
|
|
25
|
+
super(config);
|
|
26
|
+
config.server = config.host;
|
|
35
27
|
}
|
|
36
28
|
|
|
37
29
|
public async executeReader(command: IQuery, signal?: AbortSignal): Promise<IDbReader> {
|
|
@@ -61,7 +53,6 @@ export default class SqlServerDriver extends BaseDriver {
|
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
try {
|
|
64
|
-
console.log(command.text);
|
|
65
56
|
const r = await rq.query(command.text);
|
|
66
57
|
return { rows: r.recordset ?? [r.output], updated: r.rowsAffected [0]};
|
|
67
58
|
} catch (error) {
|
|
@@ -135,10 +126,11 @@ export default class SqlServerDriver extends BaseDriver {
|
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
private newConnection() {
|
|
138
|
-
const
|
|
139
|
-
|
|
129
|
+
const config = this.config;
|
|
130
|
+
const key = config.server + "//" + config.database + "/" + config.user;
|
|
131
|
+
return namedPool.getOrCreateAsync(config.server + "://" + config.database,
|
|
140
132
|
() => {
|
|
141
|
-
const pool = new sql.ConnectionPool(
|
|
133
|
+
const pool = new sql.ConnectionPool(config);
|
|
142
134
|
const oldClose = pool.close;
|
|
143
135
|
pool.close = ((c) => {
|
|
144
136
|
namedPool.delete(key);
|
|
@@ -188,7 +180,6 @@ class SqlReader implements IDbReader {
|
|
|
188
180
|
this.processPendingRows();
|
|
189
181
|
});
|
|
190
182
|
|
|
191
|
-
console.log(`Executing ${(command as any).text}`);
|
|
192
183
|
void rq.query((command as any).text);
|
|
193
184
|
|
|
194
185
|
do {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IColumn, IEntityRelation } from "../decorators/IColumn.js";
|
|
2
2
|
import { IClassOf } from "../decorators/IClassOf.js";
|
|
3
3
|
import { Query } from "../query/Query.js";
|
|
4
|
-
import NameParser from "../decorators/parser/
|
|
4
|
+
import NameParser from "../decorators/parser/NameParser.js";
|
|
5
5
|
import SchemaRegistry from "../decorators/SchemaRegistry.js";
|
|
6
6
|
import { QuotedLiteral, TableLiteral } from "../query/ast/Expressions.js";
|
|
7
7
|
import InstanceCache from "../common/cache/InstanceCache.js";
|
|
@@ -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
|
}
|