@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,37 +1,45 @@
1
- import { modelSymbol } from "../../common/symbols/symbols.js";
2
1
  import QueryCompiler from "../../compiler/QueryCompiler.js";
3
- import type EntityType from "../../entity-query/EntityType.js";
4
- import type EntityContext from "../../model/EntityContext.js";
5
- import { EntitySource } from "../../model/EntitySource.js";
6
- import { SourceExpression } from "../../model/SourceExpression.js";
7
- import { BigIntLiteral, BinaryExpression, BooleanLiteral, CallExpression, CoalesceExpression, Constant, DeleteStatement, ExistsExpression, Expression, ExpressionAs, ExpressionType, Identifier, InsertStatement, JoinExpression, MemberExpression, NullExpression, NumberLiteral, OrderByExpression, PlaceholderExpression, QuotedLiteral, ReturnUpdated, SelectStatement, StringLiteral, TableLiteral, TemplateLiteral, UpdateStatement, ValuesStatement } from "./Expressions.js";
8
- import { ITextOrFunctionArray, prepare, prepareJoin } from "./IStringTransformer.js";
2
+ import EntityType from "../../entity-query/EntityType.js";
3
+ import EntityQuery from "../../model/EntityQuery.js";
4
+ import { BigIntLiteral, BinaryExpression, BooleanLiteral, CallExpression, CoalesceExpression, ConditionalExpression, Constant, DeleteStatement, ExistsExpression, Expression, ExpressionAs, ExpressionType, Identifier, InsertStatement, JoinExpression, MemberExpression, NewObjectExpression, NullExpression, NumberLiteral, OrderByExpression, ParameterExpression, QuotedLiteral, ReturnUpdated, SelectStatement, StringLiteral, TableLiteral, TemplateLiteral, UpdateStatement, ValuesStatement } from "./Expressions.js";
5
+ import { ITextQuery, QueryParameter, prepare, prepareJoin } from "./IStringTransformer.js";
9
6
  import Visitor from "./Visitor.js";
10
7
 
11
- export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
8
+ interface IPropertyChain {
9
+ identifier?: Identifier,
10
+ parameter?: ParameterExpression,
11
+ chain: string[]
12
+ }
13
+
14
+ export interface IMappingModel {
15
+ parameter: ParameterExpression;
16
+ model?: EntityType;
17
+ selectStatement?: SelectStatement;
18
+ replace?: Expression;
19
+ }
12
20
 
13
- private targets: Map<string, SourceExpression> = new Map();
21
+ export default class ExpressionToSql extends Visitor<ITextQuery> {
22
+
23
+ protected targets: Map<ParameterExpression, IMappingModel> = new Map();
14
24
 
15
25
  constructor(
16
- private source: SourceExpression,
17
- private root: string,
18
- target: string,
26
+ private source: EntityQuery,
27
+ public root: ParameterExpression,
28
+ public target: ParameterExpression,
19
29
  private compiler: QueryCompiler
20
30
  ) {
21
31
  super();
22
- if (source) {
23
- source.parameter = target;
24
- this.targets.set(target, source);
25
- }
32
+ this.targets.set(root, { parameter: root, replace: root });
33
+ this.targets.set(target, { parameter: target, model: this.source?.type, replace: target ?? source?.selectStatement.as});
26
34
  }
27
35
 
28
- visitArray(e: Expression[], sep = ","): ITextOrFunctionArray {
36
+ visitArray(e: Expression[], sep = ","): ITextQuery {
29
37
  const r = e.map((x) => this.visit(x));
30
38
  return prepareJoin(r, sep);
31
39
  }
32
40
 
33
41
 
34
- visitValuesStatement(e: ValuesStatement): ITextOrFunctionArray {
42
+ visitValuesStatement(e: ValuesStatement): ITextQuery {
35
43
  const rows = [];
36
44
  for (const rowValues of e.values) {
37
45
  rows.push(prepare `(${ this.visitArray(rowValues) })`);
@@ -40,133 +48,165 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
40
48
 
41
49
  }
42
50
 
43
- visitTableLiteral(e: TableLiteral): ITextOrFunctionArray {
51
+ visitTableLiteral(e: TableLiteral): ITextQuery {
44
52
  if (e.schema) {
45
53
  return prepare `${this.visit(e.schema)}.${this.visit(e.name)}`;
46
54
  }
47
55
  return this.visit(e.name);
48
56
  }
49
57
 
50
- visitSelectStatement(e: SelectStatement): ITextOrFunctionArray {
51
- const fields = this.visitArray(e.fields, ",\n\t\t");
52
- const orderBy = e.orderBy?.length > 0 ? prepare `\n\t\tORDER BY ${this.visitArray(e.orderBy)}` : "";
53
- const source = this.visit(e.source);
58
+ visitSelectStatement(e: SelectStatement): ITextQuery {
59
+
60
+ // inject parameter and types if we don't have it..
61
+ if (e.as && e.model) {
62
+ const scope = this.targets.get(e.as);
63
+ if (!scope) {
64
+ this.targets.set(e.as, {
65
+ parameter: e.as,
66
+ model:
67
+ e.model,
68
+ replace: e.as,
69
+ selectStatement: e
70
+ });
71
+ } else {
72
+ scope.selectStatement = e;
73
+ scope.model = e.model;
74
+ }
75
+ }
76
+
54
77
  const where = e.where ? prepare `\n\tWHERE ${this.visit(e.where)}` : "";
55
- const as = e.as ? prepare ` AS ${this.visit(e.as)}` : "";
56
78
  const joins = e.joins?.length > 0 ? prepare `\n\t\t${this.visitArray(e.joins)}` : [];
79
+ const orderBy = e.orderBy?.length > 0 ? prepare `\n\t\tORDER BY ${this.visitArray(e.orderBy)}` : "";
57
80
  const limit = e.limit > 0 ? prepare ` LIMIT ${Number(e.limit).toString()}` : "";
58
81
  const offset = e.offset > 0 ? prepare ` OFFSET ${Number(e.offset).toString()}` : "";
82
+ const source = e.source.type === "ValuesStatement"
83
+ ? prepare `(${this.visit(e.source)})`
84
+ : this.visit(e.source);
85
+ const as = e.as ? prepare ` AS ${this.compiler.quotedLiteral(e.as.name)}` : "";
86
+ const fields = this.visitArray(e.fields, ",\n\t\t");
59
87
  return prepare `SELECT
60
88
  ${fields}
61
89
  FROM ${source}${as}${joins}${where}${orderBy}${limit}${offset}`;
62
90
  }
63
91
 
64
- visitQuotedLiteral(e: QuotedLiteral): ITextOrFunctionArray {
92
+ visitQuotedLiteral(e: QuotedLiteral): ITextQuery {
65
93
  return [this.compiler.quotedLiteral(e.literal)];
66
94
  }
67
95
 
68
- visitExpressionAs(e: ExpressionAs): ITextOrFunctionArray {
96
+ visitExpressionAs(e: ExpressionAs): ITextQuery {
69
97
  return prepare `${this.visit(e.expression)} AS ${this.visit(e.alias)}`;
70
98
  }
71
99
 
72
- visitConstant({value}: Constant): ITextOrFunctionArray {
100
+ visitConstant({value}: Constant): ITextQuery {
73
101
  return [() => value];
74
102
  }
75
103
 
76
- visitBigIntLiteral({ value }: BigIntLiteral): ITextOrFunctionArray {
104
+ visitBigIntLiteral({ value }: BigIntLiteral): ITextQuery {
77
105
  return [() => value];
78
106
  }
79
107
 
80
- visitNumberLiteral( { value }: NumberLiteral): ITextOrFunctionArray {
108
+ visitNumberLiteral( { value }: NumberLiteral): ITextQuery {
81
109
  return [() => value];
82
110
  }
83
111
 
84
- visitStringLiteral({ value }: StringLiteral): ITextOrFunctionArray {
112
+ visitStringLiteral({ value }: StringLiteral): ITextQuery {
85
113
  const escapeLiteral = this.compiler.escapeLiteral;
86
114
  return [() => escapeLiteral(value)];
87
115
  }
88
116
 
89
- visitBooleanLiteral( { value }: BooleanLiteral): ITextOrFunctionArray {
117
+ visitBooleanLiteral( { value }: BooleanLiteral): ITextQuery {
90
118
  return [ () => value ? "1" : "0" ];
91
119
  }
92
120
 
93
- visitTemplateLiteral(e: TemplateLiteral): ITextOrFunctionArray {
121
+ visitTemplateLiteral(e: TemplateLiteral): ITextQuery {
94
122
  const args = this.visitArray(e.value);
95
123
  return prepare `CONCAT(${args})`;
96
124
  }
97
125
 
98
- visitCallExpression(e: CallExpression): ITextOrFunctionArray {
126
+ visitCallExpression(e: CallExpression): ITextQuery {
99
127
  // let us check if we are using any of array extension methods...
100
128
  // .some alias .any
101
129
  // .find alias .firstOrDefault
102
130
 
103
131
  const targetProperty = this.getPropertyChain(e.callee as ExpressionType);
104
- if (targetProperty?.length) {
105
- const [ target , property, childProperty ] = targetProperty;
106
- const existingTarget = this.targets.get(target);
132
+ if (targetProperty) {
133
+ const { parameter , identifier, chain } = targetProperty;
134
+ const existingTarget = this.targets.get(parameter);
107
135
  if (existingTarget) {
108
136
 
109
137
 
110
138
  // calling method on property...
111
139
  // should be navigation...
112
140
  const targetType = existingTarget.model;
113
- const context = existingTarget.context;
114
- const relation = targetType?.getProperty(property);
141
+ const relation = targetType?.getProperty(chain[0]);
115
142
  if (relation) {
116
- if (/^(some|any)$/i.test(childProperty)) {
117
-
143
+ if (/^(some|any)$/i.test(chain[1])) {
118
144
 
119
145
  const body = e.arguments[0] as ExpressionType;
120
146
  if (body.type === "ArrowFunctionExpression") {
121
147
 
122
- const param1 = body.params[0] as Identifier;
123
- const relatedSource = this.source.addSource(relation.relation.relatedEntity, param1.value);
124
- const relatedModel = relatedSource.model;
148
+ const param1 = body.params[0];
149
+ const relatedModel = relation.relation.relatedEntity;
150
+ const relatedType = relatedModel.typeClass;
151
+ let query = this.source.context.query(relatedType);
152
+ const select = { ... (query as EntityQuery).selectStatement };
153
+
154
+ const replaceParam = this.createParameter(this.source.selectStatement, relatedModel.name[0]);
155
+ select.as = replaceParam;
156
+
157
+ this.targets.set(param1, { parameter: param1, model: relatedModel, replace: replaceParam });
158
+ this.targets.set(select.as, { parameter: param1, model: relatedModel, replace: replaceParam });
159
+ this.targets.set(replaceParam, { parameter: param1, model: relatedModel, replace: replaceParam });
125
160
  const targetKey = MemberExpression.create({
126
- target: Identifier.create({ value: target }),
161
+ target: this.target,
127
162
  property: Identifier.create({
128
163
  value: targetType.keys[0].columnName
129
164
  })
130
165
  });
131
166
 
132
167
  const relatedKey = MemberExpression.create({
133
- target: Identifier.create({ value: param1.value }),
168
+ target: replaceParam,
134
169
  property: Identifier.create({
135
170
  value: relation.relation.fkColumn.columnName
136
171
  })
137
172
  });
138
173
 
174
+ // check if we have filter...
175
+ const entityEvents = this.source.context.eventsFor(relatedType, false);
176
+ if (entityEvents) {
177
+ query = entityEvents.includeFilter(query);
178
+ }
179
+
180
+
181
+ const join = Expression.logicalAnd(
182
+ Expression.equal(targetKey, relatedKey),
183
+ body.body
184
+ );
185
+
186
+ let where = select.where;
187
+
188
+ if(where) {
189
+ where = BinaryExpression.create({
190
+ left: select.where,
191
+ operator: "AND",
192
+ right: join
193
+ });
194
+ } else {
195
+ where = join;
196
+ }
197
+
198
+ select.where = where;
199
+
200
+ select.fields = [
201
+ Identifier.create({ value: "1"})
202
+ ];
203
+
139
204
  const exists = ExistsExpression.create({
140
- target: SelectStatement.create({
141
- source: relatedModel.fullyQualifiedName,
142
- as: QuotedLiteral.create({ literal: relatedSource.alias }),
143
- fields: [
144
- ExpressionAs.create({ expression: NumberLiteral.create({ value: 1 }),
145
- alias: QuotedLiteral.create({ literal: param1.value + "1" })
146
- })],
147
- where: BinaryExpression.create({
148
- left: BinaryExpression.create({
149
- left: targetKey,
150
- operator: "=",
151
- right: relatedKey,
152
- }),
153
- operator: "AND",
154
- right:body.body
155
- })
156
- })
205
+ target: select
157
206
  });
158
- const select = exists.target as SelectStatement;
159
- const dispose = this.pushTarget(param1.value, SourceExpression.create({
160
- context,
161
- select,
162
- parameter: param1.value,
163
- alias: select.as.literal,
164
- model: relatedModel,
165
- parent: this.source
166
- }));
167
207
 
168
208
  const r = this.visit(exists);
169
- dispose();
209
+ this.targets.delete(param1);
170
210
  return r;
171
211
  }
172
212
 
@@ -174,8 +214,8 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
174
214
  }
175
215
  }
176
216
 
177
- if (target === "Sql") {
178
- const names = `${target}.${property}.${childProperty}`;
217
+ if (identifier?.value === "Sql") {
218
+ const names = `${identifier.value}.${chain.join(".")}`;
179
219
  const argList = e.arguments.map((x) => this.visit(x));
180
220
  const transformedCallee = this.compiler.sqlMethodTransformer(names, argList as any[]);
181
221
  if (transformedCallee) {
@@ -187,24 +227,36 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
187
227
  return prepare `${this.visit(e.callee)}(${args})`;
188
228
  }
189
229
 
190
- visitIdentifier(e: Identifier): ITextOrFunctionArray {
230
+ visitIdentifier(e: Identifier): ITextQuery {
191
231
  // need to visit parameters
192
232
  return [e.value];
193
233
  }
194
234
 
195
- visitMemberExpression(me: MemberExpression): ITextOrFunctionArray {
196
- const chain = this.getPropertyChain(me);
197
- if (chain) {
198
- const [root, key ] = chain;
199
- if (root === this.root) {
235
+ visitParameterExpression(pe: ParameterExpression): ITextQuery {
236
+ const replace = this.targets.get(pe);
237
+ if (replace && replace.replace !== pe) {
238
+ return this.visit(replace.replace);
239
+ }
240
+ const { name, value } = pe;
241
+ if (value !== void 0) {
242
+ return [() => value];
243
+ }
244
+ return [name];
245
+ }
246
+
247
+ visitMemberExpression(me: MemberExpression): ITextQuery {
248
+ const propertyChain = this.getPropertyChain(me);
249
+ if (propertyChain) {
250
+ const { parameter, identifier, chain } = propertyChain;
251
+ if (parameter === this.root) {
200
252
  // we have a parameter...
201
- return [(p) => p[key]];
253
+ return [(p) => p[chain[0]]];
202
254
  }
203
- const source = this.targets.get(root);
204
- if (source) {
205
- return [source.flatten(chain)];
255
+ if (parameter.value) {
256
+ const value = parameter.value;
257
+ return [() => value[chain[0]]];
206
258
  }
207
- return [chain.map((x) => this.compiler.quotedLiteral(x)).join(".")];
259
+ return [ QueryParameter.create(() => parameter.name, this.compiler.quotedLiteral) , "." , chain.map((x) => this.compiler.quotedLiteral(x)).join(".")];
208
260
  }
209
261
 
210
262
  const { target, computed, property } = me;
@@ -215,11 +267,11 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
215
267
  return prepare `${this.visit(target)}.${this.visit(property)}`;
216
268
  }
217
269
 
218
- visitNullExpression(e: NullExpression): ITextOrFunctionArray {
270
+ visitNullExpression(e: NullExpression): ITextQuery {
219
271
  return ["NULL"];
220
272
  }
221
273
 
222
- visitBinaryExpression(e: BinaryExpression): ITextOrFunctionArray {
274
+ visitBinaryExpression(e: BinaryExpression): ITextQuery {
223
275
  const left = e.left.type === "BinaryExpression"
224
276
  ? prepare `(${this.visit(e.left)})`
225
277
  : this.visit(e.left);
@@ -229,13 +281,21 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
229
281
  return prepare `${left} ${e.operator} ${right}`;
230
282
  }
231
283
 
232
- visitCoalesceExpression(e: CoalesceExpression): ITextOrFunctionArray {
284
+ visitConditionalExpression(e: ConditionalExpression): ITextQuery {
285
+ const test = this.visit(e.test);
286
+ const alternate = this.visit(e.alternate);
287
+ const consequent = this.visit(e.consequent);
288
+
289
+ return prepare `(CASE WHEN ${test} THEN ${consequent} ELSE ${alternate} END)`;
290
+ }
291
+
292
+ visitCoalesceExpression(e: CoalesceExpression): ITextQuery {
233
293
  const left = this.visit(e.left);
234
294
  const right = this.visit(e.right);
235
295
  return prepare `COALESCE(${left}, ${right})`;
236
296
  }
237
297
 
238
- visitReturnUpdated(e: ReturnUpdated): ITextOrFunctionArray {
298
+ visitReturnUpdated(e: ReturnUpdated): ITextQuery {
239
299
  if (!e) {
240
300
  return [];
241
301
  }
@@ -246,7 +306,7 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
246
306
  return prepare ` RETURNING ${fields}`;
247
307
  }
248
308
 
249
- visitInsertStatement(e: InsertStatement): ITextOrFunctionArray {
309
+ visitInsertStatement(e: InsertStatement): ITextQuery {
250
310
  const returnValues = this.visit(e.returnValues);
251
311
  if (e.values instanceof ValuesStatement) {
252
312
 
@@ -269,7 +329,7 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
269
329
 
270
330
  }
271
331
 
272
- visitUpdateStatement(e: UpdateStatement): ITextOrFunctionArray {
332
+ visitUpdateStatement(e: UpdateStatement): ITextQuery {
273
333
 
274
334
  const table = this.visit(e.table);
275
335
 
@@ -280,23 +340,29 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
280
340
  return prepare `UPDATE ${table} SET ${set} WHERE ${where}`;
281
341
  }
282
342
 
283
- visitDeleteStatement(e: DeleteStatement): ITextOrFunctionArray {
343
+ visitNewObjectExpression(e: NewObjectExpression): ITextQuery {
344
+ return prepare `FROM (${this.visitArray(e.properties)})`;
345
+ }
346
+
347
+ visitDeleteStatement(e: DeleteStatement): ITextQuery {
284
348
  const table = this.visit(e.table);
285
349
  const where = this.visit(e.where);
286
- return prepare `DELETE ${table} WHERE ${where}`;
350
+ return prepare `DELETE FROM ${table} WHERE ${where}`;
287
351
  }
288
352
 
289
- visitJoinExpression(e: JoinExpression): ITextOrFunctionArray {
353
+ visitJoinExpression(e: JoinExpression): ITextQuery {
290
354
  if(!e) {
291
355
  return [];
292
356
  }
293
357
  const table = this.visit(e.source);
294
358
  const where = this.visit(e.where);
295
- const as = e.as ? prepare ` AS ${this.visit(e.as)}` : "";
359
+ const as = e.as ? prepare ` AS ${ e.as.type === "QuotedLiteral"
360
+ ? this.compiler.quotedLiteral(e.as.literal)
361
+ : this.compiler.quotedLiteral((e.as as any).name)}` : "";
296
362
  return prepare ` ${e.joinType || "LEFT"} JOIN ${table}${as} ON ${where}`;
297
363
  }
298
364
 
299
- visitOrderByExpression(e: OrderByExpression): ITextOrFunctionArray {
365
+ visitOrderByExpression(e: OrderByExpression): ITextQuery {
300
366
  if(!e) {
301
367
  return [];
302
368
  }
@@ -306,43 +372,147 @@ export default class ExpressionToSql extends Visitor<ITextOrFunctionArray> {
306
372
  return prepare `${this.visit(e.target)}`;
307
373
  }
308
374
 
309
- visitExistsExpression(e: ExistsExpression): ITextOrFunctionArray {
375
+ visitExistsExpression(e: ExistsExpression): ITextQuery {
310
376
  return prepare `EXISTS (${this.visit(e.target)})`;
311
377
  }
312
378
 
313
- visitPlaceholderExpression(e: PlaceholderExpression): ITextOrFunctionArray {
314
- const p = e.expression();
315
- return prepare `${p}`;
316
- }
379
+ /**
380
+ * This will also create and replace joins if query is provided.
381
+ * @param x MemberExpression
382
+ * @returns Property Chain
383
+ */
384
+ private getPropertyChain(x: Expression): IPropertyChain {
317
385
 
318
- private pushTarget(target: string, type: SourceExpression) {
319
- this.targets.set(target, type);
320
- return () => this.targets.delete(target);
321
- }
386
+ // first replace root parameters
387
+ if (x.type === "MemberExpression") {
388
+ x = this.replaceParameters(x as MemberExpression);
389
+ }
322
390
 
323
- private getPropertyChain(x: Expression) {
324
391
  const chain = [];
325
- let start = x as ExpressionType;
326
- do {
392
+ let me = (x as ExpressionType).type === "MemberExpression"
393
+ ? x as MemberExpression
394
+ : void 0;
395
+ while(me) {
327
396
 
328
- if (start.type !== "MemberExpression") {
329
- return;
330
- }
397
+ const target = me.target as ExpressionType;
331
398
 
332
- const target = start.target as ExpressionType;
333
- const property = start.property as ExpressionType;
399
+ const property = me.property as ExpressionType;
334
400
  if (property.type !== "Identifier") {
335
401
  return;
336
402
  }
337
- chain.push(property.value);
403
+ chain.unshift(property.value);
404
+ if (target === this.root) {
405
+ return { parameter: target, chain };
406
+ }
407
+ if (target.type === "ParameterExpression") {
408
+ return this.flatten({ parameter: target, chain });
409
+ }
338
410
  if (target.type === "Identifier") {
339
- chain.push(target.value);
340
- break;
411
+ return this.flatten({ identifier: target, chain });
412
+ }
413
+ me = target.type === "MemberExpression"
414
+ ? target as MemberExpression
415
+ : void 0;
416
+ };
417
+ }
418
+
419
+ private createParameter (s: SelectStatement, prefix: string) {
420
+ const names = JSON.parse(s.names ?? "[]") as string[];
421
+ let i = 0;
422
+ while(true) {
423
+ const name = `${prefix}${i++}`;
424
+ if (names.includes(name)) {
425
+ continue;
426
+ }
427
+ names.push(name);
428
+ s.names = JSON.stringify(names);
429
+ return ParameterExpression.create({ name });
430
+ }
431
+ };
432
+
433
+ private flatten(pc: IPropertyChain) : IPropertyChain {
434
+
435
+ if (!pc) {
436
+ return pc;
437
+ }
438
+
439
+ // check if we have parameter..
440
+ let { parameter } = pc;
441
+ if (!parameter) {
442
+ return pc;
443
+ }
444
+ if (pc.chain.length <= 1) {
445
+ return pc;
446
+ }
447
+
448
+ const chain = [ ... pc.chain];
449
+
450
+ const scope = this.targets.get(parameter);
451
+ const select = scope?.selectStatement ?? this.source?.selectStatement;
452
+ if (!select) {
453
+ return pc;
454
+ }
455
+ let type = select.model;
456
+
457
+ select.joins ??= [];
458
+
459
+ while(chain.length > 1) {
460
+ const property = chain.shift();
461
+ const propertyInfo = type.getProperty(property);
462
+ if (!propertyInfo.relation || propertyInfo.relation.isCollection) {
463
+ return pc;
464
+ }
465
+
466
+ const relation = propertyInfo.relation;
467
+ // check if relation is optional...
468
+ if (!relation.fkColumn) {
469
+ return pc;
341
470
  }
342
- start = target;
343
- } while (true);
344
- return chain.reverse();
471
+
472
+ const { fkColumn } = relation;
473
+
474
+ const join = select.joins.find((x) => x.model === relation.relatedEntity);
475
+ if (!join) {
476
+ const joinType = fkColumn.nullable ? "LEFT" : "INNER";
477
+ const joinParameter = this.createParameter(select, relation.relatedEntity.name[0]);
478
+ select.joins.push(JoinExpression.create({
479
+ as: joinParameter,
480
+ joinType,
481
+ model: relation.relatedEntity,
482
+ source: Expression.quotedLiteral(relation.relatedEntity.name),
483
+ where: Expression.equal(
484
+ Expression.member(parameter, fkColumn.columnName),
485
+ Expression.member(joinParameter, relation.relatedEntity.keys[0].columnName)
486
+ )
487
+ }));
488
+ parameter = joinParameter;
489
+ type = relation.relatedEntity;
490
+ this.targets.set(parameter, { parameter, model: type, replace: parameter});
491
+ pc.parameter = parameter;
492
+ pc.chain = [ ... chain ];
493
+ } else {
494
+ pc.parameter = parameter = join.as as ParameterExpression;
495
+ type = join.model;
496
+ pc.chain = [... chain];
497
+ }
498
+ }
499
+
500
+ return { parameter, chain };
345
501
  }
346
502
 
503
+ private replaceParameters(x: MemberExpression): Expression {
504
+ const { target, property } = x;
505
+ if (target.type === "ParameterExpression") {
506
+ const replace = this.targets.get(target as ParameterExpression);
507
+ if (replace && replace.replace !== target) {
508
+ return Expression.member(replace.replace, property);
509
+ }
510
+ return x;
511
+ }
512
+ if (target.type === "MemberExpression") {
513
+ return Expression.member(this.replaceParameters(target as MemberExpression), property);
514
+ }
515
+ return x;
516
+ }
347
517
 
348
518
  }