@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
@@ -0,0 +1,92 @@
1
+ import { IClassOf } from "../../decorators/IClassOf.js";
2
+ import NameParser from "../../decorators/parser/NameParser.js";
3
+ import Inject from "../../di/di.js";
4
+ import type EntityType from "../../entity-query/EntityType.js";
5
+ import type EntityContext from "../EntityContext.js";
6
+ import { IEntityQuery } from "../IFilterWithParameter.js";
7
+ import ChangeEntry from "../changes/ChangeEntry.js";
8
+
9
+ const done = Promise.resolve() as Promise<void>;
10
+
11
+ export class ForeignKeyFilter<T = any> {
12
+
13
+ public type: EntityType;
14
+ public name: string;
15
+ public fkName: string;
16
+
17
+ private events: EntityEvents<any>;
18
+ private context: EntityContext;
19
+
20
+ constructor(p: Partial<ForeignKeyFilter> & { context: EntityContext, events: EntityEvents<any> }) {
21
+ Object.setPrototypeOf(p, ForeignKeyFilter.prototype);
22
+ return p as any as ForeignKeyFilter;
23
+ }
24
+
25
+ public is<TR>(fx: (x: T) => TR): boolean {
26
+ const name = NameParser.parseMember(fx);
27
+ return name === this.fkName || name === this.name;
28
+ }
29
+
30
+ public read() {
31
+ const read = this.context.query(this.type.typeClass);
32
+ return this.events.filter(read);
33
+ }
34
+
35
+ public modify() {
36
+ const read = this.context.query(this.type.typeClass);
37
+ return this.events.modify(read);
38
+ }
39
+ }
40
+
41
+
42
+ export default abstract class EntityEvents<T> {
43
+
44
+ filter(query: IEntityQuery<T>) {
45
+ return query;
46
+ }
47
+
48
+ includeFilter(query: IEntityQuery<T>, type?: any, key?: string) {
49
+ return this.filter(query);
50
+ }
51
+
52
+ modify(query: IEntityQuery<T>) {
53
+ return this.filter(query);
54
+ }
55
+
56
+ delete(query: IEntityQuery<T>) {
57
+ return this.modify(query);
58
+ }
59
+
60
+ beforeInsert(entity: T, entry: ChangeEntry) {
61
+ return done;
62
+ }
63
+
64
+ onForeignKeyFilter(filter: ForeignKeyFilter<T>) {
65
+ return filter.modify();
66
+ }
67
+
68
+ afterInsert(entity: T, entry: ChangeEntry) {
69
+ return done;
70
+ }
71
+
72
+ beforeUpdate(entity: T, entry: ChangeEntry) {
73
+ return done;
74
+ }
75
+
76
+ afterUpdate(entity: T, entry: ChangeEntry) {
77
+ return done;
78
+ }
79
+
80
+ beforeDelete(entity: T, entry: ChangeEntry) {
81
+ return done;
82
+ }
83
+
84
+ afterDelete(entity: T, entry: ChangeEntry) {
85
+ return done;
86
+ }
87
+
88
+ beforeJson(entity: T) {
89
+ return entity;
90
+ }
91
+
92
+ }
@@ -1,4 +1,4 @@
1
- import SchemaRegistry from "../decorators/SchemaRegistry.js";
1
+ import SchemaRegistry from "../../decorators/SchemaRegistry.js";
2
2
 
3
3
  export default class IdentityService {
4
4
 
@@ -0,0 +1 @@
1
+ export const existsQuery = Symbol("existsQuery");
@@ -0,0 +1,173 @@
1
+ import EntityAccessError from "../../common/EntityAccessError.js";
2
+ import Logger from "../../common/Logger.js";
3
+ import { TypeInfo } from "../../common/TypeInfo.js";
4
+ import { IEntityRelation } from "../../decorators/IColumn.js";
5
+ import { ServiceProvider } from "../../di/di.js";
6
+ import EntityType from "../../entity-query/EntityType.js";
7
+ import { ConditionalExpression, Constant, ExistsExpression, Expression, Identifier, ParameterExpression, QuotedLiteral, SelectStatement, TemplateLiteral, ValuesStatement } from "../../query/ast/Expressions.js";
8
+ import EntityContext from "../EntityContext.js";
9
+ import EntityQuery from "../EntityQuery.js";
10
+ import ChangeEntry from "../changes/ChangeEntry.js";
11
+ import EntityEvents, { ForeignKeyFilter } from "../events/EntityEvents.js";
12
+
13
+ type KeyValueArray = [string, any][];
14
+
15
+ export default class VerificationSession {
16
+
17
+ private select: SelectStatement;
18
+
19
+ private field: ConditionalExpression[];
20
+
21
+ constructor(private context: EntityContext) {
22
+
23
+ this.select = SelectStatement.create({});
24
+ }
25
+
26
+ queueVerification(change: ChangeEntry, events: EntityEvents<any>) {
27
+ const { type, entity } = change;
28
+ if (change.status !== "inserted") {
29
+ // verify access to the entity
30
+ const keys = [] as KeyValueArray;
31
+ for (const iterator of type.keys) {
32
+ const key = entity[iterator.name];
33
+ if (key === void 0) {
34
+ break;
35
+ }
36
+ keys.push([iterator.columnName, key]);
37
+ }
38
+ if (keys.length === type.keys.length) {
39
+ this.queueEntityKey(change, keys, events);
40
+ }
41
+ }
42
+
43
+ if (change.status === "deleted") {
44
+ return;
45
+ }
46
+
47
+ // for modified or inserted
48
+ // we need to verify access to each foreign key
49
+
50
+ for (const relation of type.relations) {
51
+ if (relation.isCollection) {
52
+ continue;
53
+ }
54
+
55
+ const fk = relation.fkColumn;
56
+ if (!fk) {
57
+ continue;
58
+ }
59
+
60
+ const fkValue = entity[fk.name];
61
+ if (fkValue === void 0) {
62
+ // not set... ignore..
63
+ continue;
64
+ }
65
+ this.queueEntityForeignKey(change, relation, fkValue);
66
+ }
67
+ }
68
+ queueEntityForeignKey(change: ChangeEntry, relation: IEntityRelation, value) {
69
+ const relatedModel = relation.relatedEntity;
70
+ const type = relation.relatedEntity.typeClass;
71
+ const events = this.context.eventsFor(change.type.typeClass);
72
+ const relatedEvents = this.context.eventsFor(relation.relatedEntity.typeClass);
73
+ const context = this.context;
74
+ const fk = new ForeignKeyFilter({
75
+ context,
76
+ events: relatedEvents,
77
+ type: relatedModel,
78
+ name: relation.name,
79
+ fkName: relation.fkColumn.name
80
+ });
81
+ let query = events.onForeignKeyFilter(fk);
82
+ if (query === void 0) {
83
+ query = fk.modify();
84
+ }
85
+ if (query === null) {
86
+ return;
87
+ }
88
+
89
+ const eq = query as EntityQuery;
90
+ const compare = Expression.equal(
91
+ Expression.member(eq.selectStatement.as, relatedModel.keys[0].columnName),
92
+ Expression.constant(value)
93
+ );
94
+ const typeName = TypeInfo.nameOfType(type);
95
+ this.addError(query as EntityQuery, compare , `Unable to access entity ${typeName} through foreign key ${TypeInfo.nameOfType(change.type)}.${relation.name}.\n`);
96
+ }
97
+
98
+ queueEntityKey(change: ChangeEntry, keys: KeyValueArray, events: EntityEvents<any>) {
99
+ const type = change.type.typeClass;
100
+ let query = this.context.query(type);
101
+ query = change.status === "modified" ? events.modify(query) : events.delete(query);
102
+ if (!query) {
103
+ return;
104
+ }
105
+ let compare: Expression;
106
+ const eq = query as EntityQuery;
107
+ for (const [key, value] of keys) {
108
+ const test = Expression.equal(
109
+ Expression.member(eq.selectStatement.as, Expression.quotedLiteral(key)),
110
+ Expression.constant(value)
111
+ );
112
+ compare = compare
113
+ ? Expression.logicalAnd(compare, test)
114
+ : test;
115
+ }
116
+ const typeName = TypeInfo.nameOfType(type);
117
+ this.addError(query as EntityQuery, compare, `Unable to access entity ${typeName}.\n`);
118
+ }
119
+
120
+ async verifyAsync(): Promise<any> {
121
+ if (!this.field?.length) {
122
+ return;
123
+ }
124
+ this.select.fields =[
125
+ Expression.as(Expression.templateLiteral(this.field), "error")
126
+ ];
127
+ this.select.as = ParameterExpression.create({ name: "x"});
128
+ const source = ValuesStatement.create({
129
+ values: [
130
+ [Identifier.create({ value: "1"})]
131
+ ],
132
+ as: QuotedLiteral.create({ literal: "a"}),
133
+ fields: [QuotedLiteral.create({ literal: "a"})]
134
+ });
135
+ this.select.source = source;
136
+ const compiler = this.context.driver.compiler;
137
+ const query = compiler.compileExpression(null, this.select);
138
+ const logger = ServiceProvider.resolve(this.context, Logger);
139
+ const session = logger.newSession();
140
+ try {
141
+ const { rows: [ { error }]} = await this.context.driver.executeQuery(query);
142
+ if (error) {
143
+ session.error(`Failed executing ${query.text}\n[${query.values.join(",")}]\n${error?.stack ?? error}`);
144
+ EntityAccessError.throw(error);
145
+ }
146
+ } finally {
147
+ session.dispose();
148
+ }
149
+ }
150
+
151
+ addError(query: EntityQuery, compare: Expression, error: string) {
152
+ const select = { ... query.selectStatement};
153
+ select.fields = [
154
+ Expression.identifier("1")
155
+ ];
156
+
157
+ const where = select.where
158
+ ? Expression.logicalAnd(select.where, compare)
159
+ : compare;
160
+
161
+ select.where = where;
162
+
163
+ const text = ConditionalExpression.create({
164
+ test: ExistsExpression.create({
165
+ target: select
166
+ }),
167
+ consequent: Expression.constant(""),
168
+ alternate: Expression.constant(error),
169
+ });
170
+
171
+ (this.field ??=[]).push(text);
172
+ }
173
+ }
@@ -0,0 +1,128 @@
1
+ import { ArrowFunctionExpression, BigIntLiteral, BinaryExpression, BooleanLiteral, CallExpression, CoalesceExpression, ConditionalExpression, Constant, DeleteStatement, Expression, ExpressionAs, Identifier, MemberExpression, NewObjectExpression, NullExpression, NumberLiteral, ParameterExpression, QuotedLiteral, StringLiteral, TableLiteral, TemplateElement, TemplateLiteral } from "./Expressions.js";
2
+ import Visitor from "./Visitor.js";
3
+
4
+ const isBinary = (type) => /^(BinaryExpression|CoalesceExpression)$/.test(type);
5
+
6
+ export default class DebugStringVisitor extends Visitor<string> {
7
+
8
+ static expressionToString(e: Expression) {
9
+ const dsv = new DebugStringVisitor();
10
+ return dsv.visit(e);
11
+ }
12
+
13
+ visitArrowFunctionExpression(e: ArrowFunctionExpression) {
14
+ return `(${this.visitArray(e.params)}) => ${this.visit(e.body)}`;
15
+ }
16
+
17
+ visitBigIntLiteral(e: BigIntLiteral): string {
18
+ return e.value.toString() + "n";
19
+ }
20
+
21
+ visitBinaryExpression(e: BinaryExpression): string {
22
+ const left = isBinary(e.left.type)
23
+ ? `(${this.visit(e.left)})`
24
+ : this.visit(e.left);
25
+ const right = isBinary(e.right.type)
26
+ ? `(${this.visit(e.right)})`
27
+ : this.visit(e.right);
28
+ return `${left} ${e.operator} ${right}`;
29
+ }
30
+
31
+ visitBooleanLiteral(e: BooleanLiteral): string {
32
+ return String(e.value);
33
+ }
34
+
35
+ visitCallExpression(e: CallExpression): string {
36
+ return `${this.visit(e.callee)}(${this.visitArray(e.arguments)})`;
37
+ }
38
+
39
+ visitCoalesceExpression(e: CoalesceExpression): string {
40
+ const left = isBinary(e.left.type)
41
+ ? `(${this.visit(e.left)})`
42
+ : this.visit(e.left);
43
+ const right = isBinary(e.right.type)
44
+ ? `(${this.visit(e.right)})`
45
+ : this.visit(e.right);
46
+ return `${left} ?? ${right}`;
47
+ }
48
+
49
+ visitConditionalExpression(e: ConditionalExpression): string {
50
+ return `${e.test} ? ${e.consequent} : ${e.alternate}`;
51
+ }
52
+
53
+ visitConstant(e: Constant): string {
54
+ return `"Constant:${e.value}"`;
55
+ }
56
+
57
+ visitExpressionAs(e: ExpressionAs): string {
58
+ return `${e.expression} as ${e.alias}`;
59
+ }
60
+
61
+ visitIdentifier(e: Identifier): string {
62
+ return e.value;
63
+ }
64
+
65
+ visitMemberExpression(e: MemberExpression): string {
66
+ return `${this.visit(e.target)}.${this.visit(e.property)}`;
67
+ }
68
+
69
+ visitTableLiteral(e: TableLiteral): string {
70
+ if (!e.schema) {
71
+ return this.visit(e.name);
72
+ }
73
+ return `${this.visit(e.schema)}.${this.visit(e.name)}`;
74
+ }
75
+
76
+ visitNewObjectExpression(e: NewObjectExpression): string {
77
+ return `({${this.visitArray(e.properties)}})`;
78
+ }
79
+
80
+ visitNullExpression(e: NullExpression): string {
81
+ return "null";
82
+ }
83
+
84
+ visitNumberLiteral(e: NumberLiteral): string {
85
+ return e.value.toString();
86
+ }
87
+
88
+ visitParameterExpression(e: ParameterExpression): string {
89
+ return e.name;
90
+ }
91
+
92
+ visitQuotedLiteral(e: QuotedLiteral): string {
93
+ return `"${e.literal}"`;
94
+ }
95
+
96
+ visitStringLiteral(e: StringLiteral): string {
97
+ return `"${e.value}"`;
98
+ }
99
+
100
+ visitTemplateElement(e: TemplateElement): string {
101
+ return `${e.value.cooked}`;
102
+ }
103
+
104
+ visitTemplateLiteral(e: TemplateLiteral): string {
105
+ const items = [];
106
+ if (e.quasis?.length) {
107
+ for (let i = 0; i<e.quasis.length; i++) {
108
+ items.push(this.visit(e.quasis[i]));
109
+ if (i<e.value.length) {
110
+ items.push("${" + this.visit(e.value[i]) + "}" );
111
+ }
112
+ }
113
+ } else {
114
+ for (const iterator of e.value) {
115
+ if (iterator.type === "StringLiteral") {
116
+ items.push((iterator as StringLiteral).value as string);
117
+ continue;
118
+ }
119
+ items.push("${" + this.visit(iterator) + "}" );
120
+ }
121
+ }
122
+ return "`" + items.join("") + "`";
123
+ }
124
+
125
+ private visitArray(e: Expression[], separator = ", ") {
126
+ return e.map((x) => this.visit(x)).join(separator);
127
+ }
128
+ }