@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,7 +1,7 @@
|
|
|
1
1
|
import { modelSymbol } from "../common/symbols/symbols.js";
|
|
2
2
|
import EntityType from "../entity-query/EntityType.js";
|
|
3
|
-
import { BinaryExpression, Expression, ExpressionAs, Identifier, JoinExpression, MemberExpression, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
|
|
4
|
-
import {
|
|
3
|
+
import { BinaryExpression, Expression, ExpressionAs, Identifier, JoinExpression, MemberExpression, ParameterExpression, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
|
|
4
|
+
import { ITextQueryFragment, QueryParameter } from "../query/ast/IStringTransformer.js";
|
|
5
5
|
import EntityContext from "./EntityContext.js";
|
|
6
6
|
|
|
7
7
|
const sourceSymbol = Symbol("source");
|
|
@@ -12,9 +12,8 @@ export class SourceExpression {
|
|
|
12
12
|
return new SourceExpression(p);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
parameter?: string;
|
|
15
|
+
alias: ParameterExpression;
|
|
16
|
+
parameter?: ParameterExpression;
|
|
18
17
|
context: EntityContext;
|
|
19
18
|
model?: EntityType;
|
|
20
19
|
include?: EntityType[];
|
|
@@ -59,31 +58,28 @@ export class SourceExpression {
|
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
const column = relation.relation.fkColumn;
|
|
62
|
-
const parameter = this.parameter + "." + property;
|
|
61
|
+
const parameter = ParameterExpression.create({ name: this.parameter + "." + property});
|
|
63
62
|
const source = this.addSource(model, parameter);
|
|
64
63
|
const join = JoinExpression.create({
|
|
65
|
-
as:
|
|
64
|
+
as: source.alias,
|
|
66
65
|
joinType: column.nullable ? "LEFT" : "INNER",
|
|
67
66
|
model,
|
|
68
67
|
source: QuotedLiteral.create({ literal: model.name }),
|
|
69
|
-
where:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
property: Identifier.create({ value: model.keys[0].columnName })
|
|
78
|
-
})
|
|
79
|
-
})
|
|
68
|
+
where: Expression.logicalAnd(
|
|
69
|
+
Expression.member(
|
|
70
|
+
this.alias,
|
|
71
|
+
column.columnName),
|
|
72
|
+
Expression.member(
|
|
73
|
+
source.alias,
|
|
74
|
+
model.keys[0].columnName)
|
|
75
|
+
)
|
|
80
76
|
});
|
|
81
77
|
select.joins.push(join);
|
|
82
78
|
join[sourceSymbol] = source;
|
|
83
79
|
return source;
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
addSource(model: EntityType, parameter:
|
|
82
|
+
addSource(model: EntityType, parameter: ParameterExpression) {
|
|
87
83
|
|
|
88
84
|
const { context } = this;
|
|
89
85
|
const source = SourceExpression.create({
|
|
@@ -105,15 +101,15 @@ export class SourceExpression {
|
|
|
105
101
|
break;
|
|
106
102
|
}
|
|
107
103
|
}while (true);
|
|
108
|
-
source.alias = alias;
|
|
104
|
+
source.alias = ParameterExpression.create({ name: alias });
|
|
109
105
|
this.map.set(alias, source);
|
|
110
|
-
this.paramMap.set(parameter, source);
|
|
106
|
+
this.paramMap.set(parameter.name, source);
|
|
111
107
|
return source;
|
|
112
108
|
}
|
|
113
109
|
|
|
114
|
-
flatten(chain: string[]):
|
|
110
|
+
flatten(chain: string[]): ITextQueryFragment {
|
|
115
111
|
const [start, ... others ] = chain;
|
|
116
|
-
if (start === this.parameter) {
|
|
112
|
+
if (start === this.parameter.name) {
|
|
117
113
|
return this.prepareNames(others);
|
|
118
114
|
}
|
|
119
115
|
const mapped = this.paramMap.get(start);
|
|
@@ -123,11 +119,11 @@ export class SourceExpression {
|
|
|
123
119
|
throw new Error("Not found");
|
|
124
120
|
}
|
|
125
121
|
|
|
126
|
-
prepareNames([property , ... others]: string[]):
|
|
122
|
+
prepareNames([property , ... others]: string[]): ITextQueryFragment {
|
|
127
123
|
const p = this.model.getProperty(property);
|
|
128
124
|
const quotedLiteral = this.context.driver.compiler.quotedLiteral;
|
|
129
125
|
if (others.length === 0) {
|
|
130
|
-
return `${
|
|
126
|
+
return `${ QueryParameter.create(() => this.alias.name, quotedLiteral)}.${quotedLiteral(p.field.columnName)}`;
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
// this must be a navigation...
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IColumn } from "
|
|
2
|
-
import EntityType from "
|
|
1
|
+
import { IColumn } from "../../decorators/IColumn.js";
|
|
2
|
+
import EntityType from "../../entity-query/EntityType.js";
|
|
3
3
|
import type ChangeSet from "./ChangeSet.js";
|
|
4
4
|
import { privateUpdateEntry } from "./ChangeSet.js";
|
|
5
5
|
|
|
@@ -31,17 +31,24 @@ export default class ChangeEntry implements IChanges {
|
|
|
31
31
|
|
|
32
32
|
private pending: (() => void)[];
|
|
33
33
|
|
|
34
|
+
private dependents: ChangeEntry[];
|
|
35
|
+
|
|
34
36
|
constructor(p: IChanges, changeSet: ChangeSet) {
|
|
35
37
|
Object.setPrototypeOf(p, ChangeEntry.prototype);
|
|
36
38
|
const ce = p as ChangeEntry;
|
|
37
39
|
ce.changeSet = changeSet;
|
|
38
40
|
ce.pending = [];
|
|
41
|
+
ce.dependents = [];
|
|
39
42
|
ce.modified = new Map();
|
|
40
43
|
return ce;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
public detect() {
|
|
44
47
|
|
|
48
|
+
if (this.status === "deleted") {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
const { type: { columns }, entity, original } = this;
|
|
46
53
|
|
|
47
54
|
if (original === void 0) {
|
|
@@ -76,6 +83,15 @@ export default class ChangeEntry implements IChanges {
|
|
|
76
83
|
// apply values to main entity
|
|
77
84
|
// set status to unchanged
|
|
78
85
|
|
|
86
|
+
if(this.status === "deleted") {
|
|
87
|
+
// remove...
|
|
88
|
+
for (const iterator of this.pending) {
|
|
89
|
+
iterator();
|
|
90
|
+
}
|
|
91
|
+
this.changeSet[privateUpdateEntry](this);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
// we will only apply the columns defined
|
|
80
96
|
if (dbValues !== void 0) {
|
|
81
97
|
for (const iterator of this.type.columns) {
|
|
@@ -95,6 +111,7 @@ export default class ChangeEntry implements IChanges {
|
|
|
95
111
|
|
|
96
112
|
this.pending.length = 0;
|
|
97
113
|
this.original = { ... this.entity };
|
|
114
|
+
|
|
98
115
|
this.status = "unchanged";
|
|
99
116
|
this.modified.clear();
|
|
100
117
|
}
|
|
@@ -103,7 +120,7 @@ export default class ChangeEntry implements IChanges {
|
|
|
103
120
|
const { type: { relations }, entity } = this;
|
|
104
121
|
// for parent relations.. check if related key is set or not...
|
|
105
122
|
for (const iterator of relations) {
|
|
106
|
-
if (iterator.
|
|
123
|
+
if (iterator.isInverseRelation) {
|
|
107
124
|
continue;
|
|
108
125
|
}
|
|
109
126
|
|
|
@@ -120,7 +137,15 @@ export default class ChangeEntry implements IChanges {
|
|
|
120
137
|
|
|
121
138
|
const keyValue = related[rKey.name];
|
|
122
139
|
if (keyValue === void 0) {
|
|
140
|
+
|
|
141
|
+
relatedChanges.dependents.push(this);
|
|
142
|
+
|
|
123
143
|
this.order++;
|
|
144
|
+
|
|
145
|
+
for (const d of this.dependents) {
|
|
146
|
+
d.order++;
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
const fk = iterator;
|
|
125
150
|
relatedChanges.pending.push(() => {
|
|
126
151
|
this.entity[fk.fkColumn.name] = related[rKey.name];
|
|
@@ -134,10 +159,12 @@ export default class ChangeEntry implements IChanges {
|
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
setupInverseProperties() {
|
|
162
|
+
const deleted = this.status === "deleted";
|
|
137
163
|
for (const iterator of this.type.relations) {
|
|
138
|
-
if (!iterator.
|
|
164
|
+
if (!iterator.isInverseRelation) {
|
|
139
165
|
continue;
|
|
140
166
|
}
|
|
167
|
+
const { relatedName } = iterator;
|
|
141
168
|
const related = this.entity[iterator.name];
|
|
142
169
|
if (related === void 0) {
|
|
143
170
|
continue;
|
|
@@ -148,7 +175,10 @@ export default class ChangeEntry implements IChanges {
|
|
|
148
175
|
}
|
|
149
176
|
continue;
|
|
150
177
|
}
|
|
151
|
-
related[
|
|
178
|
+
related[relatedName] = this.entity;
|
|
179
|
+
if (deleted) {
|
|
180
|
+
this.pending.push(() => delete related[relatedName]);
|
|
181
|
+
}
|
|
152
182
|
}
|
|
153
183
|
}
|
|
154
184
|
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import SchemaRegistry from "../../decorators/SchemaRegistry.js";
|
|
2
|
+
import EntityContext from "../EntityContext.js";
|
|
3
|
+
import IdentityService, { identityMapSymbol } from "../identity/IdentityService.js";
|
|
3
4
|
import ChangeEntry from "./ChangeEntry.js";
|
|
4
|
-
import { IRecord } from "../drivers/base/BaseDriver.js";
|
|
5
|
-
import IdentityService from "./IdentityService.js";
|
|
6
|
-
|
|
7
|
-
const entrySymbol = Symbol("entry");
|
|
8
|
-
|
|
9
|
-
const identitySymbol = Symbol("identity");
|
|
10
|
-
|
|
11
|
-
const getEntityByIdentity = Symbol("getEntityByIdentity");
|
|
12
5
|
|
|
13
6
|
export const privateUpdateEntry = Symbol("updateEntry");
|
|
14
7
|
|
|
@@ -16,6 +9,10 @@ export default class ChangeSet {
|
|
|
16
9
|
|
|
17
10
|
public readonly entries: ChangeEntry[] = [];
|
|
18
11
|
|
|
12
|
+
get [identityMapSymbol]() {
|
|
13
|
+
return this.identityMap;
|
|
14
|
+
}
|
|
15
|
+
|
|
19
16
|
private entryMap: Map<any, ChangeEntry> = new Map();
|
|
20
17
|
|
|
21
18
|
/**
|
|
@@ -32,6 +29,15 @@ export default class ChangeSet {
|
|
|
32
29
|
[privateUpdateEntry](entry: ChangeEntry) {
|
|
33
30
|
const jsonKey = IdentityService.getIdentity(entry.entity);
|
|
34
31
|
if (jsonKey) {
|
|
32
|
+
if (entry.status === "deleted") {
|
|
33
|
+
this.identityMap.delete(jsonKey);
|
|
34
|
+
const index = this.entries.indexOf(entry);
|
|
35
|
+
if (index !== -1) {
|
|
36
|
+
this.entries.splice(index, 1);
|
|
37
|
+
}
|
|
38
|
+
this.entryMap.delete(entry.entity);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
this.identityMap.set(jsonKey, entry.entity);
|
|
36
42
|
}
|
|
37
43
|
}
|
|
@@ -62,7 +68,6 @@ export default class ChangeSet {
|
|
|
62
68
|
original: original ? { ... original } : void 0,
|
|
63
69
|
status: "unchanged"
|
|
64
70
|
}, this);
|
|
65
|
-
entity[entrySymbol] = entry;
|
|
66
71
|
this.entries.push(entry);
|
|
67
72
|
this.entryMap.set(entity, entry);
|
|
68
73
|
return entry;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { IClassOf } from "../../decorators/IClassOf.js";
|
|
2
|
+
import { ServiceProvider } from "../../di/di.js";
|
|
3
|
+
import { NotSupportedError } from "../../query/parser/NotSupportedError.js";
|
|
4
|
+
import type EntityContext from "../EntityContext.js";
|
|
5
|
+
import type EntityEvents from "./EntityEvents.js";
|
|
6
|
+
|
|
7
|
+
export default class ContextEvents {
|
|
8
|
+
|
|
9
|
+
private map: Map<any, IClassOf<EntityEvents<any>>> = new Map();
|
|
10
|
+
|
|
11
|
+
public for<T>(type: IClassOf<T>, fail = true): IClassOf<EntityEvents<T>> {
|
|
12
|
+
const typeClass = this.map.get(type);
|
|
13
|
+
if (!typeClass) {
|
|
14
|
+
if (fail) {
|
|
15
|
+
throw new NotSupportedError();
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return typeClass;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public register<T>(type: IClassOf<T>, events: IClassOf<EntityEvents<T>>) {
|
|
23
|
+
this.map.set(type, events);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { IClassOf } from "../../decorators/IClassOf.js";
|
|
2
|
+
import NameParser from "../../decorators/parser/NameParser.js";
|
|
3
|
+
import Inject from "../../di/di.js";
|
|
4
|
+
import type EntityType from "../../entity-query/EntityType.js";
|
|
5
|
+
import type EntityContext from "../EntityContext.js";
|
|
6
|
+
import { IEntityQuery } from "../IFilterWithParameter.js";
|
|
7
|
+
import ChangeEntry from "../changes/ChangeEntry.js";
|
|
8
|
+
|
|
9
|
+
const done = Promise.resolve() as Promise<void>;
|
|
10
|
+
|
|
11
|
+
export class ForeignKeyFilter<T = any, TE = any> {
|
|
12
|
+
|
|
13
|
+
public type: EntityType;
|
|
14
|
+
public name: string;
|
|
15
|
+
public fkName: string;
|
|
16
|
+
|
|
17
|
+
private events: EntityEvents<TE>;
|
|
18
|
+
private context: EntityContext;
|
|
19
|
+
|
|
20
|
+
constructor(p: Partial<ForeignKeyFilter> & { context: EntityContext, events: EntityEvents<any> }) {
|
|
21
|
+
Object.setPrototypeOf(p, ForeignKeyFilter.prototype);
|
|
22
|
+
return p as any as ForeignKeyFilter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public is<TR>(fx: (x: T) => TR): this is ForeignKeyFilter<T, TR> & boolean {
|
|
26
|
+
const name = NameParser.parseMember(fx);
|
|
27
|
+
return name === this.fkName || name === this.name;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public read(): IEntityQuery<TE> {
|
|
31
|
+
const read = this.context.query(this.type.typeClass);
|
|
32
|
+
return this.events.filter(read);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public unfiltered(): IEntityQuery<TE> {
|
|
36
|
+
return this.context.query(this.type.typeClass);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public modify(): IEntityQuery<TE> {
|
|
40
|
+
const read = this.context.query(this.type.typeClass);
|
|
41
|
+
return this.events.modify(read);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export default abstract class EntityEvents<T> {
|
|
47
|
+
|
|
48
|
+
filter(query: IEntityQuery<T>) {
|
|
49
|
+
return query;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
includeFilter(query: IEntityQuery<T>, type?: any, key?: string) {
|
|
53
|
+
return this.filter(query);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
modify(query: IEntityQuery<T>) {
|
|
57
|
+
return this.filter(query);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
delete(query: IEntityQuery<T>) {
|
|
61
|
+
return this.modify(query);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beforeInsert(entity: T, entry: ChangeEntry) {
|
|
65
|
+
return done;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onForeignKeyFilter(filter: ForeignKeyFilter<T>) {
|
|
69
|
+
return filter.modify();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
afterInsert(entity: T, entry: ChangeEntry) {
|
|
73
|
+
return done;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
beforeUpdate(entity: T, entry: ChangeEntry) {
|
|
77
|
+
return done;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
afterUpdate(entity: T, entry: ChangeEntry) {
|
|
81
|
+
return done;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
beforeDelete(entity: T, entry: ChangeEntry) {
|
|
85
|
+
return done;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
afterDelete(entity: T, entry: ChangeEntry) {
|
|
89
|
+
return done;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
beforeJson(entity: T) {
|
|
93
|
+
return entity;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import SchemaRegistry from "
|
|
1
|
+
import SchemaRegistry from "../../decorators/SchemaRegistry.js";
|
|
2
|
+
import type EntityType from "../../entity-query/EntityType.js";
|
|
3
|
+
|
|
4
|
+
export const identityMapSymbol = Symbol("identityMapSymbol");
|
|
2
5
|
|
|
3
6
|
export default class IdentityService {
|
|
4
7
|
|
|
8
|
+
public static buildIdentity(model: EntityType, ... keys: any[]) {
|
|
9
|
+
const type = model.name;
|
|
10
|
+
return JSON.stringify({ type, keys });
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
public static getIdentity(entity) {
|
|
6
14
|
const entityType = SchemaRegistry.model(Object.getPrototypeOf(entity).constructor);
|
|
7
15
|
const keys = [];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type ChangeEntry from "../changes/ChangeEntry.js";
|
|
2
|
+
import type ChangeSet from "../changes/ChangeSet.js";
|
|
3
|
+
import IdentityService, { identityMapSymbol } from "./IdentityService.js";
|
|
4
|
+
|
|
5
|
+
export default class RelationMapper {
|
|
6
|
+
|
|
7
|
+
private map: Map<string, ChangeEntry[]> = new Map();
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private changeSet: ChangeSet,
|
|
11
|
+
private identityMap: Map<string, ChangeEntry> = changeSet[identityMapSymbol]
|
|
12
|
+
) {
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
push(id: string, waiter: ChangeEntry) {
|
|
17
|
+
let queue = this.map.get(id);
|
|
18
|
+
if (!queue) {
|
|
19
|
+
queue = [];
|
|
20
|
+
this.map.set(id, queue);
|
|
21
|
+
}
|
|
22
|
+
queue.push(waiter);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fix(entry: ChangeEntry, nest = true) {
|
|
26
|
+
|
|
27
|
+
// find all parents...
|
|
28
|
+
const { type, entity } = entry;
|
|
29
|
+
for (const iterator of type.relations) {
|
|
30
|
+
if (iterator.isInverseRelation) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const fkColumn = iterator.fkColumn.name;
|
|
34
|
+
const fkValue = entity[fkColumn];
|
|
35
|
+
if (fkValue === void 0) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// get from identity...
|
|
39
|
+
const id = IdentityService.buildIdentity(iterator.relatedEntity, fkValue);
|
|
40
|
+
const parent = this.identityMap.get(id);
|
|
41
|
+
if (!parent) {
|
|
42
|
+
let waiters = this.map.get(id);
|
|
43
|
+
if (!waiters) {
|
|
44
|
+
waiters = [];
|
|
45
|
+
this.map.set(id, waiters);
|
|
46
|
+
}
|
|
47
|
+
waiters.push(entry);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
entity[iterator.name] = parent;
|
|
51
|
+
|
|
52
|
+
const coll = (parent[iterator.relatedRelation.name] ??= []) as any[];
|
|
53
|
+
if(!coll.includes(entity)){
|
|
54
|
+
coll.push(entity);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!nest) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// see if anyone is waiting for us or not...
|
|
63
|
+
const identity = IdentityService.getIdentity(entry.entity);
|
|
64
|
+
const pending = this.map.get(identity);
|
|
65
|
+
if (pending && pending.length) {
|
|
66
|
+
for (const iterator of pending) {
|
|
67
|
+
this.fix(iterator, false);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const existsQuery = Symbol("existsQuery");
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import EntityAccessError from "../../common/EntityAccessError.js";
|
|
2
|
+
import Logger from "../../common/Logger.js";
|
|
3
|
+
import { TypeInfo } from "../../common/TypeInfo.js";
|
|
4
|
+
import { IEntityRelation } from "../../decorators/IColumn.js";
|
|
5
|
+
import { ServiceProvider } from "../../di/di.js";
|
|
6
|
+
import EntityType from "../../entity-query/EntityType.js";
|
|
7
|
+
import { ConditionalExpression, Constant, ExistsExpression, Expression, Identifier, ParameterExpression, QuotedLiteral, SelectStatement, TemplateLiteral, ValuesStatement } from "../../query/ast/Expressions.js";
|
|
8
|
+
import EntityContext from "../EntityContext.js";
|
|
9
|
+
import EntityQuery from "../EntityQuery.js";
|
|
10
|
+
import ChangeEntry from "../changes/ChangeEntry.js";
|
|
11
|
+
import EntityEvents, { ForeignKeyFilter } from "../events/EntityEvents.js";
|
|
12
|
+
|
|
13
|
+
type KeyValueArray = [string, any][];
|
|
14
|
+
|
|
15
|
+
export default class VerificationSession {
|
|
16
|
+
|
|
17
|
+
private select: SelectStatement;
|
|
18
|
+
|
|
19
|
+
private field: ConditionalExpression[];
|
|
20
|
+
|
|
21
|
+
constructor(private context: EntityContext) {
|
|
22
|
+
|
|
23
|
+
this.select = SelectStatement.create({});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
queueVerification(change: ChangeEntry, events: EntityEvents<any>) {
|
|
27
|
+
const { type, entity } = change;
|
|
28
|
+
if (change.status !== "inserted") {
|
|
29
|
+
// verify access to the entity
|
|
30
|
+
const keys = [] as KeyValueArray;
|
|
31
|
+
for (const iterator of type.keys) {
|
|
32
|
+
const key = entity[iterator.name];
|
|
33
|
+
if (key === void 0) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
keys.push([iterator.columnName, key]);
|
|
37
|
+
}
|
|
38
|
+
if (keys.length === type.keys.length) {
|
|
39
|
+
this.queueEntityKey(change, keys, events);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (change.status === "deleted") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// for modified or inserted
|
|
48
|
+
// we need to verify access to each foreign key
|
|
49
|
+
|
|
50
|
+
for (const relation of type.relations) {
|
|
51
|
+
if (relation.isInverseRelation) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const fk = relation.fkColumn;
|
|
56
|
+
if (!fk) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const fkValue = entity[fk.name];
|
|
61
|
+
if (fkValue === void 0) {
|
|
62
|
+
// not set... ignore..
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
this.queueEntityForeignKey(change, relation, fkValue);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
queueEntityForeignKey(change: ChangeEntry, relation: IEntityRelation, value) {
|
|
69
|
+
const relatedModel = relation.relatedEntity;
|
|
70
|
+
const type = relation.relatedEntity.typeClass;
|
|
71
|
+
const events = this.context.eventsFor(change.type.typeClass);
|
|
72
|
+
const relatedEvents = this.context.eventsFor(relation.relatedEntity.typeClass);
|
|
73
|
+
const context = this.context;
|
|
74
|
+
const fk = new ForeignKeyFilter({
|
|
75
|
+
context,
|
|
76
|
+
events: relatedEvents,
|
|
77
|
+
type: relatedModel,
|
|
78
|
+
name: relation.name,
|
|
79
|
+
fkName: relation.fkColumn.name
|
|
80
|
+
});
|
|
81
|
+
let query = events.onForeignKeyFilter(fk);
|
|
82
|
+
if (query === void 0) {
|
|
83
|
+
query = fk.modify();
|
|
84
|
+
}
|
|
85
|
+
if (query === null) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const eq = query as EntityQuery;
|
|
90
|
+
const compare = Expression.equal(
|
|
91
|
+
Expression.member(eq.selectStatement.as, relatedModel.keys[0].columnName),
|
|
92
|
+
Expression.constant(value)
|
|
93
|
+
);
|
|
94
|
+
const typeName = TypeInfo.nameOfType(type);
|
|
95
|
+
this.addError(query as EntityQuery, compare , `Unable to access entity ${typeName} through foreign key ${TypeInfo.nameOfType(change.type)}.${relation.name}.\n`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
queueEntityKey(change: ChangeEntry, keys: KeyValueArray, events: EntityEvents<any>) {
|
|
99
|
+
const type = change.type.typeClass;
|
|
100
|
+
let query = this.context.query(type);
|
|
101
|
+
query = change.status === "modified" ? events.modify(query) : events.delete(query);
|
|
102
|
+
if (!query) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let compare: Expression;
|
|
106
|
+
const eq = query as EntityQuery;
|
|
107
|
+
for (const [key, value] of keys) {
|
|
108
|
+
const test = Expression.equal(
|
|
109
|
+
Expression.member(eq.selectStatement.as, Expression.quotedLiteral(key)),
|
|
110
|
+
Expression.constant(value)
|
|
111
|
+
);
|
|
112
|
+
compare = compare
|
|
113
|
+
? Expression.logicalAnd(compare, test)
|
|
114
|
+
: test;
|
|
115
|
+
}
|
|
116
|
+
const typeName = TypeInfo.nameOfType(type);
|
|
117
|
+
this.addError(query as EntityQuery, compare, `Unable to access entity ${typeName}.\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async verifyAsync(): Promise<any> {
|
|
121
|
+
if (!this.field?.length) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.select.fields =[
|
|
125
|
+
Expression.as(Expression.templateLiteral(this.field), "error")
|
|
126
|
+
];
|
|
127
|
+
this.select.as = ParameterExpression.create({ name: "x"});
|
|
128
|
+
const source = ValuesStatement.create({
|
|
129
|
+
values: [
|
|
130
|
+
[Identifier.create({ value: "1"})]
|
|
131
|
+
],
|
|
132
|
+
as: QuotedLiteral.create({ literal: "a"}),
|
|
133
|
+
fields: [QuotedLiteral.create({ literal: "a"})]
|
|
134
|
+
});
|
|
135
|
+
this.select.source = source;
|
|
136
|
+
const compiler = this.context.driver.compiler;
|
|
137
|
+
const query = compiler.compileExpression(null, this.select);
|
|
138
|
+
const logger = ServiceProvider.resolve(this.context, Logger);
|
|
139
|
+
const session = logger.newSession();
|
|
140
|
+
try {
|
|
141
|
+
const { rows: [ { error }]} = await this.context.driver.executeQuery(query);
|
|
142
|
+
if (error) {
|
|
143
|
+
session.error(`Failed executing ${query.text}\n[${query.values.join(",")}]\n${error?.stack ?? error}`);
|
|
144
|
+
EntityAccessError.throw(error);
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
session.dispose();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
addError(query: EntityQuery, compare: Expression, error: string) {
|
|
152
|
+
const select = { ... query.selectStatement};
|
|
153
|
+
select.fields = [
|
|
154
|
+
Expression.identifier("1")
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const where = select.where
|
|
158
|
+
? Expression.logicalAnd(select.where, compare)
|
|
159
|
+
: compare;
|
|
160
|
+
|
|
161
|
+
select.where = where;
|
|
162
|
+
|
|
163
|
+
const text = ConditionalExpression.create({
|
|
164
|
+
test: ExistsExpression.create({
|
|
165
|
+
target: select
|
|
166
|
+
}),
|
|
167
|
+
consequent: Expression.constant(""),
|
|
168
|
+
alternate: Expression.constant(error),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
(this.field ??=[]).push(text);
|
|
172
|
+
}
|
|
173
|
+
}
|