@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 EntityAccessError from "../../../common/EntityAccessError.js";
2
+ import Inject from "../../../di/di.js";
3
+ import { IEntityQuery } from "../../../model/IFilterWithParameter.js";
4
+ import EntityEvents from "../../../model/events/EntityEvents.js";
5
+ import { Product, ProductCategory, ProductPrice } from "../../model/ShoppingContext.js";
6
+ import { UserInfo } from "./UserInfo.js";
7
+ const statusPublished = "published";
8
+
9
+ export class ProductEvents extends EntityEvents<Product> {
10
+
11
+ @Inject
12
+ user: UserInfo;
13
+
14
+ filter(query: IEntityQuery<Product>): IEntityQuery<Product> {
15
+ const { userID } = this.user;
16
+
17
+ // admin can access everything so return null
18
+ if (this.user.admin) {
19
+ return null;
20
+ }
21
+ // everyone can read published or own products
22
+ return query.where({ userID, statusPublished }, (p) => (x) => x.ownerID === p.userID || x.status === p.statusPublished);
23
+ }
24
+
25
+ modify(query: IEntityQuery<Product>): IEntityQuery<Product> {
26
+ const { userID } = this.user;
27
+
28
+ // admin can access everything so return null
29
+ if (this.user.admin) {
30
+ return null;
31
+ }
32
+
33
+ // customer can modify its own products
34
+ return query.where({ userID }, (p) => (x) => x.ownerID === p.userID);
35
+ }
36
+
37
+ }
38
+
39
+ export class ProductCategoryEvents extends EntityEvents<ProductCategory> {
40
+
41
+ @Inject
42
+ user: UserInfo;
43
+
44
+ filter(query: IEntityQuery<ProductCategory>): IEntityQuery<ProductCategory> {
45
+ return null;
46
+ }
47
+
48
+ modify(query: IEntityQuery<ProductCategory>): IEntityQuery<ProductCategory> {
49
+ const { userID } = this.user;
50
+
51
+ // admin can access everything so return null
52
+ if (this.user.admin) {
53
+ return null;
54
+ }
55
+
56
+ EntityAccessError.throw("Access denied");
57
+ }
58
+
59
+ }
60
+
61
+
62
+ export class ProductPriceEvents extends EntityEvents<ProductPrice> {
63
+
64
+ @Inject
65
+ user: UserInfo;
66
+
67
+ filter(query: IEntityQuery<ProductPrice>): IEntityQuery<ProductPrice> {
68
+
69
+ const { userID } = this.user;
70
+
71
+ // admin can access everything so return null
72
+ if (this.user.admin) {
73
+ return null;
74
+ }
75
+
76
+ // user can view prices of only published or own products
77
+ return query.where({ userID, statusPublished } , (p) => (x) => x.product.status === p.statusPublished || x.product.ownerID === p.userID);
78
+ }
79
+
80
+ modify(query: IEntityQuery<ProductPrice>): IEntityQuery<ProductPrice> {
81
+ const { userID } = this.user;
82
+
83
+ // admin can access everything so return null
84
+ if (this.user.admin) {
85
+ return null;
86
+ }
87
+
88
+ // user can only edit its own prices
89
+ return query.where({ userID }, (p) => (x) => x.product.ownerID === p.userID);
90
+ }
91
+
92
+ }
@@ -0,0 +1,18 @@
1
+ import Inject from "../../../di/di.js";
2
+ import { IEntityQuery } from "../../../model/IFilterWithParameter.js";
3
+ import EntityEvents from "../../../model/events/EntityEvents.js";
4
+ import { User } from "../../model/ShoppingContext.js";
5
+ import { UserInfo } from "./UserInfo.js";
6
+ export class UserEvents extends EntityEvents<User> {
7
+
8
+ @Inject
9
+ user: UserInfo;
10
+
11
+ filter(query: IEntityQuery<User>): IEntityQuery<User> {
12
+ if (this.user.admin) {
13
+ return null;
14
+ }
15
+ const { userID } = this.user;
16
+ return query.where({ userID }, (p) => (x) => x.userID === p.userID);
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import { RegisterScoped } from "../../../di/di.js";
2
+
3
+ @RegisterScoped
4
+ export class UserInfo {
5
+ public userID: number;
6
+ public admin: boolean;
7
+ }
@@ -0,0 +1,71 @@
1
+ import assert from "assert";
2
+ import Logger from "../../../common/Logger.js";
3
+ import { ServiceProvider } from "../../../di/di.js";
4
+ import { BaseDriver } from "../../../drivers/base/BaseDriver.js";
5
+ import ContextEvents from "../../../model/events/ContextEvents.js";
6
+ import { TestConfig } from "../../TestConfig.js";
7
+ import { ShoppingContext, User } from "../../model/ShoppingContext.js";
8
+ import { createContext } from "../../model/createContext.js";
9
+ import { ShoppingContextEvents } from "../ShoppingContextEvents.js";
10
+ import { UserInfo } from "../events/UserInfo.js";
11
+
12
+ export default async function(this: TestConfig) {
13
+
14
+ const customer = await createUser(this);
15
+
16
+ await addNewOrder.call(this, customer);
17
+
18
+ try {
19
+ await addNewOrder.call(this, customer, 1);
20
+ assert.fail("No error thrown");
21
+ } catch(error) {
22
+
23
+ }
24
+ }
25
+
26
+
27
+ async function addNewOrder(this: TestConfig, customer: User, userID?) {
28
+ const scope = ServiceProvider.global.createScope();
29
+ try {
30
+ const user = new UserInfo();
31
+ user.userID = userID ?? customer.userID;
32
+ scope.add(Logger, Logger.instance);
33
+ scope.add(BaseDriver, this.driver);
34
+ scope.add(UserInfo, user);
35
+ scope.add(ContextEvents, new ShoppingContextEvents());
36
+ const context = scope.create(ShoppingContext);
37
+ context.verifyFilters = true;
38
+
39
+ // get first headphone...
40
+ const headPhone = await context.products.all().firstOrFail();
41
+ const headPhonePrice = await context.productPrices.where({ id: headPhone.productID }, (p) => (x) => x.productID === p.id).firstOrFail();
42
+
43
+ context.orders.add({
44
+ customer,
45
+ orderDate: new Date(),
46
+ orderItems: [
47
+ context.orderItems.add({
48
+ product: headPhone,
49
+ productPrice: headPhonePrice,
50
+ amount: headPhonePrice.amount,
51
+ })
52
+ ]
53
+ });
54
+
55
+ await context.saveChanges();
56
+
57
+ } finally {
58
+ scope.dispose();
59
+ }
60
+ }
61
+
62
+ async function createUser(config: TestConfig) {
63
+ const context = await createContext(config.driver);
64
+ config.driver.connectionString.database = context.driver.connectionString.database;
65
+ const user = context.users.add({
66
+ dateCreated: new Date()
67
+ });
68
+ await context.saveChanges();
69
+ return user;
70
+ }
71
+
package/test.js CHANGED
@@ -40,10 +40,8 @@ export default class TestRunner {
40
40
  options: {
41
41
  encrypt: true, // for azure
42
42
  trustServerCertificate: true // change to true for local dev / self-signed certs
43
- },
44
- debug: true
43
+ }
45
44
  })
46
-
47
45
  ];
48
46
  }
49
47
 
@@ -89,14 +87,17 @@ export default class TestRunner {
89
87
 
90
88
  }
91
89
 
90
+ const testDb = !process.argv.includes("no-db");
92
91
 
93
- await TestRunner.runAll("./dist/tests", true);
92
+ await TestRunner.runAll("./dist/tests", testDb);
94
93
 
95
94
  let exitCode = 0;
95
+ let failed = 0;
96
96
 
97
97
  for (const { error, name } of results) {
98
98
  if (error) {
99
99
  exitCode = 1;
100
+ failed++;
100
101
  console.error(`${name} failed`);
101
102
  console.error(error?.stack ?? error);
102
103
  continue;
@@ -104,4 +105,10 @@ for (const { error, name } of results) {
104
105
  console.log(`${name} executed.`);
105
106
  }
106
107
 
108
+ if (exitCode === 0) {
109
+ console.log(`${results.length} tests ran successfully.`);
110
+ } else {
111
+ console.log(`${failed} Tests out of ${results.length} failed.`);
112
+ }
113
+
107
114
  process.exit(exitCode);
package/tsconfig.json CHANGED
@@ -2,10 +2,12 @@
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
4
  "module":"NodeNext",
5
+ "incremental": true,
5
6
  "sourceMap": true,
6
7
  "declaration": true,
7
8
  "declarationMap": true,
8
9
  "outDir": "dist",
10
+ "skipDefaultLibCheck": true,
9
11
  "experimentalDecorators": true,
10
12
  "emitDecoratorMetadata": true,
11
13
  "lib": [
@@ -1,8 +0,0 @@
1
- export default class NameParser {
2
- public static parseMember(text: ((a: any) => any)) {
3
- const t: string = text.toString();
4
-
5
- const index = t.lastIndexOf(".");
6
- return t.substring(index + 1);
7
- }
8
- }
@@ -1,21 +0,0 @@
1
- export class EntityColumn {
2
- constructor(
3
- public readonly name: string,
4
- public readonly columnName: string,
5
- public readonly storageType: string
6
- ) {
7
-
8
- }
9
- }
10
-
11
- export default class EntitySchema {
12
-
13
- constructor(
14
- public readonly name: string,
15
- public readonly table: string,
16
- public readonly columns: EntityColumn[],
17
- public readonly schema?: string,
18
- ) {
19
- }
20
-
21
- }