@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.
- package/.github/workflows/node.yml +7 -2
- package/.vscode/settings.json +1 -0
- package/README.md +57 -27
- package/package.json +2 -2
- package/src/common/EntityAccessError.ts +10 -0
- package/src/common/IDisposable.ts +25 -0
- package/src/common/ImmutableObject.ts +53 -0
- package/src/common/Logger.ts +59 -0
- package/src/common/TypeInfo.ts +3 -0
- package/src/common/cache/TimedCache.ts +2 -2
- package/src/common/usingAsync.ts +42 -12
- package/src/compiler/QueryCompiler.ts +28 -30
- package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +23 -0
- package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +23 -0
- package/src/decorators/ForeignKey.ts +1 -1
- package/src/decorators/IClassOf.ts +2 -1
- package/src/decorators/IColumn.ts +2 -0
- package/src/decorators/parser/NameParser.ts +15 -0
- package/src/di/di.ts +224 -0
- package/src/drivers/base/BaseDriver.ts +28 -3
- package/src/drivers/sql-server/ExpressionToSqlServer.ts +34 -9
- package/src/drivers/sql-server/SqlServerDriver.ts +7 -16
- package/src/entity-query/EntityType.ts +45 -3
- package/src/model/EntityContext.ts +167 -22
- package/src/model/EntityQuery.ts +118 -59
- package/src/model/EntitySource.ts +38 -46
- package/src/model/IFilterWithParameter.ts +5 -0
- package/src/model/SourceExpression.ts +21 -25
- package/src/model/{ChangeEntry.ts → changes/ChangeEntry.ts} +35 -5
- package/src/model/{ChangeSet.ts → changes/ChangeSet.ts} +16 -11
- package/src/model/events/ContextEvents.ts +26 -0
- package/src/model/events/EntityEvents.ts +96 -0
- package/src/model/{IdentityService.ts → identity/IdentityService.ts} +9 -1
- package/src/model/identity/RelationMapper.ts +71 -0
- package/src/model/symbols.ts +1 -0
- package/src/model/verification/VerificationSession.ts +173 -0
- package/src/query/ast/DebugStringVisitor.ts +175 -0
- package/src/query/ast/ExpressionToSql.ts +277 -119
- package/src/query/ast/Expressions.ts +130 -13
- package/src/query/ast/IStringTransformer.ts +19 -5
- package/src/query/ast/ParameterScope.ts +97 -0
- package/src/query/ast/ReplaceParameter.ts +40 -0
- package/src/query/ast/Types.ts +0 -0
- package/src/query/ast/Visitor.ts +26 -5
- package/src/query/expander/QueryExpander.ts +147 -0
- package/src/query/parser/ArrowToExpression.ts +134 -19
- package/src/query/parser/BabelVisitor.ts +31 -43
- package/src/query/parser/NotSupportedError.ts +5 -0
- package/src/query/parser/Restructure.ts +66 -0
- package/src/query/parser/TransformVisitor.ts +83 -0
- package/src/sql/ISql.ts +10 -0
- package/src/tests/db-tests/tests/select-items.ts +12 -0
- package/src/tests/expressions/left-joins/child-joins.ts +54 -34
- package/src/tests/expressions/sanitize/sanitize-test.ts +17 -0
- package/src/tests/expressions/select/select.ts +24 -0
- package/src/tests/expressions/simple/parse-arrow.ts +10 -0
- package/src/tests/model/ShoppingContext.ts +7 -3
- package/src/tests/model/createContext.ts +68 -17
- package/src/tests/security/ShoppingContextEvents.ts +20 -0
- package/src/tests/security/events/OrderEvents.ts +72 -0
- package/src/tests/security/events/ProductEvents.ts +92 -0
- package/src/tests/security/events/UserEvents.ts +28 -0
- package/src/tests/security/events/UserInfo.ts +7 -0
- package/src/tests/security/tests/include-items.ts +19 -0
- package/src/tests/security/tests/place-order.ts +104 -0
- package/test.js +11 -4
- package/tsconfig.json +2 -0
- package/src/decorators/parser/MemberParser.ts +0 -8
- package/src/model/EntitySchema.ts +0 -21
|
@@ -43,4 +43,14 @@ export default function () {
|
|
|
43
43
|
assert.equal(`starts_with("x"."firstName", $1)`, r.text);
|
|
44
44
|
assert.equal("Akash", r.values[0]);
|
|
45
45
|
|
|
46
|
+
const code = "1235";
|
|
47
|
+
const key = 13434;
|
|
48
|
+
r = compiler.execute({name, code, key},
|
|
49
|
+
(p) => (x: KeyCode) =>
|
|
50
|
+
x.code === Sql.cast.asNumber(p.code) && x.key === Sql.cast.asText(p.key) );
|
|
51
|
+
|
|
52
|
+
assert.equal(`("x"."code" = ($1 ::double)) AND ("x"."key" = ($2 ::text))`, r.text);
|
|
53
|
+
|
|
46
54
|
}
|
|
55
|
+
|
|
56
|
+
type KeyCode = { name: string, code: number, key: string };
|
|
@@ -2,9 +2,8 @@ import EntityContext from "../../model/EntityContext.js";
|
|
|
2
2
|
import Column from "../../decorators/Column.js";
|
|
3
3
|
import ForeignKey from "../../decorators/ForeignKey.js";
|
|
4
4
|
import Table from "../../decorators/Table.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { Console } from "console";
|
|
5
|
+
|
|
6
|
+
export const statusPublished = "published";
|
|
8
7
|
|
|
9
8
|
export class ShoppingContext extends EntityContext {
|
|
10
9
|
|
|
@@ -65,6 +64,9 @@ export class Product {
|
|
|
65
64
|
@Column({ nullable: true })
|
|
66
65
|
public ownerID: number;
|
|
67
66
|
|
|
67
|
+
@Column({ dataType: "Char", length: 20})
|
|
68
|
+
public status: string;
|
|
69
|
+
|
|
68
70
|
public orderItems: OrderItem[];
|
|
69
71
|
|
|
70
72
|
public prices: ProductPrice[];
|
|
@@ -123,8 +125,10 @@ export class ProductPrice {
|
|
|
123
125
|
@Column({ nullable: true})
|
|
124
126
|
public endDate?: Date;
|
|
125
127
|
|
|
128
|
+
@Column({ })
|
|
126
129
|
public amount: number;
|
|
127
130
|
|
|
131
|
+
@Column({})
|
|
128
132
|
public productID: number;
|
|
129
133
|
|
|
130
134
|
@ForeignKey({
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { IClassOf } from "../../decorators/IClassOf.js";
|
|
2
2
|
import { BaseDriver } from "../../drivers/base/BaseDriver.js";
|
|
3
|
-
import { ShoppingContext } from "./ShoppingContext.js";
|
|
3
|
+
import { ShoppingContext, User, statusPublished } from "./ShoppingContext.js";
|
|
4
|
+
|
|
5
|
+
const status = statusPublished;
|
|
4
6
|
|
|
5
7
|
export async function createContext(driver: BaseDriver) {
|
|
6
8
|
|
|
7
|
-
const rn = "d" + Date.now();
|
|
8
9
|
const copy = { ... driver } as BaseDriver;
|
|
9
10
|
(copy as any).connectionString = { ... driver.connectionString };
|
|
10
|
-
copy.connectionString.database = rn;
|
|
11
11
|
Object.setPrototypeOf(copy, Object.getPrototypeOf(driver));
|
|
12
12
|
const context = new ShoppingContext(copy);
|
|
13
13
|
|
|
@@ -28,16 +28,42 @@ export async function createContext(driver: BaseDriver) {
|
|
|
28
28
|
async function seed(context: ShoppingContext) {
|
|
29
29
|
const now = new Date();
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// add admin user...
|
|
32
|
+
context.users.add({ dateCreated: new Date()});
|
|
33
|
+
// add seller
|
|
34
|
+
const seller = context.users.add({ dateCreated: new Date()});
|
|
35
|
+
|
|
36
|
+
addHeadPhones(context, now, seller);
|
|
37
|
+
|
|
38
|
+
const clothes = addMaleClothes(context, seller);
|
|
39
|
+
|
|
40
|
+
addFemaleClothes(context, seller);
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
await context.saveChanges();
|
|
34
43
|
|
|
35
|
-
const
|
|
44
|
+
const product = clothes[0];
|
|
45
|
+
const productPrice = product.prices[0];
|
|
46
|
+
|
|
47
|
+
const user = context.users.add({
|
|
48
|
+
dateCreated: new Date(),
|
|
49
|
+
orders: [
|
|
50
|
+
context.orders.add({
|
|
51
|
+
orderDate: new Date(),
|
|
52
|
+
orderItems:[
|
|
53
|
+
context.orderItems.add({
|
|
54
|
+
product,
|
|
55
|
+
productPrice,
|
|
56
|
+
amount: productPrice.amount
|
|
57
|
+
})
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
});
|
|
36
62
|
|
|
37
63
|
await context.saveChanges();
|
|
38
64
|
}
|
|
39
65
|
|
|
40
|
-
function addFemaleClothes(context: ShoppingContext) {
|
|
66
|
+
function addFemaleClothes(context: ShoppingContext, owner: User) {
|
|
41
67
|
const category = context.categories.add({
|
|
42
68
|
name: "Female Clothes",
|
|
43
69
|
categoryID: "clothes/female"
|
|
@@ -48,6 +74,8 @@ function addFemaleClothes(context: ShoppingContext) {
|
|
|
48
74
|
|
|
49
75
|
context.products.add({
|
|
50
76
|
name: "White T-Shirt",
|
|
77
|
+
owner,
|
|
78
|
+
status,
|
|
51
79
|
categories: [
|
|
52
80
|
context.productCategories.add({
|
|
53
81
|
category
|
|
@@ -64,6 +92,8 @@ function addFemaleClothes(context: ShoppingContext) {
|
|
|
64
92
|
|
|
65
93
|
context.products.add({
|
|
66
94
|
name: "Red T-Shirt",
|
|
95
|
+
status,
|
|
96
|
+
owner,
|
|
67
97
|
categories: [
|
|
68
98
|
context.productCategories.add({
|
|
69
99
|
category
|
|
@@ -80,6 +110,8 @@ function addFemaleClothes(context: ShoppingContext) {
|
|
|
80
110
|
|
|
81
111
|
context.products.add({
|
|
82
112
|
name: "Blue T-Shirt",
|
|
113
|
+
status,
|
|
114
|
+
owner,
|
|
83
115
|
categories: [
|
|
84
116
|
context.productCategories.add({
|
|
85
117
|
category
|
|
@@ -96,6 +128,8 @@ function addFemaleClothes(context: ShoppingContext) {
|
|
|
96
128
|
|
|
97
129
|
context.products.add({
|
|
98
130
|
name: "Pink T-Shirt",
|
|
131
|
+
status,
|
|
132
|
+
owner,
|
|
99
133
|
categories: [
|
|
100
134
|
context.productCategories.add({
|
|
101
135
|
category
|
|
@@ -111,7 +145,7 @@ function addFemaleClothes(context: ShoppingContext) {
|
|
|
111
145
|
});
|
|
112
146
|
}
|
|
113
147
|
|
|
114
|
-
function addMaleClothes(context: ShoppingContext) {
|
|
148
|
+
function addMaleClothes(context: ShoppingContext, owner: User) {
|
|
115
149
|
const category = context.categories.add({
|
|
116
150
|
name: "Male Clothes",
|
|
117
151
|
categoryID: "clothes/male"
|
|
@@ -120,8 +154,10 @@ function addMaleClothes(context: ShoppingContext) {
|
|
|
120
154
|
const startDate = new Date();
|
|
121
155
|
const active = true;
|
|
122
156
|
|
|
123
|
-
context.products.add({
|
|
157
|
+
return [context.products.add({
|
|
124
158
|
name: "White T-Shirt",
|
|
159
|
+
status,
|
|
160
|
+
owner,
|
|
125
161
|
categories: [
|
|
126
162
|
context.productCategories.add({
|
|
127
163
|
category
|
|
@@ -134,10 +170,11 @@ function addMaleClothes(context: ShoppingContext) {
|
|
|
134
170
|
startDate
|
|
135
171
|
})
|
|
136
172
|
]
|
|
137
|
-
})
|
|
138
|
-
|
|
173
|
+
}),
|
|
139
174
|
context.products.add({
|
|
140
175
|
name: "Red T-Shirt",
|
|
176
|
+
status,
|
|
177
|
+
owner,
|
|
141
178
|
categories: [
|
|
142
179
|
context.productCategories.add({
|
|
143
180
|
category
|
|
@@ -150,10 +187,11 @@ function addMaleClothes(context: ShoppingContext) {
|
|
|
150
187
|
startDate
|
|
151
188
|
})
|
|
152
189
|
]
|
|
153
|
-
})
|
|
154
|
-
|
|
190
|
+
}),
|
|
155
191
|
context.products.add({
|
|
156
192
|
name: "Blue T-Shirt",
|
|
193
|
+
status,
|
|
194
|
+
owner,
|
|
157
195
|
categories: [
|
|
158
196
|
context.productCategories.add({
|
|
159
197
|
category
|
|
@@ -166,10 +204,11 @@ function addMaleClothes(context: ShoppingContext) {
|
|
|
166
204
|
startDate
|
|
167
205
|
})
|
|
168
206
|
]
|
|
169
|
-
})
|
|
170
|
-
|
|
207
|
+
}),
|
|
171
208
|
context.products.add({
|
|
172
209
|
name: "Pink T-Shirt",
|
|
210
|
+
status,
|
|
211
|
+
owner,
|
|
173
212
|
categories: [
|
|
174
213
|
context.productCategories.add({
|
|
175
214
|
category
|
|
@@ -182,11 +221,11 @@ function addMaleClothes(context: ShoppingContext) {
|
|
|
182
221
|
startDate
|
|
183
222
|
})
|
|
184
223
|
]
|
|
185
|
-
});
|
|
224
|
+
})];
|
|
186
225
|
}
|
|
187
226
|
|
|
188
227
|
export const headPhoneCategory = "head-phones";
|
|
189
|
-
function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
228
|
+
function addHeadPhones(context: ShoppingContext, now: Date, owner: User) {
|
|
190
229
|
const category = context.categories.add({
|
|
191
230
|
name: "Headphones",
|
|
192
231
|
categoryID: headPhoneCategory
|
|
@@ -197,6 +236,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
197
236
|
|
|
198
237
|
context.products.add({
|
|
199
238
|
name: "Jabber Head Phones",
|
|
239
|
+
owner,
|
|
240
|
+
status,
|
|
200
241
|
categories: [
|
|
201
242
|
context.productCategories.add({
|
|
202
243
|
category
|
|
@@ -213,6 +254,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
213
254
|
|
|
214
255
|
context.products.add({
|
|
215
256
|
name: "Sony Head Phones",
|
|
257
|
+
owner,
|
|
258
|
+
status,
|
|
216
259
|
categories: [
|
|
217
260
|
context.productCategories.add({
|
|
218
261
|
category
|
|
@@ -229,6 +272,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
229
272
|
|
|
230
273
|
context.products.add({
|
|
231
274
|
name: "Sony Head Phones Black",
|
|
275
|
+
status,
|
|
276
|
+
owner,
|
|
232
277
|
categories: [
|
|
233
278
|
context.productCategories.add({
|
|
234
279
|
category
|
|
@@ -245,6 +290,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
245
290
|
|
|
246
291
|
context.products.add({
|
|
247
292
|
name: "Sony Head Phones Blue",
|
|
293
|
+
owner,
|
|
294
|
+
status,
|
|
248
295
|
categories: [
|
|
249
296
|
context.productCategories.add({
|
|
250
297
|
category
|
|
@@ -261,6 +308,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
261
308
|
|
|
262
309
|
context.products.add({
|
|
263
310
|
name: "Jabber Head Phones Black",
|
|
311
|
+
status,
|
|
312
|
+
owner,
|
|
264
313
|
categories: [
|
|
265
314
|
context.productCategories.add({
|
|
266
315
|
category
|
|
@@ -277,6 +326,8 @@ function addHeadPhones(context: ShoppingContext, now: Date) {
|
|
|
277
326
|
|
|
278
327
|
context.products.add({
|
|
279
328
|
name: "Jabber Head Phones Blue",
|
|
329
|
+
owner,
|
|
330
|
+
status,
|
|
280
331
|
categories: [
|
|
281
332
|
context.productCategories.add({
|
|
282
333
|
category
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ContextEvents from "../../model/events/ContextEvents.js";
|
|
2
|
+
import { Order, OrderItem, Product, ProductCategory, ProductPrice, User } from "../model/ShoppingContext.js";
|
|
3
|
+
import { OrderEvents, OrderItemEvents } from "./events/OrderEvents.js";
|
|
4
|
+
import { ProductEvents, ProductCategoryEvents, ProductPriceEvents } from "./events/ProductEvents.js";
|
|
5
|
+
import { UserEvents } from "./events/UserEvents.js";
|
|
6
|
+
|
|
7
|
+
export class ShoppingContextEvents extends ContextEvents {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
|
|
12
|
+
this.register(User, UserEvents);
|
|
13
|
+
this.register(Product, ProductEvents);
|
|
14
|
+
this.register(ProductCategory, ProductCategoryEvents);
|
|
15
|
+
this.register(ProductPrice, ProductPriceEvents);
|
|
16
|
+
this.register(Order, OrderEvents);
|
|
17
|
+
this.register(OrderItem, OrderItemEvents);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Inject from "../../../di/di.js";
|
|
2
|
+
import { IEntityQuery } from "../../../model/IFilterWithParameter.js";
|
|
3
|
+
import EntityEvents, { ForeignKeyFilter } from "../../../model/events/EntityEvents.js";
|
|
4
|
+
import { Order, OrderItem, User } from "../../model/ShoppingContext.js";
|
|
5
|
+
import { UserInfo } from "./UserInfo.js";
|
|
6
|
+
|
|
7
|
+
export class OrderEvents extends EntityEvents<Order> {
|
|
8
|
+
|
|
9
|
+
@Inject
|
|
10
|
+
user: UserInfo;
|
|
11
|
+
|
|
12
|
+
filter(query: IEntityQuery<Order>): IEntityQuery<Order> {
|
|
13
|
+
if (this.user.admin) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const { userID } = this.user;
|
|
17
|
+
|
|
18
|
+
// user can access orders placed by the user or orders with products owned by user
|
|
19
|
+
|
|
20
|
+
return query.where({ userID }, (p) => (x) => x.customerID === p.userID || x.orderItems.some((item) => item.product.ownerID === p.userID));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
modify(query: IEntityQuery<Order>): IEntityQuery<Order> {
|
|
24
|
+
if (this.user.admin) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const { userID } = this.user;
|
|
28
|
+
// user can only modify placed orders
|
|
29
|
+
return query.where({ userID }, (p) => (x) => x.customerID === p.userID || x.orderItems.some((item) => item.product.ownerID === p.userID));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onForeignKeyFilter(filter: ForeignKeyFilter<Order>): IEntityQuery<any> {
|
|
33
|
+
if (filter.is((x) => x.customer)) {
|
|
34
|
+
return filter.read();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class OrderItemEvents extends EntityEvents<OrderItem> {
|
|
40
|
+
|
|
41
|
+
@Inject
|
|
42
|
+
user: UserInfo;
|
|
43
|
+
|
|
44
|
+
filter(query: IEntityQuery<OrderItem>): IEntityQuery<OrderItem> {
|
|
45
|
+
if (this.user.admin) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const { userID } = this.user;
|
|
49
|
+
|
|
50
|
+
// user can access orders placed by the user or orders with products owned by user
|
|
51
|
+
|
|
52
|
+
return query.where({ userID }, (p) => (x) => x.order.customerID === p.userID || x.product.ownerID === p.userID);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
modify(query: IEntityQuery<OrderItem>): IEntityQuery<OrderItem> {
|
|
56
|
+
if (this.user.admin) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const { userID } = this.user;
|
|
60
|
+
// user can only modify placed orders
|
|
61
|
+
return query.where({ userID }, (p) => (x) => x.order.customerID === p.userID || x.product.ownerID === p.userID);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onForeignKeyFilter(filter: ForeignKeyFilter<OrderItem>): IEntityQuery<any> {
|
|
65
|
+
if (filter.is((x) => x.product)) {
|
|
66
|
+
return filter.read();
|
|
67
|
+
}
|
|
68
|
+
if (filter.is((x) => x.productPrice)) {
|
|
69
|
+
return filter.read();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -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,28 @@
|
|
|
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
|
+
|| x.orders.some(
|
|
18
|
+
(op) => op.orderItems.some((oi) => oi.product.ownerID === p.userID)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
modify(query: IEntityQuery<User>): IEntityQuery<User> {
|
|
22
|
+
if (this.user.admin) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const { userID } = this.user;
|
|
26
|
+
return query.where({ userID}, (p) => (x) => x.userID === p.userID);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { TestConfig } from "../../TestConfig.js";
|
|
3
|
+
import { createContext } from "../../model/createContext.js";
|
|
4
|
+
import { ShoppingContext } from "../../model/ShoppingContext.js";
|
|
5
|
+
import Logger from "../../../common/Logger.js";
|
|
6
|
+
|
|
7
|
+
export default async function (this: TestConfig) {
|
|
8
|
+
|
|
9
|
+
const old = await createContext(this.driver);
|
|
10
|
+
|
|
11
|
+
const context = new ShoppingContext(old.driver, void 0, Logger.instance);
|
|
12
|
+
const order = await context.orders.all()
|
|
13
|
+
.where({id: 0}, (p) => (x) => x.orderID > p.id)
|
|
14
|
+
.include((x) => x.orderItems.forEach((oi) => oi.productPrice.product))
|
|
15
|
+
.first();
|
|
16
|
+
|
|
17
|
+
assert.notEqual(null, order);
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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
|
+
await getNewOrders.call(this);
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getNewOrders(this: TestConfig) {
|
|
30
|
+
const scope = ServiceProvider.global.createScope();
|
|
31
|
+
try {
|
|
32
|
+
const user = new UserInfo();
|
|
33
|
+
user.userID = 2;
|
|
34
|
+
scope.add(Logger, Logger.instance);
|
|
35
|
+
scope.add(BaseDriver, this.driver);
|
|
36
|
+
scope.add(UserInfo, user);
|
|
37
|
+
scope.add(ContextEvents, new ShoppingContextEvents());
|
|
38
|
+
const context = scope.create(ShoppingContext);
|
|
39
|
+
context.verifyFilters = true;
|
|
40
|
+
|
|
41
|
+
const order = await context.orders.all().first();
|
|
42
|
+
|
|
43
|
+
order.orderDate = new Date();
|
|
44
|
+
await context.saveChanges();
|
|
45
|
+
|
|
46
|
+
} finally {
|
|
47
|
+
scope.dispose();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function addNewOrder(this: TestConfig, customer: User, userID?) {
|
|
52
|
+
const scope = ServiceProvider.global.createScope();
|
|
53
|
+
try {
|
|
54
|
+
const user = new UserInfo();
|
|
55
|
+
user.userID = userID ?? customer.userID;
|
|
56
|
+
scope.add(Logger, Logger.instance);
|
|
57
|
+
scope.add(BaseDriver, this.driver);
|
|
58
|
+
scope.add(UserInfo, user);
|
|
59
|
+
scope.add(ContextEvents, new ShoppingContextEvents());
|
|
60
|
+
const context = scope.create(ShoppingContext);
|
|
61
|
+
context.verifyFilters = true;
|
|
62
|
+
|
|
63
|
+
// get first headphone...
|
|
64
|
+
const headPhone = await context.products.all().firstOrFail();
|
|
65
|
+
const headPhonePrice = await context.productPrices.where({ id: headPhone.productID }, (p) => (x) => x.productID === p.id).firstOrFail();
|
|
66
|
+
|
|
67
|
+
context.orders.add({
|
|
68
|
+
customer,
|
|
69
|
+
orderDate: new Date(),
|
|
70
|
+
orderItems: [
|
|
71
|
+
context.orderItems.add({
|
|
72
|
+
product: headPhone,
|
|
73
|
+
productPrice: headPhonePrice,
|
|
74
|
+
amount: headPhonePrice.amount,
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await context.saveChanges();
|
|
80
|
+
|
|
81
|
+
// lets filter the orders
|
|
82
|
+
|
|
83
|
+
const f = context.orders.filtered();
|
|
84
|
+
const myOrders = await f.count();
|
|
85
|
+
assert.equal(1, myOrders);
|
|
86
|
+
|
|
87
|
+
const all = await context.orders.all().count();
|
|
88
|
+
assert.notEqual(all, myOrders);
|
|
89
|
+
|
|
90
|
+
} finally {
|
|
91
|
+
scope.dispose();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function createUser(config: TestConfig) {
|
|
96
|
+
const context = await createContext(config.driver);
|
|
97
|
+
config.driver.connectionString.database = context.driver.connectionString.database;
|
|
98
|
+
const user = context.users.add({
|
|
99
|
+
dateCreated: new Date()
|
|
100
|
+
});
|
|
101
|
+
await context.saveChanges();
|
|
102
|
+
return user;
|
|
103
|
+
}
|
|
104
|
+
|
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",
|
|
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": [
|