@entity-access/entity-access 1.0.1
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/.eslintrc.cjs +234 -0
- package/.github/workflows/node.yml +44 -0
- package/.github/workflows/npm-publish.yml +33 -0
- package/.vscode/launch.json +20 -0
- package/.vscode/settings.json +39 -0
- package/LICENSE +201 -0
- package/README.md +247 -0
- package/package.json +39 -0
- package/src/TestRunner.ts +2 -0
- package/src/common/cache/InstanceCache.ts +14 -0
- package/src/common/cache/TimedCache.ts +74 -0
- package/src/common/symbols/symbols.ts +2 -0
- package/src/common/usingAsync.ts +24 -0
- package/src/compiler/ISqlHelpers.ts +30 -0
- package/src/compiler/QueryCompiler.ts +88 -0
- package/src/compiler/postgres/PostgreSqlMethodTransformer.ts +83 -0
- package/src/compiler/sql-server/SqlServerSqlMethodTransformer.ts +83 -0
- package/src/decorators/Column.ts +46 -0
- package/src/decorators/ForeignKey.ts +42 -0
- package/src/decorators/IClassOf.ts +1 -0
- package/src/decorators/IColumn.ts +70 -0
- package/src/decorators/ISqlType.ts +70 -0
- package/src/decorators/SchemaRegistry.ts +19 -0
- package/src/decorators/Table.ts +18 -0
- package/src/decorators/parser/MemberParser.ts +8 -0
- package/src/drivers/base/BaseDriver.ts +134 -0
- package/src/drivers/postgres/PostgreSqlDriver.ts +178 -0
- package/src/drivers/sql-server/ExpressionToSqlServer.ts +78 -0
- package/src/drivers/sql-server/SqlServerDriver.ts +215 -0
- package/src/drivers/sql-server/SqlServerLiteral.ts +12 -0
- package/src/drivers/sql-server/SqlServerQueryCompiler.ts +25 -0
- package/src/entity-query/EntityType.ts +116 -0
- package/src/migrations/Migrations.ts +17 -0
- package/src/migrations/postgres/PostgresAutomaticMigrations.ts +97 -0
- package/src/migrations/postgres/PostgresMigrations.ts +56 -0
- package/src/migrations/sql-server/SqlServerAutomaticMigrations.ts +102 -0
- package/src/migrations/sql-server/SqlServerMigrations.ts +60 -0
- package/src/model/ChangeEntry.ts +154 -0
- package/src/model/ChangeSet.ts +88 -0
- package/src/model/EntityContext.ts +48 -0
- package/src/model/EntityModel.ts +27 -0
- package/src/model/EntityQuery.ts +152 -0
- package/src/model/EntitySchema.ts +21 -0
- package/src/model/EntitySource.ts +98 -0
- package/src/model/IFilterWithParameter.ts +38 -0
- package/src/model/IdentityService.ts +23 -0
- package/src/model/SourceExpression.ts +137 -0
- package/src/query/Query.ts +158 -0
- package/src/query/ast/ExpressionToSql.ts +348 -0
- package/src/query/ast/Expressions.ts +294 -0
- package/src/query/ast/IStringTransformer.ts +74 -0
- package/src/query/ast/SqlLiteral.ts +25 -0
- package/src/query/ast/Visitor.ts +159 -0
- package/src/query/parser/ArrowToExpression.ts +160 -0
- package/src/query/parser/BabelVisitor.ts +86 -0
- package/src/sql/ISql.ts +31 -0
- package/src/sql/Sql.ts +4 -0
- package/src/tests/TestConfig.ts +6 -0
- package/src/tests/db-tests/tests/select-items.ts +37 -0
- package/src/tests/db-tests/tests/update-items.ts +17 -0
- package/src/tests/drivers/postgres/connection-test.ts +29 -0
- package/src/tests/drivers/sql-server/sql-server-test.ts +9 -0
- package/src/tests/expressions/left-joins/child-joins.ts +71 -0
- package/src/tests/expressions/simple/parse-arrow.ts +46 -0
- package/src/tests/expressions/trimInternal.ts +23 -0
- package/src/tests/model/ShoppingContext.ts +203 -0
- package/src/tests/model/createContext.ts +294 -0
- package/src/tests/query/combine.ts +28 -0
- package/test.js +107 -0
- package/tsconfig.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Entity Access
|
|
2
|
+
|
|
3
|
+
Inspired from Entity Framework Core, Entity Access is ORM for JavaScript runtime such as Node, YantraJS.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Project Status
|
|
7
|
+
1. Beta - Postgres Driver
|
|
8
|
+
2. Alpha - Sql Server Driver
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
1. Unit of Work and Repository Pattern
|
|
12
|
+
2. Arrow function based query features with automatic joins.
|
|
13
|
+
3. Automatic Migrations for missing schema - this is done for fast development and deployment.
|
|
14
|
+
4. Sql functions such as LIKE
|
|
15
|
+
5. Postgres Driver
|
|
16
|
+
6. Sql Server Driver
|
|
17
|
+
7. Automatic parameterization to safeguard sql injection attacks.
|
|
18
|
+
|
|
19
|
+
## Upcoming Features
|
|
20
|
+
1. Include
|
|
21
|
+
2. Projection - Split query mode only, single level only.
|
|
22
|
+
3. GroupBy
|
|
23
|
+
|
|
24
|
+
### Unit of Work
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const db = new ShoppingContext();
|
|
28
|
+
db.orders.add({
|
|
29
|
+
orderDate: new Date(),
|
|
30
|
+
userID,
|
|
31
|
+
orderItems: [
|
|
32
|
+
db.orderItems.add({
|
|
33
|
+
productID,
|
|
34
|
+
amount
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// save all in single transaction
|
|
40
|
+
await db.saveChanges();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Arrow function based query features
|
|
44
|
+
|
|
45
|
+
Arrow function based query provides many benefits over tagged template literals or other fluent methods to build queries.
|
|
46
|
+
1. Arrow functions are easy to visualize.
|
|
47
|
+
2. You will get intellisense help to complete the query.
|
|
48
|
+
3. You will get errors if the wrong data types are compared or used in computations.
|
|
49
|
+
4. Change of property name will automatically refactor as typescript will keep references of where the property is used.
|
|
50
|
+
|
|
51
|
+
Simple Query
|
|
52
|
+
```typescript
|
|
53
|
+
const db = new ShoppingContext();
|
|
54
|
+
|
|
55
|
+
/// first parameter is set of parameters to pass to the query
|
|
56
|
+
/// the reason it is first, is to help in intellisense.
|
|
57
|
+
|
|
58
|
+
/// second parameter is an arrow function which returns a filter
|
|
59
|
+
const q = db.orders.where({ userID }, (params) => (order) => order.userID === p.userID);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Query with Like operator
|
|
63
|
+
```typescript
|
|
64
|
+
/// Find all orders for specified customer
|
|
65
|
+
/// Sql functions
|
|
66
|
+
const userName = `akash%`;
|
|
67
|
+
const q = db.orders.where({ userName },
|
|
68
|
+
(params) =>
|
|
69
|
+
(order) =>
|
|
70
|
+
Sql.text.like(
|
|
71
|
+
order.customer.userName,
|
|
72
|
+
p.userName
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// note that the join will be performed automatically
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Typed Configurations
|
|
80
|
+
```typescript
|
|
81
|
+
class ShoppingContext {
|
|
82
|
+
products = this.model.register(Product);
|
|
83
|
+
orders = this.model.register(Order);
|
|
84
|
+
orderItems = this.model.register(OrderItem);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Table("Products")
|
|
88
|
+
class Product {
|
|
89
|
+
|
|
90
|
+
@Column({ key: true, autoGenerated: true })
|
|
91
|
+
productID: number;
|
|
92
|
+
|
|
93
|
+
orderItems: OrderItem[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Table("OrderItems")
|
|
97
|
+
class OrderItem {
|
|
98
|
+
|
|
99
|
+
@Column({ key: true, autoGenerated: true })
|
|
100
|
+
orderItemID: number;
|
|
101
|
+
|
|
102
|
+
@Column()
|
|
103
|
+
productID: number;
|
|
104
|
+
|
|
105
|
+
@Column()
|
|
106
|
+
orderID: number;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Following configuration declares everything
|
|
110
|
+
* that will give compilation error if configured wrong.
|
|
111
|
+
*/
|
|
112
|
+
@ForeignKey({
|
|
113
|
+
key: (orderItem) => orderItem.productID,
|
|
114
|
+
related: Product,
|
|
115
|
+
relatedProperty:(product) => product.orderItems
|
|
116
|
+
})
|
|
117
|
+
product: Product;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Query Examples
|
|
124
|
+
|
|
125
|
+
### Compare operators
|
|
126
|
+
|
|
127
|
+
#### Equality
|
|
128
|
+
Both strict and non strict equality will result in
|
|
129
|
+
simple equality comparison in SQL. Database provider
|
|
130
|
+
may or may not convert them correctly, so we recommend
|
|
131
|
+
using helper functions to convert before comparison.
|
|
132
|
+
```typescript
|
|
133
|
+
// find all customer from orderID
|
|
134
|
+
const q = db.customers
|
|
135
|
+
// first we will send parameters
|
|
136
|
+
.where({ orderID },
|
|
137
|
+
// second we will write an arrow
|
|
138
|
+
// accepting parameters
|
|
139
|
+
(p) =>
|
|
140
|
+
// this is the arrow which will
|
|
141
|
+
// be converted to SQL
|
|
142
|
+
// you can write very limited set of
|
|
143
|
+
// expressions in this arrow function
|
|
144
|
+
(x) => x.orders.some(
|
|
145
|
+
// This will results in exists or join
|
|
146
|
+
// based on what level of nested
|
|
147
|
+
// foreign key references are available
|
|
148
|
+
(order) => order.orderID === p.orderID )
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Above expression will result in following filter expression
|
|
154
|
+
```sql
|
|
155
|
+
EXISTS (
|
|
156
|
+
SELECT 1
|
|
157
|
+
FROM Orders as o1
|
|
158
|
+
WHERE x.customerID = o1.orderID
|
|
159
|
+
AND o1.orderID = $1
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### Like
|
|
164
|
+
|
|
165
|
+
To use `LIKE` operator, `Sql.text.like` method must be used
|
|
166
|
+
as it is. Query compiler will only match everything starting
|
|
167
|
+
with `Sql.` and it will inject available operator conversion.
|
|
168
|
+
|
|
169
|
+
You don't have to worry about sql injection as each parameter
|
|
170
|
+
passed will be sent as a sql parameter and not as a literal.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
const prefix = `${name}%`;
|
|
174
|
+
db.customers.where({ prefix },
|
|
175
|
+
(p) =>
|
|
176
|
+
(customer) => Sql.text.like(customer.firstName, p.prefix)
|
|
177
|
+
|| Sql.text.like(customer.lastName p.prefix)
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Sql Text Functions
|
|
182
|
+
For other sql text functions you can use `Sql.text.startsWith`, `Sql.text.endsWith`, `Sql.text.left`... etc as shown below.
|
|
183
|
+
```typescript
|
|
184
|
+
db.customers.where({ prefix },
|
|
185
|
+
(p) =>
|
|
186
|
+
(customer) => Sql.text.startsWith(customer.firstName, p.prefix)
|
|
187
|
+
|| Sql.text.startsWith(customer.lastName p.prefix)
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Sql date functions
|
|
192
|
+
Just as text functions you can also use date functions as shown below.
|
|
193
|
+
```typescript
|
|
194
|
+
const year = (new Date()).getFullYear();
|
|
195
|
+
// get count of all orders of this year...
|
|
196
|
+
db.orders.where({ year },
|
|
197
|
+
(p) =>
|
|
198
|
+
(order) => Sql.date.yearOf(order.orderDate) === p.year
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// above example is only for illustrations only, it will not use index.
|
|
202
|
+
// for index usage, please consider window function shown below.
|
|
203
|
+
const start:Date = /* start date */;
|
|
204
|
+
const end:Date = /* start date */;
|
|
205
|
+
// get count of all orders of this year...
|
|
206
|
+
db.orders.where({ start, end },
|
|
207
|
+
(p) =>
|
|
208
|
+
(order) => p.start <= order.orderDate && order.orderDate >= p.end
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### OrderBy
|
|
214
|
+
```typescript
|
|
215
|
+
q.orderBy({}, (p) => (x) => x.orderDate)
|
|
216
|
+
.thenBy({}, (p) => (x) => x.customer.firstName)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Limit/Offset
|
|
220
|
+
```typescript
|
|
221
|
+
q = q.orderByDescending({}, (p) => (x) => x.orderDate)
|
|
222
|
+
.thenBy({}, (p) => (x) => x.customer.firstName)
|
|
223
|
+
.limit(50)
|
|
224
|
+
.offset(50);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Enumerate
|
|
228
|
+
```typescript
|
|
229
|
+
for await(const product of q.enumerate()) {
|
|
230
|
+
//
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### First / First or Fail
|
|
235
|
+
```typescript
|
|
236
|
+
// it will return first product or null
|
|
237
|
+
const firstProduct = await q.first();
|
|
238
|
+
|
|
239
|
+
// it will throw and exception if product was not
|
|
240
|
+
// found
|
|
241
|
+
const firstProduct = await q.firstOrFail();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Count
|
|
245
|
+
```typescript
|
|
246
|
+
const total = await q.count();
|
|
247
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entity-access/entity-access",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --enable-source-maps ./test.js",
|
|
8
|
+
"test-db": "node --enable-source-maps ./test.js test-db"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@babel/parser": "^7.22.5",
|
|
15
|
+
"@babel/types": "^7.22.5",
|
|
16
|
+
"pg": "^8.11.1",
|
|
17
|
+
"pg-cursor": "^2.10.1",
|
|
18
|
+
"reflect-metadata": "^0.1.13",
|
|
19
|
+
"tcp-port-used": "^1.0.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/mssql": "^8.1.2",
|
|
23
|
+
"@types/node": "^20.3.2",
|
|
24
|
+
"@types/pg": "^8.10.2",
|
|
25
|
+
"@types/pg-cursor": "^2.7.0",
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
|
27
|
+
"@typescript-eslint/parser": "^5.59.9",
|
|
28
|
+
"eslint": "^8.42.0",
|
|
29
|
+
"eslint-config-prettier": "^8.8.0",
|
|
30
|
+
"eslint-config-standard": "^17.1.0",
|
|
31
|
+
"eslint-plugin-import": "^2.27.5",
|
|
32
|
+
"eslint-plugin-jsdoc": "^46.2.6",
|
|
33
|
+
"eslint-plugin-node": "^11.1.0",
|
|
34
|
+
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
35
|
+
"eslint-plugin-promise": "^6.1.1",
|
|
36
|
+
"eslint-plugin-react": "^7.32.2",
|
|
37
|
+
"mssql": "^9.1.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function InstanceCache(target: any, key: string): void {
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(target, key, {
|
|
4
|
+
get() {
|
|
5
|
+
const value = this[key];
|
|
6
|
+
Object.defineProperty(this, key, {
|
|
7
|
+
value,
|
|
8
|
+
});
|
|
9
|
+
return value;
|
|
10
|
+
},
|
|
11
|
+
configurable: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export default class TimedCache<TKey = any, T = any> {
|
|
2
|
+
|
|
3
|
+
private map: Map<TKey,{ value: any, expire: number, ttl: number, dispose?: (item: any) => any }> = new Map();
|
|
4
|
+
|
|
5
|
+
constructor(private ttl = 15000) {
|
|
6
|
+
// intentional
|
|
7
|
+
setInterval((x) => x.clear(), this.ttl, this);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
delete(key: any) {
|
|
11
|
+
this.map.delete(key);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getOrCreate(key: TKey, factory: (k: TKey) => T, ttl: number = 15000) {
|
|
15
|
+
let item = this.map.get(key);
|
|
16
|
+
if (!item) {
|
|
17
|
+
item = { value: factory(key), ttl, expire: Date.now() + ttl };
|
|
18
|
+
this.map.set(key, item);
|
|
19
|
+
} else {
|
|
20
|
+
item.expire = Date.now() + ttl;
|
|
21
|
+
}
|
|
22
|
+
return item.value as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getOrCreateAsync(
|
|
26
|
+
key: TKey,
|
|
27
|
+
factory: (k: TKey) => Promise<T>,
|
|
28
|
+
ttl: number = 15000,
|
|
29
|
+
dispose?: ((item: T) => any)
|
|
30
|
+
): Promise<T> {
|
|
31
|
+
let item = this.map.get(key);
|
|
32
|
+
if (!item) {
|
|
33
|
+
item = { value: factory(key), ttl, expire: Date.now() + ttl, dispose };
|
|
34
|
+
this.map.set(key, item);
|
|
35
|
+
// we need to make sure we do not cache
|
|
36
|
+
// the promise if it fails
|
|
37
|
+
item.value.catch(() => {
|
|
38
|
+
this.map.delete(key);
|
|
39
|
+
});
|
|
40
|
+
if (dispose) {
|
|
41
|
+
item.value.then((r) => {
|
|
42
|
+
item.dispose = () => dispose(r);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
item.expire = Date.now() + ttl;
|
|
47
|
+
}
|
|
48
|
+
return item.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private clear(): void {
|
|
52
|
+
const expired = [];
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const [key, value] of this.map.entries()) {
|
|
55
|
+
if(value.expire < now) {
|
|
56
|
+
expired.push(key);
|
|
57
|
+
// call dispose..
|
|
58
|
+
try {
|
|
59
|
+
const r = value.dispose?.(value.value);
|
|
60
|
+
if (r?.then) {
|
|
61
|
+
r.catch(() => void 0);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const key of expired) {
|
|
69
|
+
this.map.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
export interface IDisposable {
|
|
3
|
+
|
|
4
|
+
end?():any;
|
|
5
|
+
dispose?(): any;
|
|
6
|
+
close?(): any;
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export default async function usingAsync<T extends IDisposable>(d: T, fx: (p:T) => Promise<any>) {
|
|
12
|
+
try {
|
|
13
|
+
const r = fx(d);
|
|
14
|
+
if (r?.then) {
|
|
15
|
+
await r;
|
|
16
|
+
}
|
|
17
|
+
return r;
|
|
18
|
+
} finally {
|
|
19
|
+
const r = d.dispose?.() ?? d.end?.() ?? d.close?.();
|
|
20
|
+
if (r?.then) {
|
|
21
|
+
await r;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ISql } from "../sql/ISql.js";
|
|
2
|
+
|
|
3
|
+
type IFunction = ( ... a: any[]) => any;
|
|
4
|
+
|
|
5
|
+
type others = Omit<ISql, "in">;
|
|
6
|
+
|
|
7
|
+
type IStringReturn<T extends IFunction> = (... p: Parameters<T>) => string;
|
|
8
|
+
|
|
9
|
+
type transformed<T> = {
|
|
10
|
+
[P in keyof T]: T[P] extends IFunction ? IStringReturn<T[P]> : transformed<T[P]>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ISqlHelpers = transformed<ISql>;
|
|
14
|
+
|
|
15
|
+
export const flattenHelpers = (f, name, target = {}) => {
|
|
16
|
+
for (const key in f) {
|
|
17
|
+
if (Object.prototype.hasOwnProperty.call(f, key)) {
|
|
18
|
+
const element = f[key];
|
|
19
|
+
if (typeof element === "function") {
|
|
20
|
+
target[name + "." + key] = element;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (typeof element !== "object") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
flattenHelpers(element, name + "." + key, target);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return target;
|
|
30
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { IClassOf } from "../decorators/IClassOf.js";
|
|
2
|
+
import ExpressionToSql from "../query/ast/ExpressionToSql.js";
|
|
3
|
+
import { ISqlMethodTransformer, IStringTransformer, ITextOrFunctionArray } from "../query/ast/IStringTransformer.js";
|
|
4
|
+
import { Expression, PlaceholderExpression } from "../query/ast/Expressions.js";
|
|
5
|
+
import SqlLiteral from "../query/ast/SqlLiteral.js";
|
|
6
|
+
import ArrowToExpression from "../query/parser/ArrowToExpression.js";
|
|
7
|
+
import PostgreSqlMethodTransformer from "./postgres/PostgreSqlMethodTransformer.js";
|
|
8
|
+
import EntityType from "../entity-query/EntityType.js";
|
|
9
|
+
import { EntitySource } from "../model/EntitySource.js";
|
|
10
|
+
import { IEntityQuery } from "../model/IFilterWithParameter.js";
|
|
11
|
+
import { SourceExpression } from "../model/SourceExpression.js";
|
|
12
|
+
|
|
13
|
+
export default class QueryCompiler {
|
|
14
|
+
|
|
15
|
+
public static instance = new QueryCompiler();
|
|
16
|
+
|
|
17
|
+
public readonly arrowToExpression: typeof ArrowToExpression;
|
|
18
|
+
public readonly expressionToSql: typeof ExpressionToSql;
|
|
19
|
+
|
|
20
|
+
public readonly quotedLiteral: IStringTransformer;
|
|
21
|
+
public readonly escapeLiteral: IStringTransformer;
|
|
22
|
+
|
|
23
|
+
public readonly sqlMethodTransformer: ISqlMethodTransformer;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
{
|
|
27
|
+
arrowToExpression = ArrowToExpression,
|
|
28
|
+
expressionToSql = ExpressionToSql,
|
|
29
|
+
quotedLiteral = JSON.stringify,
|
|
30
|
+
escapeLiteral = SqlLiteral.escapeLiteral,
|
|
31
|
+
sqlMethodTransformer = PostgreSqlMethodTransformer
|
|
32
|
+
}: Partial<QueryCompiler> = {}
|
|
33
|
+
) {
|
|
34
|
+
this.arrowToExpression = arrowToExpression;
|
|
35
|
+
this.expressionToSql = expressionToSql;
|
|
36
|
+
this.escapeLiteral = escapeLiteral;
|
|
37
|
+
this.quotedLiteral = quotedLiteral;
|
|
38
|
+
this.sqlMethodTransformer = sqlMethodTransformer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public execute<P = any, T = any>(parameters: P, fx: (p: P) => (x: T) => any, source?: SourceExpression) {
|
|
42
|
+
const { param, target , body } = this.arrowToExpression.transform(fx);
|
|
43
|
+
const exp = new this.expressionToSql(source, param, target, this);
|
|
44
|
+
const query = exp.visit(body);
|
|
45
|
+
return this.invoke(query, parameters);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public compileToExpression(source: SourceExpression, p: any, fx: (p1) => (x) => any) {
|
|
49
|
+
const { param, target , body } = this.arrowToExpression.transform(fx);
|
|
50
|
+
const exp = new this.expressionToSql(source, param, target, this);
|
|
51
|
+
const visited = exp.visit(body);
|
|
52
|
+
return PlaceholderExpression.create({
|
|
53
|
+
expression: () => visited.map((x) => typeof x === "function" ? () => x(p) : x)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public compile( source: SourceExpression , fx: (p) => (x) => any) {
|
|
58
|
+
const { param, target , body } = this.arrowToExpression.transform(fx);
|
|
59
|
+
const exp = new this.expressionToSql(source, param, target, this);
|
|
60
|
+
return exp.visit(body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public compileExpression(exp: Expression) {
|
|
64
|
+
const toSql = new this.expressionToSql(null, void 0, void 0, this);
|
|
65
|
+
const query = toSql.visit(exp);
|
|
66
|
+
return this.invoke(query);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private invoke(query: ITextOrFunctionArray, p: any = {}) {
|
|
70
|
+
let text = "";
|
|
71
|
+
const values = [];
|
|
72
|
+
for (const iterator of query) {
|
|
73
|
+
if (typeof iterator === "string") {
|
|
74
|
+
text += iterator;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let value = iterator;
|
|
78
|
+
if (typeof iterator === "function") {
|
|
79
|
+
value = iterator(p);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
values.push(value);
|
|
83
|
+
text += "$" + values.length;
|
|
84
|
+
}
|
|
85
|
+
return { text, values };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
import { prepareAny } from "../../query/ast/IStringTransformer.js";
|
|
3
|
+
import { ISqlHelpers, flattenHelpers } from "../ISqlHelpers.js";
|
|
4
|
+
|
|
5
|
+
export const PostgreSqlHelper: ISqlHelpers = {
|
|
6
|
+
in(a, array) {
|
|
7
|
+
return prepareAny `${a} IN ${array}`;
|
|
8
|
+
},
|
|
9
|
+
date: {
|
|
10
|
+
addDays(d, n) {
|
|
11
|
+
return prepareAny `(${d} + (${n} * interval '1 day'))`;
|
|
12
|
+
},
|
|
13
|
+
addHours(d, n) {
|
|
14
|
+
return prepareAny `(${d} + (${n} * interval '1 hour'))`;
|
|
15
|
+
},
|
|
16
|
+
addMinutes(d, n) {
|
|
17
|
+
return prepareAny `(${d} + (${n} * interval '1 minute'))`;
|
|
18
|
+
},
|
|
19
|
+
addMonths(d, n) {
|
|
20
|
+
return prepareAny `(${d} + (${n} * interval '1 month'))`;
|
|
21
|
+
},
|
|
22
|
+
addSeconds(d, n) {
|
|
23
|
+
return prepareAny `(${d} + (${n} * interval '1 second'))`;
|
|
24
|
+
},
|
|
25
|
+
addYears(d, n) {
|
|
26
|
+
return prepareAny `(${d} + (${n} * interval '1 year'))`;
|
|
27
|
+
},
|
|
28
|
+
dayOf(d) {
|
|
29
|
+
return prepareAny `DATE_PART(${d}, day)`;
|
|
30
|
+
},
|
|
31
|
+
hourOf(d) {
|
|
32
|
+
return prepareAny `DATE_PART(${d}, hour)`;
|
|
33
|
+
},
|
|
34
|
+
minuteOf(d) {
|
|
35
|
+
return prepareAny `DATE_PART(${d}, minute)`;
|
|
36
|
+
},
|
|
37
|
+
monthOf(d) {
|
|
38
|
+
return prepareAny `DATE_PART(${d}, month)`;
|
|
39
|
+
},
|
|
40
|
+
secondOf(d) {
|
|
41
|
+
return prepareAny `DATE_PART(${d}, second)`;
|
|
42
|
+
},
|
|
43
|
+
yearOf(d) {
|
|
44
|
+
return prepareAny `DATE_PART(${d}, year)`;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
concat(...p) {
|
|
49
|
+
return prepareAny `CONCAT(${p.join(",")})`;
|
|
50
|
+
},
|
|
51
|
+
endsWith(text, test) {
|
|
52
|
+
return prepareAny `strpos(${text}, ${test}) = length(${text}) - length(${test})`;
|
|
53
|
+
},
|
|
54
|
+
iLike(text, test) {
|
|
55
|
+
return prepareAny `(${text} iLike ${test})`;
|
|
56
|
+
},
|
|
57
|
+
indexOf(text, test) {
|
|
58
|
+
return prepareAny `strpos(${text}, ${test})`;
|
|
59
|
+
},
|
|
60
|
+
left(text, length) {
|
|
61
|
+
return prepareAny `left(${text}, ${length})`;
|
|
62
|
+
},
|
|
63
|
+
like(text, test) {
|
|
64
|
+
return prepareAny `(${text} LIKE ${test})`;
|
|
65
|
+
},
|
|
66
|
+
right(text, length) {
|
|
67
|
+
return prepareAny `right(${text}, ${length})`;
|
|
68
|
+
},
|
|
69
|
+
startsWith(text, test) {
|
|
70
|
+
return prepareAny `starts_with(${text}, ${test})`;
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const names = flattenHelpers(PostgreSqlHelper, "Sql");
|
|
76
|
+
|
|
77
|
+
export default function PostgreSqlMethodTransformer(callee: string, args: any[]): string {
|
|
78
|
+
const name = names[callee];
|
|
79
|
+
if (!name) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
return names[callee]?.(... args);
|
|
83
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
import { prepareAny } from "../../query/ast/IStringTransformer.js";
|
|
3
|
+
import { ISqlHelpers, flattenHelpers } from "../ISqlHelpers.js";
|
|
4
|
+
|
|
5
|
+
export const SqlServerSqlHelper: ISqlHelpers = {
|
|
6
|
+
in(a, array) {
|
|
7
|
+
return prepareAny `${a} IN ${array}`;
|
|
8
|
+
},
|
|
9
|
+
date: {
|
|
10
|
+
addDays(d, n) {
|
|
11
|
+
return prepareAny `DateAdd(DAY, ${d}, ${n})`;
|
|
12
|
+
},
|
|
13
|
+
addHours(d, n) {
|
|
14
|
+
return prepareAny `DateAdd(HOUR, ${d}, ${n})`;
|
|
15
|
+
},
|
|
16
|
+
addMinutes(d, n) {
|
|
17
|
+
return prepareAny `DateAdd(MINUTE, ${d}, ${n})`;
|
|
18
|
+
},
|
|
19
|
+
addMonths(d, n) {
|
|
20
|
+
return prepareAny `DateAdd(MONTH, ${d}, ${n})`;
|
|
21
|
+
},
|
|
22
|
+
addSeconds(d, n) {
|
|
23
|
+
return prepareAny `DateAdd(SECOND, ${d}, ${n})`;
|
|
24
|
+
},
|
|
25
|
+
addYears(d, n) {
|
|
26
|
+
return prepareAny `DateAdd(YEAR, ${d}, ${n})`;
|
|
27
|
+
},
|
|
28
|
+
dayOf(d) {
|
|
29
|
+
return prepareAny `DATE_PART(day, ${d})`;
|
|
30
|
+
},
|
|
31
|
+
hourOf(d) {
|
|
32
|
+
return prepareAny `DATE_PART(hour, ${d})`;
|
|
33
|
+
},
|
|
34
|
+
minuteOf(d) {
|
|
35
|
+
return prepareAny `DATE_PART(minute, ${d})`;
|
|
36
|
+
},
|
|
37
|
+
monthOf(d) {
|
|
38
|
+
return prepareAny `DATE_PART(month, ${d})`;
|
|
39
|
+
},
|
|
40
|
+
secondOf(d) {
|
|
41
|
+
return prepareAny `DATE_PART(second, ${d})`;
|
|
42
|
+
},
|
|
43
|
+
yearOf(d) {
|
|
44
|
+
return prepareAny `DATE_PART(year, ${d})`;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
text: {
|
|
48
|
+
concat(...p) {
|
|
49
|
+
return prepareAny `CONCAT(${p.join(",")})`;
|
|
50
|
+
},
|
|
51
|
+
endsWith(text, test) {
|
|
52
|
+
return prepareAny `CHARINDEX(${text}, ${test}) = LEN(${text}) - LEN(${test})`;
|
|
53
|
+
},
|
|
54
|
+
iLike(text, test) {
|
|
55
|
+
return prepareAny `(${text} like ${test})`;
|
|
56
|
+
},
|
|
57
|
+
indexOf(text, test) {
|
|
58
|
+
return prepareAny `CHARINDEX(${text}, ${test})`;
|
|
59
|
+
},
|
|
60
|
+
left(text, length) {
|
|
61
|
+
return prepareAny `left(${text}, ${length})`;
|
|
62
|
+
},
|
|
63
|
+
like(text, test) {
|
|
64
|
+
return prepareAny `(${text} LIKE ${test})`;
|
|
65
|
+
},
|
|
66
|
+
right(text, length) {
|
|
67
|
+
return prepareAny `right(${text}, ${length})`;
|
|
68
|
+
},
|
|
69
|
+
startsWith(text, test) {
|
|
70
|
+
return prepareAny `CHARINDEX(${text}, ${test}) = 1`;
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const names = flattenHelpers(SqlServerSqlHelper, "Sql");
|
|
76
|
+
|
|
77
|
+
export default function SqlServerSqlMethodTransformer(callee: string, args: any[]): string {
|
|
78
|
+
const name = names[callee];
|
|
79
|
+
if (!name) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
return names[callee]?.(... args);
|
|
83
|
+
}
|