@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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { IColumn } from "../../decorators/IColumn.js";
|
|
2
|
+
import Migrations from "../Migrations.js";
|
|
3
|
+
|
|
4
|
+
export default abstract class SqlServerMigrations extends Migrations {
|
|
5
|
+
|
|
6
|
+
protected getColumnDefinition(iterator: IColumn) {
|
|
7
|
+
if (iterator.dataType === "Decimal") {
|
|
8
|
+
if (iterator.precision && iterator.scale) {
|
|
9
|
+
return `decimal (${iterator.precision}, ${iterator.scale})`;
|
|
10
|
+
}
|
|
11
|
+
return `decimal (18,2)`;
|
|
12
|
+
}
|
|
13
|
+
const type = this.getColumnType(iterator);
|
|
14
|
+
if (iterator.length) {
|
|
15
|
+
return `${type} (${iterator.length})`;
|
|
16
|
+
}
|
|
17
|
+
return type;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected getColumnType(iterator: IColumn) {
|
|
21
|
+
switch(iterator.dataType) {
|
|
22
|
+
case "BigInt":
|
|
23
|
+
return "bigint";
|
|
24
|
+
case "AsciiChar":
|
|
25
|
+
if (!iterator.length) {
|
|
26
|
+
return "varchar(max)";
|
|
27
|
+
}
|
|
28
|
+
return "varchar";
|
|
29
|
+
case "Char":
|
|
30
|
+
if (!iterator.length) {
|
|
31
|
+
return "nvarchar(max)";
|
|
32
|
+
}
|
|
33
|
+
return "nvarchar";
|
|
34
|
+
case "DateTime":
|
|
35
|
+
return "DateTime2";
|
|
36
|
+
case "DateTimeOffset":
|
|
37
|
+
return "DateTimeOffset";
|
|
38
|
+
case "Double":
|
|
39
|
+
return "float";
|
|
40
|
+
case "Float":
|
|
41
|
+
return "real";
|
|
42
|
+
case "Int":
|
|
43
|
+
return "int";
|
|
44
|
+
case "Boolean":
|
|
45
|
+
return "bit";
|
|
46
|
+
case "ByteArray":
|
|
47
|
+
return "varbinary";
|
|
48
|
+
case "Decimal":
|
|
49
|
+
return "decimal";
|
|
50
|
+
case "JSON":
|
|
51
|
+
return "json";
|
|
52
|
+
case "JSONB":
|
|
53
|
+
return "jsonb";
|
|
54
|
+
case "UUID":
|
|
55
|
+
return "UniqueIdentifier";
|
|
56
|
+
}
|
|
57
|
+
const a: never = iterator.dataType;
|
|
58
|
+
throw new Error("Not Defined");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { IColumn } from "../decorators/IColumn.js";
|
|
2
|
+
import EntityType from "../entity-query/EntityType.js";
|
|
3
|
+
import type ChangeSet from "./ChangeSet.js";
|
|
4
|
+
import { privateUpdateEntry } from "./ChangeSet.js";
|
|
5
|
+
|
|
6
|
+
export interface IChanges {
|
|
7
|
+
type: EntityType;
|
|
8
|
+
entity: any;
|
|
9
|
+
order: number;
|
|
10
|
+
original: any;
|
|
11
|
+
status: "detached" | "attached" | "inserted" | "modified" | "deleted" | "unchanged";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IChange {
|
|
15
|
+
column: IColumn;
|
|
16
|
+
oldValue: any;
|
|
17
|
+
newValue: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default class ChangeEntry implements IChanges {
|
|
21
|
+
|
|
22
|
+
type: EntityType;
|
|
23
|
+
entity: any;
|
|
24
|
+
order: number;
|
|
25
|
+
original: any;
|
|
26
|
+
status: "detached" | "attached" | "inserted" | "modified" | "deleted" | "unchanged";
|
|
27
|
+
|
|
28
|
+
modified: Map<IColumn, IChange>;
|
|
29
|
+
|
|
30
|
+
changeSet: ChangeSet;
|
|
31
|
+
|
|
32
|
+
private pending: (() => void)[];
|
|
33
|
+
|
|
34
|
+
constructor(p: IChanges, changeSet: ChangeSet) {
|
|
35
|
+
Object.setPrototypeOf(p, ChangeEntry.prototype);
|
|
36
|
+
const ce = p as ChangeEntry;
|
|
37
|
+
ce.changeSet = changeSet;
|
|
38
|
+
ce.pending = [];
|
|
39
|
+
ce.modified = new Map();
|
|
40
|
+
return ce;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public detect() {
|
|
44
|
+
|
|
45
|
+
const { type: { columns }, entity, original } = this;
|
|
46
|
+
|
|
47
|
+
if (original === void 0) {
|
|
48
|
+
this.status = "inserted";
|
|
49
|
+
this.detectDependencies();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.detectDependencies();
|
|
54
|
+
|
|
55
|
+
for (const iterator of columns) {
|
|
56
|
+
const oldValue = original[iterator.columnName];
|
|
57
|
+
const newValue = entity[iterator.name];
|
|
58
|
+
if (entity[iterator.name] !== original[iterator.columnName]) {
|
|
59
|
+
let modifiedEntry = this.modified.get(iterator);
|
|
60
|
+
if (!modifiedEntry) {
|
|
61
|
+
modifiedEntry = { column: iterator, oldValue, newValue };
|
|
62
|
+
this.modified.set(iterator, modifiedEntry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if (this.modified.size > 0) {
|
|
69
|
+
this.status = "modified";
|
|
70
|
+
} else {
|
|
71
|
+
this.status = "unchanged";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public apply(dbValues) {
|
|
76
|
+
// apply values to main entity
|
|
77
|
+
// set status to unchanged
|
|
78
|
+
|
|
79
|
+
// we will only apply the columns defined
|
|
80
|
+
if (dbValues !== void 0) {
|
|
81
|
+
for (const iterator of this.type.columns) {
|
|
82
|
+
const dbValue = dbValues[iterator.columnName];
|
|
83
|
+
if (dbValue !== void 0) {
|
|
84
|
+
this.entity[iterator.name] = dbValues[iterator.columnName];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const iterator of this.pending) {
|
|
90
|
+
iterator();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// we will set the identity key
|
|
94
|
+
this.changeSet[privateUpdateEntry](this);
|
|
95
|
+
|
|
96
|
+
this.pending.length = 0;
|
|
97
|
+
this.original = { ... this.entity };
|
|
98
|
+
this.status = "unchanged";
|
|
99
|
+
this.modified.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
detectDependencies() {
|
|
103
|
+
const { type: { relations }, entity } = this;
|
|
104
|
+
// for parent relations.. check if related key is set or not...
|
|
105
|
+
for (const iterator of relations) {
|
|
106
|
+
if (iterator.isCollection) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// get related entry..
|
|
111
|
+
const related = entity[iterator.name];
|
|
112
|
+
if (!related) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// if related has key defined.. set it...
|
|
117
|
+
const rKey = iterator.relatedEntity.keys[0];
|
|
118
|
+
|
|
119
|
+
const relatedChanges = this.changeSet.getEntry(related);
|
|
120
|
+
|
|
121
|
+
const keyValue = related[rKey.name];
|
|
122
|
+
if (keyValue === void 0) {
|
|
123
|
+
this.order++;
|
|
124
|
+
const fk = iterator;
|
|
125
|
+
relatedChanges.pending.push(() => {
|
|
126
|
+
this.entity[fk.fkColumn.name] = related[rKey.name];
|
|
127
|
+
});
|
|
128
|
+
this.modified.set(iterator, { column: iterator.fkColumn, oldValue: void 0, newValue: void 0});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.entity[iterator.fkColumn.name] = related[rKey.name];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setupInverseProperties() {
|
|
137
|
+
for (const iterator of this.type.relations) {
|
|
138
|
+
if (!iterator.isCollection) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const related = this.entity[iterator.name];
|
|
142
|
+
if (related === void 0) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(related)) {
|
|
146
|
+
for (const r of related) {
|
|
147
|
+
r[iterator.relatedName] = this.entity;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
related[iterator.relatedName] = this.entity;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type EntityContext from "./EntityContext.js";
|
|
2
|
+
import SchemaRegistry from "../decorators/SchemaRegistry.js";
|
|
3
|
+
import ChangeEntry from "./ChangeEntry.js";
|
|
4
|
+
import { IRecord } from "../drivers/base/BaseDriver.js";
|
|
5
|
+
import IdentityService from "./IdentityService.js";
|
|
6
|
+
|
|
7
|
+
const entrySymbol = Symbol("entry");
|
|
8
|
+
|
|
9
|
+
const identitySymbol = Symbol("identity");
|
|
10
|
+
|
|
11
|
+
const getEntityByIdentity = Symbol("getEntityByIdentity");
|
|
12
|
+
|
|
13
|
+
export const privateUpdateEntry = Symbol("updateEntry");
|
|
14
|
+
|
|
15
|
+
export default class ChangeSet {
|
|
16
|
+
|
|
17
|
+
public readonly entries: ChangeEntry[] = [];
|
|
18
|
+
|
|
19
|
+
private entryMap: Map<any, ChangeEntry> = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This will provide new entity for same key
|
|
23
|
+
*/
|
|
24
|
+
private identityMap: Map<string,ChangeEntry> = new Map();
|
|
25
|
+
|
|
26
|
+
private nextId = 1;
|
|
27
|
+
|
|
28
|
+
constructor(private context: EntityContext) {
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[privateUpdateEntry](entry: ChangeEntry) {
|
|
33
|
+
const jsonKey = IdentityService.getIdentity(entry.entity);
|
|
34
|
+
if (jsonKey) {
|
|
35
|
+
this.identityMap.set(jsonKey, entry.entity);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getEntry(entity, original = void 0): ChangeEntry {
|
|
40
|
+
let entry = this.entryMap.get(entity);
|
|
41
|
+
if (entry) {
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
const type = SchemaRegistry.model(Object.getPrototypeOf(entity).constructor);
|
|
45
|
+
const jsonKey = IdentityService.getIdentity(entity);
|
|
46
|
+
if (jsonKey) {
|
|
47
|
+
const existing = this.identityMap.get(jsonKey);
|
|
48
|
+
if (existing) {
|
|
49
|
+
entity = existing;
|
|
50
|
+
entry = this.entryMap.get(entity);
|
|
51
|
+
if (entry) {
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
this.identityMap.set(jsonKey, entity);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
entry = new ChangeEntry({
|
|
59
|
+
type,
|
|
60
|
+
entity,
|
|
61
|
+
order: 0,
|
|
62
|
+
original: original ? { ... original } : void 0,
|
|
63
|
+
status: "unchanged"
|
|
64
|
+
}, this);
|
|
65
|
+
entity[entrySymbol] = entry;
|
|
66
|
+
this.entries.push(entry);
|
|
67
|
+
this.entryMap.set(entity, entry);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detect changes will detect and sort the entries as they should be inserted.
|
|
73
|
+
*/
|
|
74
|
+
public detectChanges() {
|
|
75
|
+
|
|
76
|
+
for (const iterator of this.entries) {
|
|
77
|
+
iterator.setupInverseProperties();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const iterator of this.entries) {
|
|
81
|
+
iterator.detect();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.entries.sort((a, b) => a.order - b.order);
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseDriver } from "../drivers/base/BaseDriver.js";
|
|
2
|
+
import ChangeSet from "./ChangeSet.js";
|
|
3
|
+
import EntityModel from "./EntityModel.js";
|
|
4
|
+
import { Expression } from "../query/ast/Expressions.js";
|
|
5
|
+
import QueryCompiler from "../compiler/QueryCompiler.js";
|
|
6
|
+
|
|
7
|
+
export default class EntityContext {
|
|
8
|
+
|
|
9
|
+
public readonly model = new EntityModel(this);
|
|
10
|
+
public readonly changeSet = new ChangeSet(this);
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
public driver: BaseDriver
|
|
14
|
+
) {
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async saveChanges() {
|
|
19
|
+
|
|
20
|
+
this.changeSet.detectChanges();
|
|
21
|
+
|
|
22
|
+
await this.driver.runInTransaction(async () => {
|
|
23
|
+
for (const iterator of this.changeSet.entries) {
|
|
24
|
+
switch(iterator.status) {
|
|
25
|
+
case "inserted":
|
|
26
|
+
const insert = this.driver.createInsertExpression(iterator.type, iterator.entity);
|
|
27
|
+
const r = await this.executeExpression(insert);
|
|
28
|
+
iterator.apply(r);
|
|
29
|
+
break;
|
|
30
|
+
case "modified":
|
|
31
|
+
if (iterator.modified.size > 0) {
|
|
32
|
+
const update = this.driver.createUpdateExpression(iterator);
|
|
33
|
+
await this.executeExpression(update);
|
|
34
|
+
}
|
|
35
|
+
iterator.apply({});
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async executeExpression(expression: Expression) {
|
|
43
|
+
const { text, values } = this.driver.compiler.compileExpression(expression);
|
|
44
|
+
const r = await this.driver.executeQuery({ text, values });
|
|
45
|
+
return r.rows[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type EntityContext from "./EntityContext.js";
|
|
2
|
+
import { IClassOf } from "../decorators/IClassOf.js";
|
|
3
|
+
import SchemaRegistry from "../decorators/SchemaRegistry.js";
|
|
4
|
+
import EntityQuery from "./EntityQuery.js";
|
|
5
|
+
import { Expression } from "@babel/types";
|
|
6
|
+
import { EntitySource } from "./EntitySource.js";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export default class EntityModel {
|
|
11
|
+
|
|
12
|
+
public entities: Map<IClassOf<any>, EntitySource> = new Map();
|
|
13
|
+
|
|
14
|
+
constructor(private context: EntityContext) {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
register<T>(type: IClassOf<T>) {
|
|
18
|
+
let source = this.entities.get(type);
|
|
19
|
+
if (!source) {
|
|
20
|
+
const model = SchemaRegistry.model(type);
|
|
21
|
+
source = new EntitySource(model, this.context);
|
|
22
|
+
this.entities.set(type, source);
|
|
23
|
+
}
|
|
24
|
+
return source as EntitySource<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { BinaryExpression, CallExpression, Expression, ExpressionAs, Identifier, OrderByExpression, QuotedLiteral, SelectStatement, TableSource } from "../query/ast/Expressions.js";
|
|
2
|
+
import { EntitySource } from "./EntitySource.js";
|
|
3
|
+
import { IOrderedEntityQuery, IEntityQuery, ILambdaExpression } from "./IFilterWithParameter.js";
|
|
4
|
+
import { SourceExpression } from "./SourceExpression.js";
|
|
5
|
+
|
|
6
|
+
export default class EntityQuery<T = any>
|
|
7
|
+
implements IOrderedEntityQuery<T>, IEntityQuery<T> {
|
|
8
|
+
constructor (public readonly source: SourceExpression) {
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
withSignal(signal: AbortSignal): any {
|
|
12
|
+
const source = this.source.copy();
|
|
13
|
+
source.signal = signal;
|
|
14
|
+
return new EntityQuery(source);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
thenBy(parameters: any, fx: any): any {
|
|
18
|
+
return this.orderBy(parameters, fx);
|
|
19
|
+
}
|
|
20
|
+
thenByDescending(parameters: any, fx: any) {
|
|
21
|
+
return this.orderByDescending(parameters, fx);
|
|
22
|
+
}
|
|
23
|
+
where<P>(parameters: P, fx: (p: P) => (x: T) => boolean): any {
|
|
24
|
+
|
|
25
|
+
const source = this.source.copy();
|
|
26
|
+
const { select } = source;
|
|
27
|
+
const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx);
|
|
28
|
+
if(!select.where) {
|
|
29
|
+
select.where = exp;
|
|
30
|
+
} else {
|
|
31
|
+
select.where = BinaryExpression.create({
|
|
32
|
+
left: select.where,
|
|
33
|
+
operator: "AND",
|
|
34
|
+
right: exp
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return new EntityQuery(source);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async toArray(): Promise<T[]> {
|
|
41
|
+
const results: T[] = [];
|
|
42
|
+
for await (const iterator of this.enumerate()) {
|
|
43
|
+
results.push(iterator);
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async *enumerate(): AsyncGenerator<T, any, unknown> {
|
|
49
|
+
const type = this.source.model?.typeClass;
|
|
50
|
+
const signal = this.source.signal;
|
|
51
|
+
const query = this.source.context.driver.compiler.compileExpression(this.source.select);
|
|
52
|
+
const reader = await this.source.context.driver.executeReader(query, signal);
|
|
53
|
+
try {
|
|
54
|
+
for await (const iterator of reader.next(10, signal)) {
|
|
55
|
+
if (type) {
|
|
56
|
+
Object.setPrototypeOf(iterator, type.prototype);
|
|
57
|
+
// set identity...
|
|
58
|
+
const entry = this.source.context.changeSet.getEntry(iterator, iterator);
|
|
59
|
+
yield entry.entity;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
yield iterator as T;
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await reader.dispose();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async firstOrFail(): Promise<T> {
|
|
70
|
+
for await(const iterator of this.limit(1).enumerate()) {
|
|
71
|
+
return iterator;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`No records found for ${this.source.model?.name || "Table"}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async first(): Promise<T> {
|
|
77
|
+
for await(const iterator of this.limit(1).enumerate()) {
|
|
78
|
+
return iterator;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
toQuery(): { text: string; values: any[]; } {
|
|
83
|
+
return this.source.context.driver.compiler.compileExpression(this.source.select);
|
|
84
|
+
}
|
|
85
|
+
orderBy(parameters: any, fx: any): any {
|
|
86
|
+
const source = this.source.copy();
|
|
87
|
+
const { select } = source;
|
|
88
|
+
const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx as any);
|
|
89
|
+
select.orderBy ??= [];
|
|
90
|
+
select.orderBy.push(OrderByExpression.create({
|
|
91
|
+
target: exp
|
|
92
|
+
}));
|
|
93
|
+
return new EntityQuery(source);
|
|
94
|
+
}
|
|
95
|
+
orderByDescending(parameters: any, fx: any): any {
|
|
96
|
+
const source = this.source.copy();
|
|
97
|
+
const { select } = source;
|
|
98
|
+
const exp = this.source.context.driver.compiler.compileToExpression(source, parameters, fx as any);
|
|
99
|
+
select.orderBy ??= [];
|
|
100
|
+
select.orderBy.push(OrderByExpression.create({
|
|
101
|
+
target: exp,
|
|
102
|
+
descending: true
|
|
103
|
+
}));
|
|
104
|
+
return new EntityQuery(source);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
limit(n: number): any {
|
|
108
|
+
const source = this.source.copy();
|
|
109
|
+
const { select } = source;
|
|
110
|
+
select.limit = n;
|
|
111
|
+
return new EntityQuery(source);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
offset(n: number): any {
|
|
115
|
+
const source = this.source.copy();
|
|
116
|
+
const { select } = source;
|
|
117
|
+
select.offset = n;
|
|
118
|
+
return new EntityQuery(source);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async count(parameters?:any, fx?: any): Promise<number> {
|
|
122
|
+
if (parameters !== void 0) {
|
|
123
|
+
return this.where(parameters, fx).count();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const source = this.source.copy();
|
|
127
|
+
const { select } = source;
|
|
128
|
+
|
|
129
|
+
select.fields = [
|
|
130
|
+
ExpressionAs.create({
|
|
131
|
+
expression: CallExpression.create({
|
|
132
|
+
callee: Identifier.create({ value: "COUNT"}),
|
|
133
|
+
arguments: [ Identifier.create({ value: "*"})]
|
|
134
|
+
}),
|
|
135
|
+
alias: QuotedLiteral.create({ literal: "count" })
|
|
136
|
+
})
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const query = this.source.context.driver.compiler.compileExpression(select);
|
|
140
|
+
const reader = await this.source.context.driver.executeReader(query);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
for await (const iterator of reader.next()) {
|
|
144
|
+
return iterator.count as number;
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
await reader.dispose();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type EntityContext from "./EntityContext.js";
|
|
2
|
+
import type EntityType from "../entity-query/EntityType.js";
|
|
3
|
+
import type { IEntityQuery, IFilterExpression } from "./IFilterWithParameter.js";
|
|
4
|
+
import { BinaryExpression, Expression, ExpressionAs, QuotedLiteral, SelectStatement, TableLiteral } from "../query/ast/Expressions.js";
|
|
5
|
+
import EntityQuery from "./EntityQuery.js";
|
|
6
|
+
import TimedCache from "../common/cache/TimedCache.js";
|
|
7
|
+
import { contextSymbol, modelSymbol } from "../common/symbols/symbols.js";
|
|
8
|
+
import { SourceExpression } from "./SourceExpression.js";
|
|
9
|
+
|
|
10
|
+
const modelCache = new TimedCache<any, SelectStatement>();
|
|
11
|
+
|
|
12
|
+
export class EntitySource<T = any> {
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
public readonly filters: {
|
|
16
|
+
read?: () => IFilterExpression;
|
|
17
|
+
modify?: () => IFilterExpression;
|
|
18
|
+
delete?: () => IFilterExpression;
|
|
19
|
+
include?: () => IFilterExpression;
|
|
20
|
+
} = {};
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
get [modelSymbol]() {
|
|
24
|
+
return this.model;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get[contextSymbol]() {
|
|
28
|
+
return this.context;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private filter: Expression;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
private readonly model: EntityType,
|
|
35
|
+
private readonly context: EntityContext
|
|
36
|
+
) {
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public add(item: Partial<T>) {
|
|
41
|
+
const p = Object.getPrototypeOf(item).constructor;
|
|
42
|
+
if (!p || p === Object) {
|
|
43
|
+
Object.setPrototypeOf(item, this.model.typeClass.prototype);
|
|
44
|
+
}
|
|
45
|
+
const entry = this.context.changeSet.getEntry(item);
|
|
46
|
+
if (entry.status !== "detached" && entry.status !== "unchanged") {
|
|
47
|
+
throw new Error("Entity is already attached to the context");
|
|
48
|
+
}
|
|
49
|
+
entry.status = "inserted";
|
|
50
|
+
return item as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public all(): IEntityQuery<T> {
|
|
54
|
+
const { model, context } = this;
|
|
55
|
+
const select = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
|
|
56
|
+
return new EntityQuery<T>(SourceExpression.create({
|
|
57
|
+
alias: select.as.literal,
|
|
58
|
+
context,
|
|
59
|
+
model,
|
|
60
|
+
select
|
|
61
|
+
})) as IEntityQuery<T>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
|
|
65
|
+
const { model, context } = this;
|
|
66
|
+
const select = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
|
|
67
|
+
return new EntityQuery<T>(SourceExpression.create({
|
|
68
|
+
alias: select.as.literal,
|
|
69
|
+
context,
|
|
70
|
+
model,
|
|
71
|
+
select
|
|
72
|
+
})).where(parameter, fx) as IEntityQuery<T>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
generateModel(): SelectStatement {
|
|
76
|
+
const source = this.model.fullyQualifiedName;
|
|
77
|
+
const as = QuotedLiteral.create({ literal: this.model.name[0] + "1" });
|
|
78
|
+
const fields = this.model.columns.map((c) => c.name !== c.columnName
|
|
79
|
+
? ExpressionAs.create({
|
|
80
|
+
expression: QuotedLiteral.propertyChain(as.literal, c.columnName),
|
|
81
|
+
alias: QuotedLiteral.create({ literal: c.name })
|
|
82
|
+
})
|
|
83
|
+
: QuotedLiteral.propertyChain(as.literal, c.columnName));
|
|
84
|
+
return SelectStatement.create({
|
|
85
|
+
source,
|
|
86
|
+
as,
|
|
87
|
+
fields,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public toQuery() {
|
|
92
|
+
const filter = this.filter;
|
|
93
|
+
if(!filter) {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
return this.context.driver.compiler.compileExpression(filter);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
export type IFilterWithParameter<P = any, T = any> = (p: P) => (x: T) => boolean;
|
|
3
|
+
|
|
4
|
+
export type ILambdaExpression<P = any, T = any, TR = any> = [input: P, x: (p: P) => (s: T) => TR];
|
|
5
|
+
|
|
6
|
+
export type IFilterExpression<P = any, T = any> = [input: P, x: (p: P) => (s: T) => boolean];
|
|
7
|
+
|
|
8
|
+
export interface IBaseQuery<T> {
|
|
9
|
+
enumerate(): AsyncGenerator<T>;
|
|
10
|
+
|
|
11
|
+
firstOrFail(): Promise<T>;
|
|
12
|
+
first(): Promise<T>;
|
|
13
|
+
|
|
14
|
+
toArray(): Promise<T[]>;
|
|
15
|
+
|
|
16
|
+
toQuery(): { text: string, values: any[]};
|
|
17
|
+
|
|
18
|
+
limit<DT>(this: DT, limit: number): DT;
|
|
19
|
+
offset<DT>(this: DT, offset: number): DT;
|
|
20
|
+
where<P, DT>(this: DT, parameters: P, fx: (p: P) => (x: T) => boolean): DT;
|
|
21
|
+
|
|
22
|
+
count(): Promise<number>;
|
|
23
|
+
count<P>(parameters: P, fx: (p: P) => (x: T) => boolean): Promise<number>;
|
|
24
|
+
|
|
25
|
+
withSignal<DT>(this:DT, signal: AbortSignal): DT;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IOrderedEntityQuery<T> extends IBaseQuery<T> {
|
|
29
|
+
|
|
30
|
+
thenBy<P>(parameters: P, fx: (p: P) => (x: T) => any);
|
|
31
|
+
thenByDescending<P>(parameters: P, fx: (p: P) => (x: T) => any);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IEntityQuery<T> extends IBaseQuery<T> {
|
|
35
|
+
|
|
36
|
+
orderBy<P>(parameters: P, fx: (p: P) => (x: T) => any): IOrderedEntityQuery<T>;
|
|
37
|
+
orderByDescending<P>(parameters: P, fx: (p: P) => (x: T) => any): IOrderedEntityQuery<T>;
|
|
38
|
+
}
|