@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.
Files changed (69) hide show
  1. package/.github/workflows/node.yml +7 -2
  2. package/.vscode/settings.json +1 -0
  3. package/README.md +57 -27
  4. package/package.json +2 -2
  5. package/src/common/EntityAccessError.ts +10 -0
  6. package/src/common/IDisposable.ts +25 -0
  7. package/src/common/ImmutableObject.ts +53 -0
  8. package/src/common/Logger.ts +59 -0
  9. package/src/common/TypeInfo.ts +3 -0
  10. package/src/common/cache/TimedCache.ts +2 -2
  11. package/src/common/usingAsync.ts +42 -12
  12. package/src/compiler/QueryCompiler.ts +28 -30
  13. package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +23 -0
  14. package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +23 -0
  15. package/src/decorators/ForeignKey.ts +1 -1
  16. package/src/decorators/IClassOf.ts +2 -1
  17. package/src/decorators/IColumn.ts +2 -0
  18. package/src/decorators/parser/NameParser.ts +15 -0
  19. package/src/di/di.ts +224 -0
  20. package/src/drivers/base/BaseDriver.ts +28 -3
  21. package/src/drivers/sql-server/ExpressionToSqlServer.ts +34 -9
  22. package/src/drivers/sql-server/SqlServerDriver.ts +7 -16
  23. package/src/entity-query/EntityType.ts +45 -3
  24. package/src/model/EntityContext.ts +167 -22
  25. package/src/model/EntityQuery.ts +118 -59
  26. package/src/model/EntitySource.ts +38 -46
  27. package/src/model/IFilterWithParameter.ts +5 -0
  28. package/src/model/SourceExpression.ts +21 -25
  29. package/src/model/{ChangeEntry.ts → changes/ChangeEntry.ts} +35 -5
  30. package/src/model/{ChangeSet.ts → changes/ChangeSet.ts} +16 -11
  31. package/src/model/events/ContextEvents.ts +26 -0
  32. package/src/model/events/EntityEvents.ts +96 -0
  33. package/src/model/{IdentityService.ts → identity/IdentityService.ts} +9 -1
  34. package/src/model/identity/RelationMapper.ts +71 -0
  35. package/src/model/symbols.ts +1 -0
  36. package/src/model/verification/VerificationSession.ts +173 -0
  37. package/src/query/ast/DebugStringVisitor.ts +175 -0
  38. package/src/query/ast/ExpressionToSql.ts +277 -119
  39. package/src/query/ast/Expressions.ts +130 -13
  40. package/src/query/ast/IStringTransformer.ts +19 -5
  41. package/src/query/ast/ParameterScope.ts +97 -0
  42. package/src/query/ast/ReplaceParameter.ts +40 -0
  43. package/src/query/ast/Types.ts +0 -0
  44. package/src/query/ast/Visitor.ts +26 -5
  45. package/src/query/expander/QueryExpander.ts +147 -0
  46. package/src/query/parser/ArrowToExpression.ts +134 -19
  47. package/src/query/parser/BabelVisitor.ts +31 -43
  48. package/src/query/parser/NotSupportedError.ts +5 -0
  49. package/src/query/parser/Restructure.ts +66 -0
  50. package/src/query/parser/TransformVisitor.ts +83 -0
  51. package/src/sql/ISql.ts +10 -0
  52. package/src/tests/db-tests/tests/select-items.ts +12 -0
  53. package/src/tests/expressions/left-joins/child-joins.ts +54 -34
  54. package/src/tests/expressions/sanitize/sanitize-test.ts +17 -0
  55. package/src/tests/expressions/select/select.ts +24 -0
  56. package/src/tests/expressions/simple/parse-arrow.ts +10 -0
  57. package/src/tests/model/ShoppingContext.ts +7 -3
  58. package/src/tests/model/createContext.ts +68 -17
  59. package/src/tests/security/ShoppingContextEvents.ts +20 -0
  60. package/src/tests/security/events/OrderEvents.ts +72 -0
  61. package/src/tests/security/events/ProductEvents.ts +92 -0
  62. package/src/tests/security/events/UserEvents.ts +28 -0
  63. package/src/tests/security/events/UserInfo.ts +7 -0
  64. package/src/tests/security/tests/include-items.ts +19 -0
  65. package/src/tests/security/tests/place-order.ts +104 -0
  66. package/test.js +11 -4
  67. package/tsconfig.json +2 -0
  68. package/src/decorators/parser/MemberParser.ts +0 -8
  69. 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 { ITextOrFunction } from "../query/ast/IStringTransformer.js";
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
- signal: AbortSignal;
16
- alias: string;
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: QuotedLiteral.create({ literal: source.alias}),
64
+ as: source.alias,
66
65
  joinType: column.nullable ? "LEFT" : "INNER",
67
66
  model,
68
67
  source: QuotedLiteral.create({ literal: model.name }),
69
- where: BinaryExpression.create({
70
- left: MemberExpression.create({
71
- target: Identifier.create({ value: this.alias }),
72
- property: Identifier.create({ value: column.columnName })
73
- }),
74
- operator: "=",
75
- right: MemberExpression.create({
76
- target: Identifier.create({ value: source.alias }),
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: string) {
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[]): ITextOrFunction {
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[]): ITextOrFunction {
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 `${quotedLiteral(this.alias)}.${quotedLiteral(p.field.columnName)}`;
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 "../decorators/IColumn.js";
2
- import EntityType from "../entity-query/EntityType.js";
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.isCollection) {
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.isCollection) {
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[iterator.relatedName] = this.entity;
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 type EntityContext from "./EntityContext.js";
2
- import SchemaRegistry from "../decorators/SchemaRegistry.js";
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 "../decorators/SchemaRegistry.js";
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
+ }