@entity-access/entity-access 1.0.5 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entity-access/entity-access",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,10 +11,10 @@ export default class TimedCache<TKey = any, T = any> {
11
11
  this.map.delete(key);
12
12
  }
13
13
 
14
- getOrCreate(key: TKey, factory: (k: TKey) => T, ttl: number = 15000) {
14
+ getOrCreate<TP>(key: TKey, p1: TP, factory: (k: TKey,p: TP) => T, ttl: number = 15000) {
15
15
  let item = this.map.get(key);
16
16
  if (!item) {
17
- item = { value: factory(key), ttl, expire: Date.now() + ttl };
17
+ item = { value: factory(key, p1), ttl, expire: Date.now() + ttl };
18
18
  this.map.set(key, item);
19
19
  } else {
20
20
  item.expire = Date.now() + ttl;
@@ -45,6 +45,8 @@ export interface IEntityRelation {
45
45
  */
46
46
  name: string;
47
47
 
48
+ isInverseRelation?: boolean;
49
+
48
50
  isCollection?: boolean;
49
51
 
50
52
 
@@ -47,25 +47,11 @@ export default class ExpressionToSqlServer extends ExpressionToSql {
47
47
 
48
48
  visitSelectStatement(e: SelectStatement): ITextQuery {
49
49
 
50
- if (e.as && e.model) {
51
- const scope = this.targets.get(e.as);
52
- if (!scope) {
53
- this.targets.set(e.as, {
54
- parameter: e.as,
55
- model:
56
- e.model,
57
- replace: e.as,
58
- selectStatement: e
59
- });
60
- } else {
61
- scope.selectStatement = e;
62
- scope.model = e.model;
63
- }
64
- }
50
+ this.prepareStatement(e);
65
51
 
66
52
  const orderBy = e.orderBy?.length > 0 ? prepare `\n\t\tORDER BY ${this.visitArray(e.orderBy)}` : "";
67
53
  const where = e.where ? prepare `\n\tWHERE ${this.visit(e.where)}` : "";
68
- const joins = e.joins?.length > 0 ? prepare `\n\t\t${this.visitArray(e.joins)}` : [];
54
+ const joins = e.joins?.length > 0 ? prepare `\n\t\t${this.visitArray(e.joins, "\n")}` : [];
69
55
 
70
56
  const fields = this.visitArray(e.fields, ",\n\t\t");
71
57
 
@@ -3,7 +3,7 @@ import { IClassOf } from "../decorators/IClassOf.js";
3
3
  import { Query } from "../query/Query.js";
4
4
  import NameParser from "../decorators/parser/NameParser.js";
5
5
  import SchemaRegistry from "../decorators/SchemaRegistry.js";
6
- import { QuotedLiteral, TableLiteral } from "../query/ast/Expressions.js";
6
+ import { Expression, ExpressionAs, QuotedLiteral, SelectStatement, TableLiteral } from "../query/ast/Expressions.js";
7
7
  import InstanceCache from "../common/cache/InstanceCache.js";
8
8
 
9
9
 
@@ -40,6 +40,9 @@ export default class EntityType {
40
40
  private columnMap: Map<string, IColumn> = new Map();
41
41
  private relationMap: Map<string, IEntityRelation> = new Map();
42
42
 
43
+ private selectAll: SelectStatement;
44
+ private selectOne: SelectStatement;
45
+
43
46
  public getProperty(name: string) {
44
47
  const field = this.fieldMap.get(name);
45
48
  const relation = this.relationMap.get(name);
@@ -103,14 +106,53 @@ export default class EntityType {
103
106
  relatedTypeClass: this.typeClass,
104
107
  dotNotCreateIndex: true,
105
108
  fkColumn,
106
- isCollection: true,
109
+ isInverseRelation: true,
107
110
  relatedRelation: relation,
108
111
  relatedEntity: this
109
112
  };
110
113
  relatedType.relationMap.set(inverseRelation.name, inverseRelation);
111
114
  relatedType.relations.push(inverseRelation);
112
115
  inverseRelation.relatedRelation = relation;
116
+ relation.relatedRelation = inverseRelation;
113
117
 
114
118
  }
115
119
 
120
+ public selectAllFields() {
121
+ if (this.selectAll) {
122
+ return { ... this.selectAll };
123
+ }
124
+ const source = this.fullyQualifiedName;
125
+ const as = Expression.parameter(this.name[0] + "1");
126
+ const fields = this.columns.map((c) => c.name !== c.columnName
127
+ ? ExpressionAs.create({
128
+ expression: Expression.member(as, c.columnName),
129
+ alias: QuotedLiteral.create({ literal: c.name })
130
+ })
131
+ : Expression.member(as, c.columnName));
132
+ this.selectAll = SelectStatement.create({
133
+ source,
134
+ model: this,
135
+ as,
136
+ fields
137
+ });
138
+ return { ... this.selectAll };
139
+ }
140
+
141
+ public selectOneNumber() {
142
+ if (this.selectOne) {
143
+ return { ... this.selectOne };
144
+ }
145
+ const source = this.fullyQualifiedName;
146
+ const as = Expression.parameter(this.name[0] + "1");
147
+ const fields = [
148
+ Expression.identifier("1")
149
+ ];
150
+ this.selectOne = SelectStatement.create({
151
+ source,
152
+ model: this,
153
+ as,
154
+ fields
155
+ });
156
+ return { ... this.selectOne };
157
+ }
116
158
  }
@@ -3,8 +3,10 @@ import { DisposableScope } from "../common/usingAsync.js";
3
3
  import { ServiceProvider } from "../di/di.js";
4
4
  import EntityType from "../entity-query/EntityType.js";
5
5
  import { CallExpression, Expression, ExpressionAs, Identifier, OrderByExpression, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
6
+ import { QueryExpander } from "../query/expander/QueryExpander.js";
6
7
  import EntityContext from "./EntityContext.js";
7
8
  import { IOrderedEntityQuery, IEntityQuery } from "./IFilterWithParameter.js";
9
+ import RelationMapper from "./identity/RelationMapper.js";
8
10
 
9
11
  export default class EntityQuery<T = any>
10
12
  implements IOrderedEntityQuery<T>, IEntityQuery<T> {
@@ -49,6 +51,14 @@ export default class EntityQuery<T = any>
49
51
  }));
50
52
  }
51
53
 
54
+ include(p: any): any {
55
+ const selectStatement = QueryExpander.expand(this.context, { ... this.selectStatement }, p);
56
+ return new EntityQuery({
57
+ ... this,
58
+ selectStatement
59
+ });
60
+ }
61
+
52
62
  async toArray(): Promise<T[]> {
53
63
  const results: T[] = [];
54
64
  for await (const iterator of this.enumerate()) {
@@ -66,6 +76,20 @@ export default class EntityQuery<T = any>
66
76
  scope.register(session);
67
77
  const type = this.type;
68
78
  const signal = this.signal;
79
+
80
+ const relationMapper = new RelationMapper(this.context.changeSet);
81
+
82
+ const include = this.selectStatement.include;
83
+ if (include?.length > 0) {
84
+ // since we will be streaming results...
85
+ // it is important that we load all the
86
+ // included entities first...
87
+ const loaders = include.map((x) => this.load(relationMapper, session, x, signal));
88
+ await Promise.all(loaders);
89
+ }
90
+
91
+ signal?.throwIfAborted();
92
+
69
93
  query = this.context.driver.compiler.compileExpression(this, this.selectStatement);
70
94
  const reader = await this.context.driver.executeReader(query, signal);
71
95
  scope.register(reader);
@@ -74,11 +98,13 @@ export default class EntityQuery<T = any>
74
98
  Object.setPrototypeOf(iterator, type.typeClass.prototype);
75
99
  // set identity...
76
100
  const entry = this.context.changeSet.getEntry(iterator, iterator);
101
+ relationMapper.fix(entry);
77
102
  yield entry.entity;
78
103
  continue;
79
104
  }
80
105
  yield iterator as T;
81
106
  }
107
+
82
108
  } catch(error) {
83
109
  session.error(`Failed executing ${query?.text}\n${error.stack ?? error}`);
84
110
  throw error;
@@ -87,6 +113,23 @@ export default class EntityQuery<T = any>
87
113
  }
88
114
  }
89
115
 
116
+ async load(relationMapper: RelationMapper, session: Logger, select: SelectStatement, signal: AbortSignal) {
117
+ const query = this.context.driver.compiler.compileExpression(this, select);
118
+ const reader = await this.context.driver.executeReader(query, signal);
119
+ try {
120
+ for await (const iterator of reader.next(10, signal)) {
121
+ Object.setPrototypeOf(iterator, select.model.typeClass.prototype);
122
+ const entry = this.context.changeSet.getEntry(iterator, iterator);
123
+ relationMapper.fix(entry);
124
+ }
125
+ } catch (error) {
126
+ session.error(`Failed loading ${query.text}\n${error.stack ?? error}`);
127
+ throw error;
128
+ } finally {
129
+ await reader.dispose();
130
+ }
131
+ }
132
+
90
133
  async firstOrFail(): Promise<T> {
91
134
  for await(const iterator of this.limit(1).enumerate()) {
92
135
  return iterator;
@@ -1,13 +1,10 @@
1
1
  import type EntityContext from "./EntityContext.js";
2
2
  import type EntityType from "../entity-query/EntityType.js";
3
3
  import type { IEntityQuery, IFilterExpression } from "./IFilterWithParameter.js";
4
- import { Expression, ExpressionAs, QuotedLiteral, SelectStatement } from "../query/ast/Expressions.js";
4
+ import { Expression } from "../query/ast/Expressions.js";
5
5
  import EntityQuery from "./EntityQuery.js";
6
- import TimedCache from "../common/cache/TimedCache.js";
7
6
  import { contextSymbol, modelSymbol } from "../common/symbols/symbols.js";
8
7
 
9
- const modelCache = new TimedCache<any, SelectStatement>();
10
-
11
8
  export class EntitySource<T = any> {
12
9
 
13
10
  get [modelSymbol]() {
@@ -61,30 +58,19 @@ export class EntitySource<T = any> {
61
58
  return this.asQuery();
62
59
  }
63
60
 
64
- public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
65
- return this.asQuery().where(parameter, fx);
61
+ public filtered(mode: "read" | "modify" = "read"): IEntityQuery<T> {
62
+ const query = this.asQuery();
63
+ const events = this.context.eventsFor(this.model.typeClass, true);
64
+ return mode === "modify" ? events.modify(query) : events.filter(query);
66
65
  }
67
66
 
68
- generateModel(): SelectStatement {
69
- const source = this.model.fullyQualifiedName;
70
- const as = Expression.parameter(this.model.name[0] + "1");
71
- const fields = this.model.columns.map((c) => c.name !== c.columnName
72
- ? ExpressionAs.create({
73
- expression: Expression.member(as, c.columnName),
74
- alias: QuotedLiteral.create({ literal: c.name })
75
- })
76
- : Expression.member(as, c.columnName));
77
- return SelectStatement.create({
78
- source,
79
- as,
80
- fields,
81
- names: JSON.stringify([as.name])
82
- });
67
+ public where<P>(...[parameter, fx]: IFilterExpression<P, T>) {
68
+ return this.asQuery().where(parameter, fx);
83
69
  }
84
70
 
85
71
  public asQuery() {
86
72
  const { model, context } = this;
87
- const selectStatement = modelCache.getOrCreate(`select-model-${this.model.name}`, () => this.generateModel());
73
+ const selectStatement = this.model.selectAllFields();
88
74
  selectStatement.model = model;
89
75
  return new EntityQuery<T>({
90
76
  context,
@@ -26,6 +26,8 @@ export interface IBaseQuery<T> {
26
26
  count<P>(parameters: P, fx: (p: P) => (x: T) => boolean): Promise<number>;
27
27
 
28
28
  withSignal<DT>(this:DT, signal: AbortSignal): DT;
29
+
30
+ include<TR>(fx: (x: T) => TR | TR[]): IBaseQuery<T>;
29
31
  }
30
32
 
31
33
  export interface IOrderedEntityQuery<T> extends IBaseQuery<T> {
@@ -120,7 +120,7 @@ export default class ChangeEntry implements IChanges {
120
120
  const { type: { relations }, entity } = this;
121
121
  // for parent relations.. check if related key is set or not...
122
122
  for (const iterator of relations) {
123
- if (iterator.isCollection) {
123
+ if (iterator.isInverseRelation) {
124
124
  continue;
125
125
  }
126
126
 
@@ -161,7 +161,7 @@ export default class ChangeEntry implements IChanges {
161
161
  setupInverseProperties() {
162
162
  const deleted = this.status === "deleted";
163
163
  for (const iterator of this.type.relations) {
164
- if (!iterator.isCollection) {
164
+ if (!iterator.isInverseRelation) {
165
165
  continue;
166
166
  }
167
167
  const { relatedName } = iterator;
@@ -1,6 +1,6 @@
1
1
  import SchemaRegistry from "../../decorators/SchemaRegistry.js";
2
2
  import EntityContext from "../EntityContext.js";
3
- import IdentityService from "../identity/IdentityService.js";
3
+ import IdentityService, { identityMapSymbol } from "../identity/IdentityService.js";
4
4
  import ChangeEntry from "./ChangeEntry.js";
5
5
 
6
6
  export const privateUpdateEntry = Symbol("updateEntry");
@@ -9,6 +9,10 @@ export default class ChangeSet {
9
9
 
10
10
  public readonly entries: ChangeEntry[] = [];
11
11
 
12
+ get [identityMapSymbol]() {
13
+ return this.identityMap;
14
+ }
15
+
12
16
  private entryMap: Map<any, ChangeEntry> = new Map();
13
17
 
14
18
  /**
@@ -8,13 +8,13 @@ import ChangeEntry from "../changes/ChangeEntry.js";
8
8
 
9
9
  const done = Promise.resolve() as Promise<void>;
10
10
 
11
- export class ForeignKeyFilter<T = any> {
11
+ export class ForeignKeyFilter<T = any, TE = any> {
12
12
 
13
13
  public type: EntityType;
14
14
  public name: string;
15
15
  public fkName: string;
16
16
 
17
- private events: EntityEvents<any>;
17
+ private events: EntityEvents<TE>;
18
18
  private context: EntityContext;
19
19
 
20
20
  constructor(p: Partial<ForeignKeyFilter> & { context: EntityContext, events: EntityEvents<any> }) {
@@ -22,17 +22,21 @@ export class ForeignKeyFilter<T = any> {
22
22
  return p as any as ForeignKeyFilter;
23
23
  }
24
24
 
25
- public is<TR>(fx: (x: T) => TR): boolean {
25
+ public is<TR>(fx: (x: T) => TR): this is ForeignKeyFilter<T, TR> & boolean {
26
26
  const name = NameParser.parseMember(fx);
27
27
  return name === this.fkName || name === this.name;
28
28
  }
29
29
 
30
- public read() {
30
+ public read(): IEntityQuery<TE> {
31
31
  const read = this.context.query(this.type.typeClass);
32
32
  return this.events.filter(read);
33
33
  }
34
34
 
35
- public modify() {
35
+ public unfiltered(): IEntityQuery<TE> {
36
+ return this.context.query(this.type.typeClass);
37
+ }
38
+
39
+ public modify(): IEntityQuery<TE> {
36
40
  const read = this.context.query(this.type.typeClass);
37
41
  return this.events.modify(read);
38
42
  }
@@ -1,7 +1,15 @@
1
1
  import SchemaRegistry from "../../decorators/SchemaRegistry.js";
2
+ import type EntityType from "../../entity-query/EntityType.js";
3
+
4
+ export const identityMapSymbol = Symbol("identityMapSymbol");
2
5
 
3
6
  export default class IdentityService {
4
7
 
8
+ public static buildIdentity(model: EntityType, ... keys: any[]) {
9
+ const type = model.name;
10
+ return JSON.stringify({ type, keys });
11
+ }
12
+
5
13
  public static getIdentity(entity) {
6
14
  const entityType = SchemaRegistry.model(Object.getPrototypeOf(entity).constructor);
7
15
  const keys = [];
@@ -0,0 +1,71 @@
1
+ import type ChangeEntry from "../changes/ChangeEntry.js";
2
+ import type ChangeSet from "../changes/ChangeSet.js";
3
+ import IdentityService, { identityMapSymbol } from "./IdentityService.js";
4
+
5
+ export default class RelationMapper {
6
+
7
+ private map: Map<string, ChangeEntry[]> = new Map();
8
+
9
+ constructor(
10
+ private changeSet: ChangeSet,
11
+ private identityMap: Map<string, ChangeEntry> = changeSet[identityMapSymbol]
12
+ ) {
13
+
14
+ }
15
+
16
+ push(id: string, waiter: ChangeEntry) {
17
+ let queue = this.map.get(id);
18
+ if (!queue) {
19
+ queue = [];
20
+ this.map.set(id, queue);
21
+ }
22
+ queue.push(waiter);
23
+ }
24
+
25
+ fix(entry: ChangeEntry, nest = true) {
26
+
27
+ // find all parents...
28
+ const { type, entity } = entry;
29
+ for (const iterator of type.relations) {
30
+ if (iterator.isInverseRelation) {
31
+ continue;
32
+ }
33
+ const fkColumn = iterator.fkColumn.name;
34
+ const fkValue = entity[fkColumn];
35
+ if (fkValue === void 0) {
36
+ continue;
37
+ }
38
+ // get from identity...
39
+ const id = IdentityService.buildIdentity(iterator.relatedEntity, fkValue);
40
+ const parent = this.identityMap.get(id);
41
+ if (!parent) {
42
+ let waiters = this.map.get(id);
43
+ if (!waiters) {
44
+ waiters = [];
45
+ this.map.set(id, waiters);
46
+ }
47
+ waiters.push(entry);
48
+ continue;
49
+ }
50
+ entity[iterator.name] = parent;
51
+
52
+ const coll = (parent[iterator.relatedRelation.name] ??= []) as any[];
53
+ if(!coll.includes(entity)){
54
+ coll.push(entity);
55
+ }
56
+ }
57
+
58
+ if (!nest) {
59
+ return;
60
+ }
61
+
62
+ // see if anyone is waiting for us or not...
63
+ const identity = IdentityService.getIdentity(entry.entity);
64
+ const pending = this.map.get(identity);
65
+ if (pending && pending.length) {
66
+ for (const iterator of pending) {
67
+ this.fix(iterator, false);
68
+ }
69
+ }
70
+ }
71
+ }
@@ -48,7 +48,7 @@ export default class VerificationSession {
48
48
  // we need to verify access to each foreign key
49
49
 
50
50
  for (const relation of type.relations) {
51
- if (relation.isCollection) {
51
+ if (relation.isInverseRelation) {
52
52
  continue;
53
53
  }
54
54
 
@@ -1,4 +1,4 @@
1
- import { ArrowFunctionExpression, BigIntLiteral, BinaryExpression, BooleanLiteral, CallExpression, CoalesceExpression, ConditionalExpression, Constant, DeleteStatement, Expression, ExpressionAs, Identifier, MemberExpression, NewObjectExpression, NullExpression, NumberLiteral, ParameterExpression, QuotedLiteral, StringLiteral, TableLiteral, TemplateElement, TemplateLiteral } from "./Expressions.js";
1
+ import { ArrowFunctionExpression, BigIntLiteral, BinaryExpression, BooleanLiteral, CallExpression, CoalesceExpression, ConditionalExpression, Constant, DeleteStatement, ExistsExpression, Expression, ExpressionAs, Identifier, InsertStatement, JoinExpression, MemberExpression, NewObjectExpression, NullExpression, NumberLiteral, OrderByExpression, ParameterExpression, QuotedLiteral, ReturnUpdated, SelectStatement, StringLiteral, TableLiteral, TemplateElement, TemplateLiteral, UpdateStatement, ValuesStatement } from "./Expressions.js";
2
2
  import Visitor from "./Visitor.js";
3
3
 
4
4
  const isBinary = (type) => /^(BinaryExpression|CoalesceExpression)$/.test(type);
@@ -47,7 +47,7 @@ export default class DebugStringVisitor extends Visitor<string> {
47
47
  }
48
48
 
49
49
  visitConditionalExpression(e: ConditionalExpression): string {
50
- return `${e.test} ? ${e.consequent} : ${e.alternate}`;
50
+ return `${this.visit(e.test)} ? ${this.visit(e.consequent)} : ${this.visit(e.alternate)}`;
51
51
  }
52
52
 
53
53
  visitConstant(e: Constant): string {
@@ -55,7 +55,7 @@ export default class DebugStringVisitor extends Visitor<string> {
55
55
  }
56
56
 
57
57
  visitExpressionAs(e: ExpressionAs): string {
58
- return `${e.expression} as ${e.alias}`;
58
+ return `${this.visit(e.expression)} as ${this.visit(e.alias)}`;
59
59
  }
60
60
 
61
61
  visitIdentifier(e: Identifier): string {
@@ -94,7 +94,7 @@ export default class DebugStringVisitor extends Visitor<string> {
94
94
  }
95
95
 
96
96
  visitStringLiteral(e: StringLiteral): string {
97
- return `"${e.value}"`;
97
+ return `'${e.value}'`;
98
98
  }
99
99
 
100
100
  visitTemplateElement(e: TemplateElement): string {
@@ -122,6 +122,53 @@ export default class DebugStringVisitor extends Visitor<string> {
122
122
  return "`" + items.join("") + "`";
123
123
  }
124
124
 
125
+ visitDeleteStatement(e: DeleteStatement): string {
126
+ if (e.where) {
127
+ return `DELETE FROM ${this.visit(e.table)} WHERE ${this.visit(e.where)}`;
128
+ }
129
+ return `DELETE FROM ${this.visit(e.table)}`;
130
+ }
131
+
132
+ visitExistsExpression(e: ExistsExpression): string {
133
+ return `EXISTS (${this.visit(e.target)})`;
134
+ }
135
+
136
+ visitInsertStatement(e: InsertStatement): string {
137
+ return `INSERT INTO ${this.visit(e.table)} ${e.values} ${this.visit(e.returnValues)}`;
138
+ }
139
+
140
+ visitJoinExpression(e: JoinExpression): string {
141
+ return `\n${e.joinType} JOIN ${this.visit(e.source)}\n\t\tON ${this.visit(e.where)}\n`;
142
+ }
143
+
144
+ visitOrderByExpression(e: OrderByExpression): string {
145
+ return `${e.target} ${e.descending ? "DESC" : "ASC"}`;
146
+ }
147
+
148
+ visitReturnUpdated(e: ReturnUpdated): string {
149
+ return `\nRETURNING ${this.visitArray(e.fields)}`;
150
+ }
151
+
152
+ visitValuesStatement(e: ValuesStatement): string {
153
+ const rows = e.values.map((x) => `(${this.visit(x[0])})`).join(",\n\t");
154
+ return `(VALUES ${rows}) as ${this.visit(e.as)})`;
155
+ }
156
+
157
+ visitSelectStatement(e: SelectStatement): string {
158
+ const select = `SELECT\n\t${this.visitArray(e.fields, ",\n\t")}\n\tFROM ${this.visit(e.source)}`;
159
+ const as = e.as ? this.visit(e.as): "";
160
+ const joins = e.joins?.length > 0 ? this.visitArray(e.joins, "\n\t") : "";
161
+ const where = e.where ? `\n\tWHERE ${this.visit(e.where)}` : "";
162
+ const orderBy = e.orderBy ? `\n\tORDER BY ${this.visitArray(e.orderBy, "\n\t\tTHEN BY")}`: "";
163
+ const limit = e.limit > 0 ? `\n\tLIMIT ${e.limit}` : "";
164
+ const offset = e.offset > 0 ? `\n\OFFSET ${e.offset}` : "";
165
+ return `${select}${as}${joins}${where}${orderBy}${limit}${offset}`;
166
+ }
167
+
168
+ visitUpdateStatement(e: UpdateStatement): string {
169
+ return "UPDATE";
170
+ }
171
+
125
172
  private visitArray(e: Expression[], separator = ", ") {
126
173
  return e.map((x) => this.visit(x)).join(separator);
127
174
  }