Skip to content

Commit 73f1663

Browse files
committed
feat: add query runner to execute sql queries using gRPC relational client
Signed-off-by: Rakib Ansary <rakibansary@gmail.com>
1 parent 30a0ac2 commit 73f1663

File tree

12 files changed

+242
-440
lines changed

12 files changed

+242
-440
lines changed

src/common/QueryRunner.ts

Lines changed: 97 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,91 @@
1-
import { ColumnType, Query, QueryRequest, Value } from "../grpc/models/rdb/relational";
1+
/* TODO:
2+
1. Move this to @topcoder-framework
3+
2. Cleanup the exported interfaces
4+
3. Make "Client" a constructor parameter that implements a "Client" interface
5+
4 "ExecuteSqlQuery" should return a Promise<T> where T is the type of the result for "read" queries, but should return "number" for "write" queries indicating either
6+
a) the number of rows affected or
7+
b) the ID of the row insertede
8+
*/
9+
10+
import { ColumnType, Operator, Query, QueryRequest, Value } from "../grpc/models/rdb/relational";
211

312
import { relationalClient } from "../grpc/client/relational";
13+
import { TableColumns, TableColumn } from "./TableColumn";
414

5-
export interface TableOptions {
15+
export type Schema = {
616
dbSchema: string;
717
tableName: string;
8-
columns: {
9-
name: string;
10-
type: ColumnType;
11-
}[];
1218
idColumn?: string;
1319
idSequence?: string;
1420
idTable?: string;
15-
}
21+
columns: TableColumns;
22+
};
1623

17-
interface SqlQuery {
24+
interface ExecuteSqlQuery {
1825
exec(): Promise<unknown>;
1926
}
2027

21-
export interface SelectQuery extends SqlQuery {
22-
query(columns: string[]): SelectQuery;
23-
where(): SelectQuery;
24-
join(): SelectQuery;
28+
type JoinAndWhereClause = JoinClause & WhereClause & ExecuteSqlQuery;
29+
30+
export interface SelectQuery {
31+
select(columns: TableColumn[]): JoinAndWhereClause & LimitClause & OffsetClause;
32+
}
33+
34+
export interface JoinClause {
35+
join(): JoinAndWhereClause;
36+
}
37+
38+
export interface WhereClause {
39+
where(whereCriteria: {
40+
key: string,
41+
operator: Operator,
42+
value: Value
43+
}): JoinAndWhereClause & LimitClause & OffsetClause;
44+
}
45+
46+
export interface LimitClause {
47+
limit(limit: number): OffsetClause & ExecuteSqlQuery;
2548
}
2649

27-
export interface InsertQuery extends SqlQuery {
28-
insert(input: unknown): InsertQuery;
50+
export interface OffsetClause {
51+
offset(offset: number): ExecuteSqlQuery;
2952
}
3053

31-
export interface UpdateQuery extends SqlQuery {
32-
update(input: Record<string, unknown>): UpdateQuery;
54+
export interface InsertQuery<CreateInput> {
55+
insert(input: CreateInput): ExecuteSqlQuery;
3356
}
3457

35-
export interface DeleteQuery extends SqlQuery {
36-
delete(): DeleteQuery;
58+
export interface UpdateQuery<UpdateInput> {
59+
update(lookupCriteria: {[key: string]: unknown} ,input: UpdateInput): ExecuteSqlQuery;
3760
}
3861

39-
export class QueryRunner<T, CreateInput extends object>
40-
implements SelectQuery, InsertQuery, UpdateQuery, DeleteQuery
62+
export interface DeleteQuery {
63+
delete(): ExecuteSqlQuery;
64+
}
65+
66+
export class QueryRunner<T, CreateInput extends {[key: string]: unknown}, UpdateInput extends {[key: string]: unknown}>
67+
implements
68+
SelectQuery, JoinClause, WhereClause, LimitClause, OffsetClause,
69+
InsertQuery<CreateInput>,
70+
UpdateQuery<UpdateInput>,
71+
DeleteQuery,
72+
ExecuteSqlQuery
4173
{
4274
#query: Query | null = null;
4375

44-
// TODO: Optimize this as each instantiation of this class will create a new object with the same keys and values.
45-
#attributeKeyTypeMap: Record<string, ColumnType> = this.options.columns.reduce(
46-
(acc, cur) => ({
47-
...acc,
48-
[cur.name.replace(/([-_][a-z])/gi, ($1) => {
49-
return $1.toUpperCase().replace("-", "").replace("_", "");
50-
})]: cur.type,
51-
}),
52-
{}
53-
);
54-
55-
constructor(protected options: TableOptions) {}
76+
constructor(private schema: Schema) {}
5677

57-
query(columns: string[]): SelectQuery {
78+
select(columns: TableColumn[]): JoinAndWhereClause & LimitClause & OffsetClause {
5879
this.#query = {
5980
query: {
6081
$case: "select",
6182
select: {
62-
schema: this.options.dbSchema,
63-
table: this.options.tableName,
83+
schema: this.schema.dbSchema,
84+
table: this.schema.tableName,
6485
column: columns.map((col) => ({
65-
tableName: this.options.tableName,
66-
name: col,
67-
type: this.#attributeKeyTypeMap[col],
86+
tableName: this.schema.tableName,
87+
name: col.name,
88+
type: col.type
6889
})),
6990
where: [],
7091
join: [],
@@ -78,22 +99,48 @@ export class QueryRunner<T, CreateInput extends object>
7899
return this;
79100
}
80101

81-
where(): SelectQuery {
102+
// TODO: use "convenience" methods from lib-util to build the clause
103+
where(whereCriteria: {
104+
key: string,
105+
operator: Operator,
106+
value: Value
107+
}): JoinAndWhereClause & LimitClause & OffsetClause {
108+
if (this.#query?.query?.$case != "select") {
109+
throw new Error("Cannot set where clause on a non-select query");
110+
}
111+
112+
this.#query.query.select.where.push(whereCriteria);
113+
return this;
114+
}
115+
116+
join(): JoinAndWhereClause {
117+
// TODO: Implement join clause
118+
return this;
119+
}
120+
121+
limit(limit: number): OffsetClause & ExecuteSqlQuery {
122+
if (this.#query?.query?.$case != "select") {
123+
throw new Error("Cannot set limit on a non-select query");
124+
}
125+
this.#query.query.select.limit = limit;
82126
return this;
83127
}
84128

85-
join(): SelectQuery {
129+
offset(offset: number): ExecuteSqlQuery {
130+
if (this.#query?.query?.$case != "select") {
131+
throw new Error("Cannot set offset on a non-select query");
132+
}
133+
this.#query.query.select.offset = offset;
86134
return this;
87135
}
88136

89-
insert(input: CreateInput): InsertQuery {
90-
console.log("Create Input", input);
137+
insert(input: CreateInput): ExecuteSqlQuery {
91138
this.#query = {
92139
query: {
93140
$case: "insert",
94141
insert: {
95-
schema: this.options.dbSchema,
96-
table: this.options.tableName,
142+
schema: this.schema.dbSchema,
143+
table: this.schema.tableName,
97144
columnValue: [
98145
{
99146
column: "create_date",
@@ -116,11 +163,11 @@ export class QueryRunner<T, CreateInput extends object>
116163
...Object.entries(input)
117164
.filter(([_key, value]) => value !== undefined)
118165
.map(([key, value]) => ({
119-
column: key.replace(/([A-Z])/g, "_$1").toLowerCase(),
166+
column: this.schema.columns[key].name,
120167
value: this.toValue(key, value),
121168
})),
122169
],
123-
idTable: this.options.tableName,
170+
idTable: this.schema.tableName,
124171
idColumn: "project_phase_id",
125172
idSequence: "project_phase_id_seq",
126173
},
@@ -130,18 +177,15 @@ export class QueryRunner<T, CreateInput extends object>
130177
return this;
131178
}
132179

133-
update(input: Record<string, unknown>): UpdateQuery {
180+
update(input: Record<string, unknown>): ExecuteSqlQuery {
134181
return this;
135182
}
136183

137-
delete(): DeleteQuery {
184+
delete(): ExecuteSqlQuery {
138185
return this;
139186
}
140187

141188
async exec(): Promise<number | T[]> {
142-
console.log("Execute query");
143-
console.log("Insert query", JSON.stringify(this.#query));
144-
145189
if (!this.#query) {
146190
throw new Error("No query to execute");
147191
}
@@ -182,7 +226,8 @@ export class QueryRunner<T, CreateInput extends object>
182226
}
183227

184228
private toValue(key: string, value: unknown): Value {
185-
const dataType: ColumnType = this.#attributeKeyTypeMap[key];
229+
const dataType: ColumnType = this.schema.columns[key].type;
230+
186231
if (dataType == null) {
187232
throw new Error(`Unknown column ${key}`);
188233
}

src/common/TableColumn.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ColumnType } from "../../dist/grpc/models/rdb/relational";
2+
3+
export type TableColumn = {
4+
name: string;
5+
type: ColumnType;
6+
};
7+
8+
export type TableColumns = {
9+
[key: string]: TableColumn;
10+
};

src/domain/LegacyChallenge.ts

Lines changed: 29 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import _ from "lodash";
2+
import { QueryRunner } from "../common/QueryRunner";
23

34
import { relationalClient } from "../grpc/client/relational";
45
import { ColumnType, Operator, QueryRequest, QueryResponse } from "../grpc/models/rdb/relational";
6+
import { CheckChallengeExistsResponse } from "../models/domain-layer/legacy/legacy_challenge";
7+
import { Value } from "../models/google/protobuf/struct";
8+
import { Project } from "../schema/Project";
59

610
class LegacyChallengeDomain {
711
constructor(private tableName: string = "project") {}
@@ -51,77 +55,36 @@ class LegacyChallengeDomain {
5155
// return [];
5256
// }
5357

54-
public async checkChallengeExists(legacyChallengeId: number): Promise<boolean> {
55-
const queryRequest: QueryRequest = {
56-
query: {
57-
query: {
58-
$case: "select",
59-
select: {
60-
table: this.tableName,
61-
join: [],
62-
column: [
63-
{
64-
name: "project_id",
65-
type: ColumnType.COLUMN_TYPE_INT,
66-
},
67-
],
68-
where: [
69-
{
70-
key: "project_id",
71-
operator: Operator.OPERATOR_EQUAL,
72-
value: {
73-
value: {
74-
$case: "intValue",
75-
intValue: legacyChallengeId,
76-
},
77-
},
78-
},
79-
],
80-
groupBy: [],
81-
orderBy: [],
82-
limit: 1,
83-
offset: 0,
58+
public async checkChallengeExists(
59+
legacyChallengeId: number
60+
): Promise<CheckChallengeExistsResponse> {
61+
const challenges = (await new QueryRunner(Project)
62+
.select([Project.columns.projectId])
63+
.where({
64+
key: "project_id",
65+
operator: Operator.OPERATOR_EQUAL,
66+
value: {
67+
value: {
68+
$case: "intValue",
69+
intValue: legacyChallengeId,
8470
},
8571
},
86-
},
72+
})
73+
.limit(1)
74+
.offset(0)
75+
.exec()) as [
76+
{
77+
values: {
78+
projectId: Value;
79+
};
80+
}
81+
];
82+
83+
return {
84+
exists: challenges.length == 1,
8785
};
88-
89-
const queryResponse: QueryResponse = await relationalClient.query(queryRequest);
90-
91-
if (queryResponse.result?.$case == "selectResult") {
92-
const rows = queryResponse.result.selectResult.rows;
93-
return Promise.resolve(rows.length > 0);
94-
}
95-
96-
return Promise.resolve(false);
9786
}
9887

99-
// public async addOrUpdateChallengeInfo(
100-
// challengeInfo: LegacyChallengeInfoRequest
101-
// ): Promise<boolean> {
102-
// const queryRequest: QueryRequest = {
103-
// query: {
104-
// query: {
105-
// $case: "insert",
106-
// insert: {
107-
// table: this.tableName,
108-
// columnValue: [],
109-
// },
110-
// },
111-
// },
112-
// };
113-
114-
// const queryResponse: QueryResponse = await relationalClient.query(
115-
// queryRequest
116-
// );
117-
118-
// if (queryResponse.result?.$case == "insertResult") {
119-
// return Promise.resolve(true);
120-
// }
121-
122-
// return Promise.resolve(false);
123-
// }
124-
12588
public async listAvailableChallengeInfoTypes(key: string): Promise<number> {
12689
const queryRequest: QueryRequest = {
12790
query: {

0 commit comments

Comments
 (0)