@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
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 { ITextOrFunctionArray, prepare } from "../../query/ast/IStringTransformer.js";
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): ITextOrFunctionArray {
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): ITextOrFunctionArray {
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): ITextOrFunctionArray {
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
- const orderBy = e.orderBy?.length > 0 ? prepare `\n\t\tORDER BY ${this.visitArray(e.orderBy)}` : "";
68
- const source = this.visit(e.source);
69
- const where = e.where ? prepare `\n\tWHERE ${this.visit(e.where)}` : "";
70
- const as = e.as ? prepare ` AS ${this.visit(e.as)}` : "";
71
- const joins = e.joins?.length > 0 ? prepare `\n\t\t${this.visitArray(e.joins)}` : [];
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
- database: config.database,
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 key = this.config.server + "//" + this.config.database + "/" + this.config.user;
139
- return namedPool.getOrCreateAsync(this.config.server + "://" + this.config.database,
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(this.config);
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/MemberParser.js";
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 QueryCompiler from "../compiler/QueryCompiler.js";
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
- public driver: BaseDriver
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
- await this.driver.runInTransaction(async () => {
23
- for (const iterator of this.changeSet.entries) {
24
- switch(iterator.status) {
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
- const insert = this.driver.createInsertExpression(iterator.type, iterator.entity);
27
- const r = await this.executeExpression(insert);
28
- iterator.apply(r);
29
- break;
147
+ await events.afterInsert(entity, entity);
148
+ continue;
149
+ case "deleted":
150
+ await events.afterDelete(entity, entity);
151
+ continue;
30
152
  case "modified":
31
- if (iterator.modified.size > 0) {
32
- const update = this.driver.createUpdateExpression(iterator);
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
  }