@entity-access/entity-access 1.0.251 → 1.0.253
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/.vscode/launch.json +2 -1
- package/dist/common/symbols/symbols.d.ts +1 -0
- package/dist/common/symbols/symbols.d.ts.map +1 -1
- package/dist/common/symbols/symbols.js +1 -0
- package/dist/common/symbols/symbols.js.map +1 -1
- package/dist/compiler/QueryCompiler.d.ts +3 -3
- package/dist/decorators/ForeignKey.d.ts +8 -7
- package/dist/decorators/ForeignKey.d.ts.map +1 -1
- package/dist/decorators/ForeignKey.js +43 -8
- package/dist/decorators/ForeignKey.js.map +1 -1
- package/dist/decorators/IColumn.d.ts +6 -3
- package/dist/decorators/IColumn.d.ts.map +1 -1
- package/dist/decorators/Relate.d.ts.map +1 -1
- package/dist/decorators/Relate.js +8 -6
- package/dist/decorators/Relate.js.map +1 -1
- package/dist/entity-query/EntityType.d.ts +5 -1
- package/dist/entity-query/EntityType.d.ts.map +1 -1
- package/dist/entity-query/EntityType.js +57 -25
- package/dist/entity-query/EntityType.js.map +1 -1
- package/dist/migrations/postgres/PostgresAutomaticMigrations.js +1 -1
- package/dist/migrations/postgres/PostgresAutomaticMigrations.js.map +1 -1
- package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js +1 -1
- package/dist/migrations/sql-server/SqlServerAutomaticMigrations.js.map +1 -1
- package/dist/model/EntityModel.js +2 -2
- package/dist/model/EntityModel.js.map +1 -1
- package/dist/model/EntitySource.d.ts +6 -1
- package/dist/model/EntitySource.d.ts.map +1 -1
- package/dist/model/EntitySource.js.map +1 -1
- package/dist/model/SourceExpression.d.ts +1 -22
- package/dist/model/SourceExpression.d.ts.map +1 -1
- package/dist/model/SourceExpression.js +116 -98
- package/dist/model/SourceExpression.js.map +1 -1
- package/dist/model/changes/ChangeEntry.d.ts.map +1 -1
- package/dist/model/changes/ChangeEntry.js +62 -25
- package/dist/model/changes/ChangeEntry.js.map +1 -1
- package/dist/model/changes/ChangeSet.d.ts +2 -1
- package/dist/model/changes/ChangeSet.d.ts.map +1 -1
- package/dist/model/changes/ChangeSet.js +4 -3
- package/dist/model/changes/ChangeSet.js.map +1 -1
- package/dist/model/identity/IdentityMap.d.ts +23 -0
- package/dist/model/identity/IdentityMap.d.ts.map +1 -0
- package/dist/model/identity/IdentityMap.js +113 -0
- package/dist/model/identity/IdentityMap.js.map +1 -0
- package/dist/model/identity/RelationMapper.d.ts +2 -3
- package/dist/model/identity/RelationMapper.d.ts.map +1 -1
- package/dist/model/identity/RelationMapper.js +60 -27
- package/dist/model/identity/RelationMapper.js.map +1 -1
- package/dist/model/identity/SearchIndex.d.ts +17 -0
- package/dist/model/identity/SearchIndex.d.ts.map +1 -0
- package/dist/model/identity/SearchIndex.js +109 -0
- package/dist/model/identity/SearchIndex.js.map +1 -0
- package/dist/model/verification/VerificationSession.d.ts +1 -1
- package/dist/model/verification/VerificationSession.d.ts.map +1 -1
- package/dist/model/verification/VerificationSession.js +18 -16
- package/dist/model/verification/VerificationSession.js.map +1 -1
- package/dist/query/ast/ExpressionToSql.d.ts.map +1 -1
- package/dist/query/ast/ExpressionToSql.js +74 -52
- package/dist/query/ast/ExpressionToSql.js.map +1 -1
- package/dist/query/expander/QueryExpander.d.ts.map +1 -1
- package/dist/query/expander/QueryExpander.js +41 -10
- package/dist/query/expander/QueryExpander.js.map +1 -1
- package/dist/query/parser/ArrowToExpression.d.ts +16 -5
- package/dist/query/parser/ArrowToExpression.d.ts.map +1 -1
- package/dist/query/parser/ArrowToExpression.js +45 -25
- package/dist/query/parser/ArrowToExpression.js.map +1 -1
- package/dist/tests/db-tests/tests/multi-fk-tests.d.ts +3 -0
- package/dist/tests/db-tests/tests/multi-fk-tests.d.ts.map +1 -0
- package/dist/tests/db-tests/tests/multi-fk-tests.js +38 -0
- package/dist/tests/db-tests/tests/multi-fk-tests.js.map +1 -0
- package/dist/tests/expressions/left-joins/child-joins.js +7 -7
- package/dist/tests/model/ShoppingContext.d.ts +9 -0
- package/dist/tests/model/ShoppingContext.d.ts.map +1 -1
- package/dist/tests/model/ShoppingContext.js +34 -0
- package/dist/tests/model/ShoppingContext.js.map +1 -1
- package/dist/tests/security/tests/include-items.d.ts.map +1 -1
- package/dist/tests/security/tests/include-items.js +1 -0
- package/dist/tests/security/tests/include-items.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/common/symbols/symbols.ts +2 -1
- package/src/decorators/ForeignKey.ts +66 -28
- package/src/decorators/IColumn.ts +4 -3
- package/src/decorators/Relate.ts +8 -6
- package/src/entity-query/EntityType.ts +60 -26
- package/src/migrations/postgres/PostgresAutomaticMigrations.ts +1 -1
- package/src/migrations/sql-server/SqlServerAutomaticMigrations.ts +1 -1
- package/src/model/EntityModel.ts +2 -2
- package/src/model/EntitySource.ts +6 -1
- package/src/model/SourceExpression.ts +132 -132
- package/src/model/changes/ChangeEntry.ts +68 -25
- package/src/model/changes/ChangeSet.ts +4 -3
- package/src/model/identity/IdentityMap.ts +126 -0
- package/src/model/identity/RelationMapper.ts +71 -27
- package/src/model/identity/SearchIndex.ts +120 -0
- package/src/model/verification/VerificationSession.ts +19 -16
- package/src/query/ast/ExpressionToSql.ts +77 -61
- package/src/query/expander/QueryExpander.ts +52 -28
- package/src/query/parser/ArrowToExpression.ts +50 -26
- package/src/tests/db-tests/tests/multi-fk-tests.ts +46 -0
- package/src/tests/expressions/left-joins/child-joins.ts +7 -7
- package/src/tests/model/ShoppingContext.ts +32 -0
- package/src/tests/security/tests/include-items.ts +1 -0
|
@@ -98,7 +98,7 @@ export class QueryExpander {
|
|
|
98
98
|
// let where: Expression;
|
|
99
99
|
// let joinWhere: Expression;
|
|
100
100
|
|
|
101
|
-
const fk = relation.
|
|
101
|
+
const fk = relation.fkMap ?? relation.relatedRelation.fkMap;
|
|
102
102
|
|
|
103
103
|
key += "." + relation.name;
|
|
104
104
|
|
|
@@ -107,15 +107,17 @@ export class QueryExpander {
|
|
|
107
107
|
return relationSet;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
let where: Expression;
|
|
111
|
+
|
|
110
112
|
if(relation.isInverseRelation) {
|
|
111
113
|
|
|
112
|
-
const keyColumn = model.keys[0].columnName;
|
|
113
|
-
let columnName = fk.columnName;
|
|
114
|
-
// for inverse relation, we need to
|
|
115
|
-
// use primary key of current model
|
|
116
|
-
if (!relation.isCollection) {
|
|
117
|
-
|
|
118
|
-
}
|
|
114
|
+
// const keyColumn = model.keys[0].columnName;
|
|
115
|
+
// let columnName = fk.columnName;
|
|
116
|
+
// // for inverse relation, we need to
|
|
117
|
+
// // use primary key of current model
|
|
118
|
+
// if (!relation.isCollection) {
|
|
119
|
+
// columnName = select.model.keys[0].columnName;
|
|
120
|
+
// }
|
|
119
121
|
|
|
120
122
|
|
|
121
123
|
const joins = (select.joins ??= []);
|
|
@@ -124,21 +126,32 @@ export class QueryExpander {
|
|
|
124
126
|
// This join has to be INNER JOIN as we are only interested
|
|
125
127
|
// in the results that matches parent query exactly
|
|
126
128
|
|
|
129
|
+
for (const { fkColumn, relatedKeyColumn } of relation.relatedRelation.fkMap) {
|
|
130
|
+
const joinColumn = fkColumn.entityType === joinParameter.model ? fkColumn : relatedKeyColumn;
|
|
131
|
+
const relatedColumn = relatedKeyColumn.entityType === select.sourceParameter.model ? relatedKeyColumn : fkColumn;
|
|
132
|
+
const joinOn = Expression.equal(
|
|
133
|
+
Expression.member(joinParameter, Expression.identifier(joinColumn.columnName)),
|
|
134
|
+
Expression.member(select.sourceParameter, Expression.identifier(relatedColumn.columnName))
|
|
135
|
+
);
|
|
136
|
+
where = where ? Expression.logicalAnd(where, joinOn) : joinOn;
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
joins.push(JoinExpression.create({
|
|
128
140
|
joinType: "INNER",
|
|
129
141
|
source: { ... parent },
|
|
130
142
|
as: joinParameter,
|
|
131
143
|
model: parent.model,
|
|
132
|
-
where
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
144
|
+
where
|
|
145
|
+
// where: Expression.equal(
|
|
146
|
+
// Expression.member(
|
|
147
|
+
// joinParameter,
|
|
148
|
+
// Expression.identifier(keyColumn)
|
|
149
|
+
// ),
|
|
150
|
+
// Expression.member(
|
|
151
|
+
// select.sourceParameter,
|
|
152
|
+
// Expression.identifier(columnName)
|
|
153
|
+
// )
|
|
154
|
+
// )
|
|
142
155
|
}));
|
|
143
156
|
|
|
144
157
|
// if (parent.where) {
|
|
@@ -192,22 +205,33 @@ export class QueryExpander {
|
|
|
192
205
|
// This join has to be INNER JOIN as we are only interested
|
|
193
206
|
// in the results that matches parent query exactly
|
|
194
207
|
|
|
208
|
+
for (const { fkColumn, relatedKeyColumn } of relation.fkMap) {
|
|
209
|
+
const joinOn = Expression.equal(
|
|
210
|
+
Expression.member(selectJoinParameter,
|
|
211
|
+
Expression.identifier(fkColumn.columnName)),
|
|
212
|
+
Expression.member(select.sourceParameter,
|
|
213
|
+
Expression.identifier(relatedKeyColumn.columnName))
|
|
214
|
+
);
|
|
215
|
+
where = where ? Expression.logicalAnd(where, joinOn) : joinOn;
|
|
216
|
+
}
|
|
217
|
+
|
|
195
218
|
selectJoins.push(JoinExpression.create({
|
|
196
219
|
joinType: "INNER",
|
|
197
220
|
source: { ... parent },
|
|
198
221
|
as: selectJoinParameter,
|
|
199
222
|
model: parent.model,
|
|
223
|
+
where
|
|
200
224
|
// model,
|
|
201
|
-
where: Expression.equal(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
225
|
+
// where: Expression.equal(
|
|
226
|
+
// Expression.member(
|
|
227
|
+
// selectJoinParameter,
|
|
228
|
+
// Expression.identifier(fk.columnName)
|
|
229
|
+
// ),
|
|
230
|
+
// Expression.member(
|
|
231
|
+
// select.sourceParameter,
|
|
232
|
+
// Expression.identifier(relation.relatedEntity.keys[0].columnName)
|
|
233
|
+
// )
|
|
234
|
+
// )
|
|
211
235
|
}));
|
|
212
236
|
|
|
213
237
|
this.include.push(select);
|
|
@@ -10,8 +10,19 @@ type IQueryFragment = string | { name?: string, value?: any };
|
|
|
10
10
|
|
|
11
11
|
const parsedCache = new TimedCache<string, bpe.Node>();
|
|
12
12
|
|
|
13
|
+
const parameterCacheSymbol = Symbol("parameterCacheSymbol");
|
|
14
|
+
|
|
15
|
+
const defaultObject = {};
|
|
16
|
+
|
|
13
17
|
export default class ArrowToExpression extends BabelVisitor<Expression> {
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Parses lambda expression function or string to set simple SQL ready AST which
|
|
21
|
+
* transforms basic && to AND etc.
|
|
22
|
+
* @param fx function code (usually lambda expression)
|
|
23
|
+
* @param target parameter target
|
|
24
|
+
* @returns Parsed expression
|
|
25
|
+
*/
|
|
15
26
|
public static transform(fx: (p: any) => (x: any) => any, target?: ParameterExpression) {
|
|
16
27
|
const key = fx.toString();
|
|
17
28
|
const node = parsedCache.getOrCreate(key, fx, (k, f) => {
|
|
@@ -21,41 +32,54 @@ export default class ArrowToExpression extends BabelVisitor<Expression> {
|
|
|
21
32
|
return this.transformUncached(node, target);
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Since expression parsed as a different parameter in nested lambda (p) => (x) => x..,
|
|
37
|
+
* we need to replace x with provided target to bind x with respective ParameterExpression.
|
|
38
|
+
* As ParameterExpression contains the type and model associated with the table represented by `x`.
|
|
39
|
+
* @param node parsed node
|
|
40
|
+
* @param target parameter to replace
|
|
41
|
+
* @returns transformed node
|
|
42
|
+
*/
|
|
43
|
+
private static transformUncached(node: bpe.Node, tx?: ParameterExpression) {
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
throw new Error("Expecting an arrow function");
|
|
28
|
-
}
|
|
45
|
+
const cache = node[parameterCacheSymbol] ??= new TimedCache<ParameterExpression,bpe.Node>();
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
return cache.getOrCreate(tx ?? defaultObject, tx, (_, target) => {
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new Error("Expecting an identifier");
|
|
49
|
+
if (node.type !== "ArrowFunctionExpression") {
|
|
50
|
+
throw new Error("Expecting an arrow function");
|
|
35
51
|
}
|
|
36
|
-
params.push(ParameterExpression.create({ name: iterator.name }));
|
|
37
|
-
}
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
if (body.type !== "ArrowFunctionExpression") {
|
|
41
|
-
throw new Error("Expecting an arrow function");
|
|
42
|
-
}
|
|
53
|
+
const params = [] as ParameterExpression[];
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
for (const iterator of node.params) {
|
|
56
|
+
if (iterator.type !== "Identifier") {
|
|
57
|
+
throw new Error("Expecting an identifier");
|
|
58
|
+
}
|
|
59
|
+
params.push(ParameterExpression.create({ name: iterator.name }));
|
|
60
|
+
}
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
let body = node.body;
|
|
63
|
+
if (body.type !== "ArrowFunctionExpression") {
|
|
64
|
+
throw new Error("Expecting an arrow function");
|
|
65
|
+
}
|
|
50
66
|
|
|
51
|
-
|
|
67
|
+
const firstTarget = body.params[0];
|
|
68
|
+
if (firstTarget.type !== "Identifier") {
|
|
69
|
+
throw new Error("Expecting an identifier");
|
|
70
|
+
}
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
target ??= ParameterExpression.create({ name: firstTarget.name});
|
|
73
|
+
|
|
74
|
+
body = body.body;
|
|
75
|
+
|
|
76
|
+
const visitor = new this(params, target, firstTarget.name);
|
|
77
|
+
return {
|
|
78
|
+
params,
|
|
79
|
+
target,
|
|
80
|
+
body: visitor.visit(body)
|
|
81
|
+
};
|
|
82
|
+
});
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
public readonly leftJoins: string[] = [];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { TestConfig } from "../../TestConfig.js";
|
|
3
|
+
import { createContext } from "../../model/createContext.js";
|
|
4
|
+
|
|
5
|
+
export default async function(this: TestConfig) {
|
|
6
|
+
|
|
7
|
+
if (!this.db) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const context = await createContext(this.driver);
|
|
12
|
+
|
|
13
|
+
const { userID } = await context.users.asQuery().first();
|
|
14
|
+
const { categoryID } = await context.categories.asQuery().first();
|
|
15
|
+
|
|
16
|
+
await context.userCategories.saveDirect({
|
|
17
|
+
mode: "insert",
|
|
18
|
+
changes: {
|
|
19
|
+
userID,
|
|
20
|
+
categoryID
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await context.userCategoryTags.saveDirect({
|
|
25
|
+
mode: "insert",
|
|
26
|
+
changes: {
|
|
27
|
+
categoryID,
|
|
28
|
+
userID,
|
|
29
|
+
tag: "A"
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
await context.userCategoryTags.saveDirect({
|
|
33
|
+
mode: "insert",
|
|
34
|
+
changes: {
|
|
35
|
+
categoryID,
|
|
36
|
+
userID,
|
|
37
|
+
tag: "B"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const first = await context.userCategories.asQuery()
|
|
42
|
+
.include((x) => x.tags)
|
|
43
|
+
.first();
|
|
44
|
+
|
|
45
|
+
assert.notEqual(void 0, first.tags);
|
|
46
|
+
}
|
|
@@ -14,7 +14,7 @@ FROM products AS p1
|
|
|
14
14
|
WHERE EXISTS (SELECT
|
|
15
15
|
1
|
|
16
16
|
FROM order_items AS o
|
|
17
|
-
WHERE (
|
|
17
|
+
WHERE (o.product_id = $1) AND (p1.product_id = o.product_id))`;
|
|
18
18
|
|
|
19
19
|
const sql2 = `SELECT
|
|
20
20
|
p1.product_id,
|
|
@@ -26,10 +26,10 @@ FROM products AS p1
|
|
|
26
26
|
WHERE EXISTS (SELECT
|
|
27
27
|
1
|
|
28
28
|
FROM order_items AS o
|
|
29
|
-
WHERE (
|
|
29
|
+
WHERE (o.product_id = $1) AND (p1.product_id = o.product_id)) AND EXISTS (SELECT
|
|
30
30
|
1
|
|
31
31
|
FROM order_items AS o1
|
|
32
|
-
WHERE (
|
|
32
|
+
WHERE (o1.amount > $2) AND (p1.product_id = o1.product_id))`;
|
|
33
33
|
|
|
34
34
|
const sql3 = `SELECT
|
|
35
35
|
p1.product_id,
|
|
@@ -41,11 +41,11 @@ FROM products AS p1
|
|
|
41
41
|
WHERE EXISTS (SELECT
|
|
42
42
|
1
|
|
43
43
|
FROM order_items AS o
|
|
44
|
-
WHERE (
|
|
44
|
+
WHERE (o.product_id = $1) AND (p1.product_id = o.product_id)) AND EXISTS (SELECT
|
|
45
45
|
1
|
|
46
46
|
FROM order_items AS o1
|
|
47
47
|
INNER JOIN orders AS o2 ON o1.order_id = o2.order_id
|
|
48
|
-
WHERE (
|
|
48
|
+
WHERE (o2.order_date > $2) AND (p1.product_id = o1.product_id))`;
|
|
49
49
|
|
|
50
50
|
const productJoin = `SELECT
|
|
51
51
|
p1.product_id,
|
|
@@ -77,10 +77,10 @@ p1.product_description
|
|
|
77
77
|
FROM products AS p1
|
|
78
78
|
WHERE EXISTS
|
|
79
79
|
(SELECT 1 FROM order_items AS o
|
|
80
|
-
WHERE (
|
|
80
|
+
WHERE (o.product_id = $1) AND (p1.product_id = o.product_id)) AND
|
|
81
81
|
NOT (EXISTS (SELECT 1 FROM order_items AS o1
|
|
82
82
|
INNER JOIN orders AS o2 ON o1.order_id = o2.order_id
|
|
83
|
-
WHERE (
|
|
83
|
+
WHERE (o2.order_date > $2) AND (p1.product_id = o1.product_id)))
|
|
84
84
|
`;
|
|
85
85
|
|
|
86
86
|
export default function() {
|
|
@@ -6,6 +6,7 @@ import Index from "../../decorators/Index.js";
|
|
|
6
6
|
import DateTime from "../../types/DateTime.js";
|
|
7
7
|
import { UserFile } from "./UseFile.js";
|
|
8
8
|
import Sql from "../../sql/Sql.js";
|
|
9
|
+
import MultiForeignKeys from "../../decorators/ForeignKey.js";
|
|
9
10
|
|
|
10
11
|
export const statusPublished = "published";
|
|
11
12
|
|
|
@@ -34,6 +35,8 @@ export class ShoppingContext extends EntityContext {
|
|
|
34
35
|
public userFiles = this.model.register(UserFile);
|
|
35
36
|
|
|
36
37
|
public emailAddresses = this.model.register(EmailAddress);
|
|
38
|
+
|
|
39
|
+
public userCategoryTags = this.model.register(UserCategoryTag);
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
@Table("Users")
|
|
@@ -191,6 +194,35 @@ export class UserCategory {
|
|
|
191
194
|
public user: User;
|
|
192
195
|
|
|
193
196
|
public category: Category;
|
|
197
|
+
|
|
198
|
+
public tags: UserCategoryTag[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@Table("UserCategoryTags")
|
|
202
|
+
export class UserCategoryTag {
|
|
203
|
+
|
|
204
|
+
@Column({ key: true, dataType: "BigInt", generated: "identity"})
|
|
205
|
+
tagID: number;
|
|
206
|
+
|
|
207
|
+
@Column({ dataType: "Char", length: 200 })
|
|
208
|
+
tag: string;
|
|
209
|
+
|
|
210
|
+
@Column({ dataType: "BigInt"})
|
|
211
|
+
public userID: number;
|
|
212
|
+
|
|
213
|
+
@Column({ dataType: "Char", length: 200 })
|
|
214
|
+
public categoryID: string;
|
|
215
|
+
|
|
216
|
+
@MultiForeignKeys(UserCategory, {
|
|
217
|
+
inverseProperty: (x) => x.tags,
|
|
218
|
+
foreignKeys: [
|
|
219
|
+
{ foreignKey: (x) => x.userID, key: (x) => x.userID },
|
|
220
|
+
{ foreignKey: (x) => x.categoryID, key: (x) => x.categoryID}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
public userCategory: UserCategory;
|
|
225
|
+
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
@Table("Products")
|