@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,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
  }
@@ -1,17 +1,39 @@
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 { QueryExpander } from "../query/expander/QueryExpander.js";
7
+ import EntityContext from "./EntityContext.js";
8
+ import { IOrderedEntityQuery, IEntityQuery } from "./IFilterWithParameter.js";
9
+ import RelationMapper from "./identity/RelationMapper.js";
5
10
 
6
11
  export default class EntityQuery<T = any>
7
12
  implements IOrderedEntityQuery<T>, IEntityQuery<T> {
8
- constructor (public readonly source: SourceExpression) {
13
+
14
+ public context: EntityContext;
15
+ public type: EntityType;
16
+ public selectStatement: SelectStatement;
17
+ public signal?: AbortSignal;
18
+ constructor (p: Partial<EntityQuery<any>>
19
+ ) {
20
+ // lets clone select...
21
+ Object.setPrototypeOf(p, EntityQuery.prototype);
22
+ return p as EntityQuery;
23
+ }
24
+
25
+ select(p: any, fx: any): any {
26
+ return this.map(p, fx);
27
+ }
28
+
29
+ map(p: any, fx: any): any {
30
+ // const source = this.source.copy();
31
+ // const { select } = source;
32
+ // const exp = this.source.context.driver.compiler.compileToExpression(source, p, fx);
9
33
  }
10
34
 
11
35
  withSignal(signal: AbortSignal): any {
12
- const source = this.source.copy();
13
- source.signal = signal;
14
- return new EntityQuery(source);
36
+ return new EntityQuery({ ... this, signal });
15
37
  }
16
38
 
17
39
  thenBy(parameters: any, fx: any): any {
@@ -20,21 +42,21 @@ export default class EntityQuery<T = any>
20
42
  thenByDescending(parameters: any, fx: any) {
21
43
  return this.orderByDescending(parameters, fx);
22
44
  }
45
+
23
46
  where<P>(parameters: P, fx: (p: P) => (x: T) => boolean): any {
24
47
 
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);
48
+ return this.extend(parameters, fx, (select, body) => ({
49
+ ... select,
50
+ where: select.where ? Expression.logicalAnd(select.where, body): body
51
+ }));
52
+ }
53
+
54
+ include(p: any): any {
55
+ const selectStatement = QueryExpander.expand(this.context, { ... this.selectStatement }, p);
56
+ return new EntityQuery({
57
+ ... this,
58
+ selectStatement
59
+ });
38
60
  }
39
61
 
40
62
  async toArray(): Promise<T[]> {
@@ -46,21 +68,63 @@ export default class EntityQuery<T = any>
46
68
  }
47
69
 
48
70
  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);
71
+ const logger = ServiceProvider.resolve(this.context, Logger);
72
+ const scope = new DisposableScope();
73
+ const session = logger.newSession();
74
+ let query: { text: string, values: any[]};
53
75
  try {
76
+ scope.register(session);
77
+ const type = this.type;
78
+ const signal = this.signal;
79
+
80
+ const relationMapper = new RelationMapper(this.context.changeSet);
81
+
82
+ const include = this.selectStatement.include;
83
+ if (include?.length > 0) {
84
+ // since we will be streaming results...
85
+ // it is important that we load all the
86
+ // included entities first...
87
+ const loaders = include.map((x) => this.load(relationMapper, session, x, signal));
88
+ await Promise.all(loaders);
89
+ }
90
+
91
+ signal?.throwIfAborted();
92
+
93
+ query = this.context.driver.compiler.compileExpression(this, this.selectStatement);
94
+ const reader = await this.context.driver.executeReader(query, signal);
95
+ scope.register(reader);
54
96
  for await (const iterator of reader.next(10, signal)) {
55
97
  if (type) {
56
- Object.setPrototypeOf(iterator, type.prototype);
98
+ Object.setPrototypeOf(iterator, type.typeClass.prototype);
57
99
  // set identity...
58
- const entry = this.source.context.changeSet.getEntry(iterator, iterator);
100
+ const entry = this.context.changeSet.getEntry(iterator, iterator);
101
+ relationMapper.fix(entry);
59
102
  yield entry.entity;
60
103
  continue;
61
104
  }
62
105
  yield iterator as T;
63
106
  }
107
+
108
+ } catch(error) {
109
+ session.error(`Failed executing ${query?.text}\n${error.stack ?? error}`);
110
+ throw error;
111
+ } finally {
112
+ await scope.dispose();
113
+ }
114
+ }
115
+
116
+ async load(relationMapper: RelationMapper, session: Logger, select: SelectStatement, signal: AbortSignal) {
117
+ const query = this.context.driver.compiler.compileExpression(this, select);
118
+ const reader = await this.context.driver.executeReader(query, signal);
119
+ try {
120
+ for await (const iterator of reader.next(10, signal)) {
121
+ Object.setPrototypeOf(iterator, select.model.typeClass.prototype);
122
+ const entry = this.context.changeSet.getEntry(iterator, iterator);
123
+ relationMapper.fix(entry);
124
+ }
125
+ } catch (error) {
126
+ session.error(`Failed loading ${query.text}\n${error.stack ?? error}`);
127
+ throw error;
64
128
  } finally {
65
129
  await reader.dispose();
66
130
  }
@@ -70,7 +134,7 @@ export default class EntityQuery<T = any>
70
134
  for await(const iterator of this.limit(1).enumerate()) {
71
135
  return iterator;
72
136
  }
73
- throw new Error(`No records found for ${this.source.model?.name || "Table"}`);
137
+ throw new Error(`No records found for ${this.type?.name || "Table"}`);
74
138
  }
75
139
 
76
140
  async first(): Promise<T> {
@@ -80,42 +144,32 @@ export default class EntityQuery<T = any>
80
144
  return null;
81
145
  }
82
146
  toQuery(): { text: string; values: any[]; } {
83
- return this.source.context.driver.compiler.compileExpression(this.source.select);
147
+ return this.context.driver.compiler.compileExpression(this, this.selectStatement);
84
148
  }
85
149
  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
150
+ return this.extend(parameters, fx, (select, target) => ({
151
+ ... select,
152
+ orderBy: select.orderBy
153
+ ? [ ... select.orderBy, OrderByExpression.create({ target})]
154
+ : [OrderByExpression.create({ target})]
92
155
  }));
93
- return new EntityQuery(source);
94
156
  }
95
157
  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
158
+ const descending = true;
159
+ return this.extend(parameters, fx, (select, target) => ({
160
+ ... select,
161
+ orderBy: select.orderBy
162
+ ? [ ... select.orderBy, OrderByExpression.create({ target, descending })]
163
+ : [OrderByExpression.create({ target, descending })]
103
164
  }));
104
- return new EntityQuery(source);
105
165
  }
106
166
 
107
167
  limit(n: number): any {
108
- const source = this.source.copy();
109
- const { select } = source;
110
- select.limit = n;
111
- return new EntityQuery(source);
168
+ return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, limit: n} });
112
169
  }
113
170
 
114
171
  offset(n: number): any {
115
- const source = this.source.copy();
116
- const { select } = source;
117
- select.offset = n;
118
- return new EntityQuery(source);
172
+ return new EntityQuery({ ... this, selectStatement: { ... this.selectStatement, offset: n} });
119
173
  }
120
174
 
121
175
  async count(parameters?:any, fx?: any): Promise<number> {
@@ -123,10 +177,7 @@ export default class EntityQuery<T = any>
123
177
  return this.where(parameters, fx).count();
124
178
  }
125
179
 
126
- const source = this.source.copy();
127
- const { select } = source;
128
-
129
- select.fields = [
180
+ const select = { ... this.selectStatement, fields: [
130
181
  ExpressionAs.create({
131
182
  expression: CallExpression.create({
132
183
  callee: Identifier.create({ value: "COUNT"}),
@@ -134,10 +185,12 @@ export default class EntityQuery<T = any>
134
185
  }),
135
186
  alias: QuotedLiteral.create({ literal: "count" })
136
187
  })
137
- ];
188
+ ] };
138
189
 
139
- const query = this.source.context.driver.compiler.compileExpression(select);
140
- const reader = await this.source.context.driver.executeReader(query);
190
+ const nq = new EntityQuery({ ... this, selectStatement: select });
191
+
192
+ const query = this.context.driver.compiler.compileExpression(nq, select);
193
+ const reader = await this.context.driver.executeReader(query);
141
194
 
142
195
  try {
143
196
  for await (const iterator of reader.next()) {
@@ -149,4 +202,10 @@ export default class EntityQuery<T = any>
149
202
 
150
203
  }
151
204
 
205
+ private extend(parameters: any, fx: any, map: (select: SelectStatement, exp: Expression) => SelectStatement) {
206
+ const exp = this.context.driver.compiler.compile(this, fx);
207
+ exp.params[0].value = parameters;
208
+ return new EntityQuery({ ... this, selectStatement: map(this.selectStatement, exp.body)});
209
+ }
210
+
152
211
  }
@@ -1,25 +1,12 @@
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 } from "../query/ast/Expressions.js";
5
5
  import EntityQuery from "./EntityQuery.js";
6
- import TimedCache from "../common/cache/TimedCache.js";
7
6
  import { contextSymbol, modelSymbol } from "../common/symbols/symbols.js";
8
- import { SourceExpression } from "./SourceExpression.js";
9
-
10
- const modelCache = new TimedCache<any, SelectStatement>();
11
7
 
12
8
  export class EntitySource<T = any> {
13
9
 
14
-
15
- public readonly filters: {
16
- read?: () => IFilterExpression;
17
- modify?: () => IFilterExpression;
18
- delete?: () => IFilterExpression;
19
- include?: () => IFilterExpression;
20
- } = {};
21
-
22
-
23
10
  get [modelSymbol]() {
24
11
  return this.model;
25
12
  }
@@ -37,7 +24,7 @@ export class EntitySource<T = any> {
37
24
 
38
25
  }
39
26
 
40
- public add(item: Partial<T>) {
27
+ public add(item: Partial<T>): T {
41
28
  const p = Object.getPrototypeOf(item).constructor;
42
29
  if (!p || p === Object) {
43
30
  Object.setPrototypeOf(item, this.model.typeClass.prototype);
@@ -47,45 +34,50 @@ export class EntitySource<T = any> {
47
34
  throw new Error("Entity is already attached to the context");
48
35
  }
49
36
  entry.status = "inserted";
50
- return item as T;
37
+ return entry.entity;
38
+ }
39
+
40
+ /**
41
+ * Entity can only be deleted if all primary keys are present
42
+ * @param item entity to delete
43
+ */
44
+ public delete(item: Partial<T>) {
45
+ const p = Object.getPrototypeOf(item).constructor;
46
+ if (!p || p === Object) {
47
+ Object.setPrototypeOf(item, this.model.typeClass.prototype);
48
+ }
49
+ const entry = this.context.changeSet.getEntry(item);
50
+ if (entry.status !== "detached" && entry.status !== "unchanged") {
51
+ throw new Error("Entity is already attached to the context");
52
+ }
53
+ entry.status = "deleted";
54
+ return entry.entity;
51
55
  }
52
56
 
53
57
  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>;
58
+ return this.asQuery();
59
+ }
60
+
61
+ public filtered(mode: "read" | "modify" = "read"): IEntityQuery<T> {
62
+ const query = this.asQuery();
63
+ const events = this.context.eventsFor(this.model.typeClass, true);
64
+ return mode === "modify" ? events.modify(query) : events.filter(query);
62
65
  }
63
66
 
64
67
  public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
68
+ return this.asQuery().where(parameter, fx);
69
+ }
70
+
71
+ public asQuery() {
65
72
  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,
73
+ const selectStatement = this.model.selectAllFields();
74
+ selectStatement.model = model;
75
+ return new EntityQuery<T>({
69
76
  context,
70
- model,
71
- select
72
- })).where(parameter, fx) as IEntityQuery<T>;
73
- }
77
+ type: model,
78
+ selectStatement
79
+ }) as any as IEntityQuery<T>;
74
80
 
75
- generateModel(): SelectStatement {
76
- const source = this.model.fullyQualifiedName;
77
- const as = QuotedLiteral.create({ literal: this.model.name[0] + "1" });
78
- const fields = this.model.columns.map((c) => c.name !== c.columnName
79
- ? ExpressionAs.create({
80
- expression: QuotedLiteral.propertyChain(as.literal, c.columnName),
81
- alias: QuotedLiteral.create({ literal: c.name })
82
- })
83
- : QuotedLiteral.propertyChain(as.literal, c.columnName));
84
- return SelectStatement.create({
85
- source,
86
- as,
87
- fields,
88
- });
89
81
  }
90
82
 
91
83
  public toQuery() {
@@ -93,6 +85,6 @@ export class EntitySource<T = any> {
93
85
  if(!filter) {
94
86
  return "";
95
87
  }
96
- return this.context.driver.compiler.compileExpression(filter);
88
+ return this.context.driver.compiler.compileExpression( this.asQuery() as any, filter);
97
89
  }
98
90
  }
@@ -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[]};
@@ -23,6 +26,8 @@ export interface IBaseQuery<T> {
23
26
  count<P>(parameters: P, fx: (p: P) => (x: T) => boolean): Promise<number>;
24
27
 
25
28
  withSignal<DT>(this:DT, signal: AbortSignal): DT;
29
+
30
+ include<TR>(fx: (x: T) => TR | TR[]): IBaseQuery<T>;
26
31
  }
27
32
 
28
33
  export interface IOrderedEntityQuery<T> extends IBaseQuery<T> {