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