@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.
Files changed (62) 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/usingAsync.ts +42 -12
  11. package/src/compiler/QueryCompiler.ts +28 -30
  12. package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +23 -0
  13. package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +23 -0
  14. package/src/decorators/ForeignKey.ts +1 -1
  15. package/src/decorators/IClassOf.ts +2 -1
  16. package/src/decorators/parser/NameParser.ts +15 -0
  17. package/src/di/di.ts +224 -0
  18. package/src/drivers/base/BaseDriver.ts +28 -3
  19. package/src/drivers/sql-server/ExpressionToSqlServer.ts +48 -9
  20. package/src/drivers/sql-server/SqlServerDriver.ts +7 -16
  21. package/src/entity-query/EntityType.ts +1 -1
  22. package/src/model/EntityContext.ts +167 -22
  23. package/src/model/EntityQuery.ts +76 -60
  24. package/src/model/EntitySource.ts +39 -33
  25. package/src/model/IFilterWithParameter.ts +3 -0
  26. package/src/model/SourceExpression.ts +21 -25
  27. package/src/model/{ChangeEntry.ts → changes/ChangeEntry.ts} +33 -3
  28. package/src/model/{ChangeSet.ts → changes/ChangeSet.ts} +12 -11
  29. package/src/model/events/ContextEvents.ts +26 -0
  30. package/src/model/events/EntityEvents.ts +92 -0
  31. package/src/model/{IdentityService.ts → identity/IdentityService.ts} +1 -1
  32. package/src/model/symbols.ts +1 -0
  33. package/src/model/verification/VerificationSession.ts +173 -0
  34. package/src/query/ast/DebugStringVisitor.ts +128 -0
  35. package/src/query/ast/ExpressionToSql.ts +289 -119
  36. package/src/query/ast/Expressions.ts +111 -12
  37. package/src/query/ast/IStringTransformer.ts +16 -4
  38. package/src/query/ast/ReplaceParameter.ts +40 -0
  39. package/src/query/ast/Visitor.ts +20 -5
  40. package/src/query/parser/ArrowToExpression.ts +116 -16
  41. package/src/query/parser/BabelVisitor.ts +27 -44
  42. package/src/query/parser/NotSupportedError.ts +5 -0
  43. package/src/query/parser/Restructure.ts +66 -0
  44. package/src/query/parser/TransformVisitor.ts +83 -0
  45. package/src/sql/ISql.ts +10 -0
  46. package/src/tests/db-tests/tests/select-items.ts +12 -0
  47. package/src/tests/expressions/left-joins/child-joins.ts +19 -26
  48. package/src/tests/expressions/sanitize/sanitize-test.ts +17 -0
  49. package/src/tests/expressions/select/select.ts +24 -0
  50. package/src/tests/expressions/simple/parse-arrow.ts +10 -0
  51. package/src/tests/model/ShoppingContext.ts +7 -3
  52. package/src/tests/model/createContext.ts +45 -13
  53. package/src/tests/security/ShoppingContextEvents.ts +20 -0
  54. package/src/tests/security/events/OrderEvents.ts +66 -0
  55. package/src/tests/security/events/ProductEvents.ts +92 -0
  56. package/src/tests/security/events/UserEvents.ts +18 -0
  57. package/src/tests/security/events/UserInfo.ts +7 -0
  58. package/src/tests/security/tests/place-order.ts +71 -0
  59. package/test.js +11 -4
  60. package/tsconfig.json +2 -0
  61. package/src/decorators/parser/MemberParser.ts +0 -8
  62. package/src/model/EntitySchema.ts +0 -21
@@ -1,17 +1,37 @@
1
- import { BinaryExpression, CallExpression, Expression, ExpressionAs, Identifier, OrderByExpression, QuotedLiteral, SelectStatement, TableSource } from "../query/ast/Expressions.js";
2
- import { EntitySource } from "./EntitySource.js";
3
- import { IOrderedEntityQuery, IEntityQuery, ILambdaExpression } from "./IFilterWithParameter.js";
4
- import { SourceExpression } from "./SourceExpression.js";
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 EntityContext from "./EntityContext.js";
7
+ import { IOrderedEntityQuery, IEntityQuery } from "./IFilterWithParameter.js";
5
8
 
6
9
  export default class EntityQuery<T = any>
7
10
  implements IOrderedEntityQuery<T>, IEntityQuery<T> {
8
- constructor (public readonly source: SourceExpression) {
11
+
12
+ public context: EntityContext;
13
+ public type: EntityType;
14
+ public selectStatement: SelectStatement;
15
+ public signal?: AbortSignal;
16
+ constructor (p: Partial<EntityQuery<any>>
17
+ ) {
18
+ // lets clone select...
19
+ Object.setPrototypeOf(p, EntityQuery.prototype);
20
+ return p as EntityQuery;
21
+ }
22
+
23
+ select(p: any, fx: any): any {
24
+ return this.map(p, fx);
25
+ }
26
+
27
+ map(p: any, fx: any): any {
28
+ // const source = this.source.copy();
29
+ // const { select } = source;
30
+ // const exp = this.source.context.driver.compiler.compileToExpression(source, p, fx);
9
31
  }
10
32
 
11
33
  withSignal(signal: AbortSignal): any {
12
- const source = this.source.copy();
13
- source.signal = signal;
14
- return new EntityQuery(source);
34
+ return new EntityQuery({ ... this, signal });
15
35
  }
16
36
 
17
37
  thenBy(parameters: any, fx: any): any {
@@ -20,21 +40,13 @@ export default class EntityQuery<T = any>
20
40
  thenByDescending(parameters: any, fx: any) {
21
41
  return this.orderByDescending(parameters, fx);
22
42
  }
43
+
23
44
  where<P>(parameters: P, fx: (p: P) => (x: T) => boolean): any {
24
45
 
25
- const source = this.source.copy();
26
- const { select } = source;
27
- const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx);
28
- if(!select.where) {
29
- select.where = exp;
30
- } else {
31
- select.where = BinaryExpression.create({
32
- left: select.where,
33
- operator: "AND",
34
- right: exp
35
- });
36
- }
37
- return new EntityQuery(source);
46
+ return this.extend(parameters, fx, (select, body) => ({
47
+ ... select,
48
+ where: select.where ? Expression.logicalAnd(select.where, body): body
49
+ }));
38
50
  }
39
51
 
40
52
  async toArray(): Promise<T[]> {
@@ -46,23 +58,32 @@ export default class EntityQuery<T = any>
46
58
  }
47
59
 
48
60
  async *enumerate(): AsyncGenerator<T, any, unknown> {
49
- const type = this.source.model?.typeClass;
50
- const signal = this.source.signal;
51
- const query = this.source.context.driver.compiler.compileExpression(this.source.select);
52
- const reader = await this.source.context.driver.executeReader(query, signal);
61
+ const logger = ServiceProvider.resolve(this.context, Logger);
62
+ const scope = new DisposableScope();
63
+ const session = logger.newSession();
64
+ let query: { text: string, values: any[]};
53
65
  try {
66
+ scope.register(session);
67
+ const type = this.type;
68
+ const signal = this.signal;
69
+ query = this.context.driver.compiler.compileExpression(this, this.selectStatement);
70
+ const reader = await this.context.driver.executeReader(query, signal);
71
+ scope.register(reader);
54
72
  for await (const iterator of reader.next(10, signal)) {
55
73
  if (type) {
56
- Object.setPrototypeOf(iterator, type.prototype);
74
+ Object.setPrototypeOf(iterator, type.typeClass.prototype);
57
75
  // set identity...
58
- const entry = this.source.context.changeSet.getEntry(iterator, iterator);
76
+ const entry = this.context.changeSet.getEntry(iterator, iterator);
59
77
  yield entry.entity;
60
78
  continue;
61
79
  }
62
80
  yield iterator as T;
63
81
  }
82
+ } catch(error) {
83
+ session.error(`Failed executing ${query?.text}\n${error.stack ?? error}`);
84
+ throw error;
64
85
  } finally {
65
- await reader.dispose();
86
+ await scope.dispose();
66
87
  }
67
88
  }
68
89
 
@@ -70,7 +91,7 @@ export default class EntityQuery<T = any>
70
91
  for await(const iterator of this.limit(1).enumerate()) {
71
92
  return iterator;
72
93
  }
73
- throw new Error(`No records found for ${this.source.model?.name || "Table"}`);
94
+ throw new Error(`No records found for ${this.type?.name || "Table"}`);
74
95
  }
75
96
 
76
97
  async first(): Promise<T> {
@@ -80,42 +101,32 @@ export default class EntityQuery<T = any>
80
101
  return null;
81
102
  }
82
103
  toQuery(): { text: string; values: any[]; } {
83
- return this.source.context.driver.compiler.compileExpression(this.source.select);
104
+ return this.context.driver.compiler.compileExpression(this, this.selectStatement);
84
105
  }
85
106
  orderBy(parameters: any, fx: any): any {
86
- const source = this.source.copy();
87
- const { select } = source;
88
- const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx as any);
89
- select.orderBy ??= [];
90
- select.orderBy.push(OrderByExpression.create({
91
- target: exp
107
+ return this.extend(parameters, fx, (select, target) => ({
108
+ ... select,
109
+ orderBy: select.orderBy
110
+ ? [ ... select.orderBy, OrderByExpression.create({ target})]
111
+ : [OrderByExpression.create({ target})]
92
112
  }));
93
- return new EntityQuery(source);
94
113
  }
95
114
  orderByDescending(parameters: any, fx: any): any {
96
- const source = this.source.copy();
97
- const { select } = source;
98
- const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx as any);
99
- select.orderBy ??= [];
100
- select.orderBy.push(OrderByExpression.create({
101
- target: exp,
102
- descending: true
115
+ const descending = true;
116
+ return this.extend(parameters, fx, (select, target) => ({
117
+ ... select,
118
+ orderBy: select.orderBy
119
+ ? [ ... select.orderBy, OrderByExpression.create({ target, descending })]
120
+ : [OrderByExpression.create({ target, descending })]
103
121
  }));
104
- return new EntityQuery(source);
105
122
  }
106
123
 
107
124
  limit(n: number): any {
108
- const source = this.source.copy();
109
- const { select } = source;
110
- select.limit = n;
111
- return new EntityQuery(source);
125
+ return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, limit: n} });
112
126
  }
113
127
 
114
128
  offset(n: number): any {
115
- const source = this.source.copy();
116
- const { select } = source;
117
- select.offset = n;
118
- return new EntityQuery(source);
129
+ return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, offset: n} });
119
130
  }
120
131
 
121
132
  async count(parameters?:any, fx?: any): Promise<number> {
@@ -123,10 +134,7 @@ export default class EntityQuery<T = any>
123
134
  return this.where(parameters, fx).count();
124
135
  }
125
136
 
126
- const source = this.source.copy();
127
- const { select } = source;
128
-
129
- select.fields = [
137
+ const select = { ... this.selectStatement, fields: [
130
138
  ExpressionAs.create({
131
139
  expression: CallExpression.create({
132
140
  callee: Identifier.create({ value: "COUNT"}),
@@ -134,10 +142,12 @@ export default class EntityQuery<T = any>
134
142
  }),
135
143
  alias: QuotedLiteral.create({ literal: "count" })
136
144
  })
137
- ];
145
+ ] };
138
146
 
139
- const query = this.source.context.driver.compiler.compileExpression(select);
140
- const reader = await this.source.context.driver.executeReader(query);
147
+ const nq = new EntityQuery({ ... this, selectStatement: select });
148
+
149
+ const query = this.context.driver.compiler.compileExpression(nq, select);
150
+ const reader = await this.context.driver.executeReader(query);
141
151
 
142
152
  try {
143
153
  for await (const iterator of reader.next()) {
@@ -149,4 +159,10 @@ export default class EntityQuery<T = any>
149
159
 
150
160
  }
151
161
 
162
+ private extend(parameters: any, fx: any, map: (select: SelectStatement, exp: Expression) => SelectStatement) {
163
+ const exp = this.context.driver.compiler.compile(this, fx);
164
+ exp.params[0].value = parameters;
165
+ return new EntityQuery({ ... this, selectStatement: map(this.selectStatement, exp.body)});
166
+ }
167
+
152
168
  }
@@ -1,25 +1,15 @@
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 { BinaryExpression, Expression, ExpressionAs, QuotedLiteral, SelectStatement, TableLiteral } from "../query/ast/Expressions.js";
4
+ import { Expression, ExpressionAs, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
5
5
  import EntityQuery from "./EntityQuery.js";
6
6
  import TimedCache from "../common/cache/TimedCache.js";
7
7
  import { contextSymbol, modelSymbol } from "../common/symbols/symbols.js";
8
- import { SourceExpression } from "./SourceExpression.js";
9
8
 
10
9
  const modelCache = new TimedCache<any, SelectStatement>();
11
10
 
12
11
  export class EntitySource<T = any> {
13
12
 
14
-
15
- public readonly filters: {
16
- read?: () => IFilterExpression;
17
- modify?: () => IFilterExpression;
18
- delete?: () => IFilterExpression;
19
- include?: () => IFilterExpression;
20
- } = {};
21
-
22
-
23
13
  get [modelSymbol]() {
24
14
  return this.model;
25
15
  }
@@ -37,7 +27,7 @@ export class EntitySource<T = any> {
37
27
 
38
28
  }
39
29
 
40
- public add(item: Partial<T>) {
30
+ public add(item: Partial<T>): T {
41
31
  const p = Object.getPrototypeOf(item).constructor;
42
32
  if (!p || p === Object) {
43
33
  Object.setPrototypeOf(item, this.model.typeClass.prototype);
@@ -47,52 +37,68 @@ export class EntitySource<T = any> {
47
37
  throw new Error("Entity is already attached to the context");
48
38
  }
49
39
  entry.status = "inserted";
50
- return item as T;
40
+ return entry.entity;
41
+ }
42
+
43
+ /**
44
+ * Entity can only be deleted if all primary keys are present
45
+ * @param item entity to delete
46
+ */
47
+ public delete(item: Partial<T>) {
48
+ const p = Object.getPrototypeOf(item).constructor;
49
+ if (!p || p === Object) {
50
+ Object.setPrototypeOf(item, this.model.typeClass.prototype);
51
+ }
52
+ const entry = this.context.changeSet.getEntry(item);
53
+ if (entry.status !== "detached" && entry.status !== "unchanged") {
54
+ throw new Error("Entity is already attached to the context");
55
+ }
56
+ entry.status = "deleted";
57
+ return entry.entity;
51
58
  }
52
59
 
53
60
  public all(): IEntityQuery<T> {
54
- const { model, context } = this;
55
- const select = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
56
- return new EntityQuery<T>(SourceExpression.create({
57
- alias: select.as.literal,
58
- context,
59
- model,
60
- select
61
- })) as IEntityQuery<T>;
61
+ return this.asQuery();
62
62
  }
63
63
 
64
64
  public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
65
- const { model, context } = this;
66
- const select = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
67
- return new EntityQuery<T>(SourceExpression.create({
68
- alias: select.as.literal,
69
- context,
70
- model,
71
- select
72
- })).where(parameter, fx) as IEntityQuery<T>;
65
+ return this.asQuery().where(parameter, fx);
73
66
  }
74
67
 
75
68
  generateModel(): SelectStatement {
76
69
  const source = this.model.fullyQualifiedName;
77
- const as = QuotedLiteral.create({ literal: this.model.name[0] + "1" });
70
+ const as = Expression.parameter(this.model.name[0] + "1");
78
71
  const fields = this.model.columns.map((c) => c.name !== c.columnName
79
72
  ? ExpressionAs.create({
80
- expression: QuotedLiteral.propertyChain(as.literal, c.columnName),
73
+ expression: Expression.member(as, c.columnName),
81
74
  alias: QuotedLiteral.create({ literal: c.name })
82
75
  })
83
- : QuotedLiteral.propertyChain(as.literal, c.columnName));
76
+ : Expression.member(as, c.columnName));
84
77
  return SelectStatement.create({
85
78
  source,
86
79
  as,
87
80
  fields,
81
+ names: JSON.stringify([as.name])
88
82
  });
89
83
  }
90
84
 
85
+ public asQuery() {
86
+ const { model, context } = this;
87
+ const selectStatement = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
88
+ selectStatement.model = model;
89
+ return new EntityQuery<T>({
90
+ context,
91
+ type: model,
92
+ selectStatement
93
+ }) as any as IEntityQuery<T>;
94
+
95
+ }
96
+
91
97
  public toQuery() {
92
98
  const filter = this.filter;
93
99
  if(!filter) {
94
100
  return "";
95
101
  }
96
- return this.context.driver.compiler.compileExpression(filter);
102
+ return this.context.driver.compiler.compileExpression( this.asQuery() as any, filter);
97
103
  }
98
104
  }
@@ -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[]};
@@ -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
  }
@@ -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
164
  if (!iterator.isCollection) {
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 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
 
@@ -32,6 +25,15 @@ export default class ChangeSet {
32
25
  [privateUpdateEntry](entry: ChangeEntry) {
33
26
  const jsonKey = IdentityService.getIdentity(entry.entity);
34
27
  if (jsonKey) {
28
+ if (entry.status === "deleted") {
29
+ this.identityMap.delete(jsonKey);
30
+ const index = this.entries.indexOf(entry);
31
+ if (index !== -1) {
32
+ this.entries.splice(index, 1);
33
+ }
34
+ this.entryMap.delete(entry.entity);
35
+ return;
36
+ }
35
37
  this.identityMap.set(jsonKey, entry.entity);
36
38
  }
37
39
  }
@@ -62,7 +64,6 @@ export default class ChangeSet {
62
64
  original: original ? { ... original } : void 0,
63
65
  status: "unchanged"
64
66
  }, this);
65
- entity[entrySymbol] = entry;
66
67
  this.entries.push(entry);
67
68
  this.entryMap.set(entity, entry);
68
69
  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
+ }