Skip to content

Commit b5d378d

Browse files
Finish implementing Queryable tests (#15)
* Finish implementing ItemIndividualQuery-specific functions * Implement all ReimbursementItemBoxQuery functions * Add nonexistent read check to CompositeCrudQueryable tests * Refactor crud queryable interfaces out to separate files * Factor simple crud queries out into generic functions * Factor composiste crud queries out into generic functions * Convert crud queryables to be classes and inherit default crud functions * Fix ReimbursementQuery test datetimes; ValidCategoryQuery updating should fail since PKs cannot be updated * Change crud queryable fields to be protected instead of private * Finish implementing TransactionItemQuery and add guards to composite queryable tests to clean up created objects * Finish implementing CsssUserQuery
1 parent 1fe79fd commit b5d378d

18 files changed

+409
-388
lines changed

src/db/CompositeCrudQueryable.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as DB from "../db/DB";
2+
3+
export class CompositeCrudQueryable<T, TInit, TMut, PK1, PK2> {
4+
protected tableName: string;
5+
protected pk1Name: string;
6+
protected pk2Name: string;
7+
8+
constructor(tableName: string, pk1Name: string, pk2Name: string) {
9+
this.tableName = tableName;
10+
this.pk1Name = pk1Name;
11+
this.pk2Name = pk2Name;
12+
}
13+
14+
/**
15+
* Creates a new entry for the given object in the database.
16+
* @param object Initializer of the object
17+
* @returns Promise resolving to the given initialized object if successful
18+
*/
19+
public create = async (object: TInit): Promise<T> => {
20+
// Object.keys and Object.values return things in the same order so this is safe
21+
const keys = Object.keys(object);
22+
const values = Object.values(object);
23+
24+
const queryResponse = await DB.query(
25+
`INSERT INTO ${this.tableName} (${keys.join(",")})` +
26+
`VALUES (${keys.map((prop, i) => `$${i + 1}`).join(",")})` +
27+
"RETURNING *",
28+
values
29+
);
30+
if (queryResponse.rows.length === 1) {
31+
return queryResponse.rows[0];
32+
} else {
33+
return null;
34+
}
35+
};
36+
37+
/**
38+
* Reads the database and returns the object with the given composite key.
39+
* @param pk1 First foreign key of the object that forms the composite key
40+
* @param pk2 Second foreign key of the object that forms the composite key
41+
* @returns Promise resolving to object with given composite key, or null if no object is found
42+
*/
43+
public read = async (pk1: PK1, pk2: PK2): Promise<T> => {
44+
const queryResponse = await DB.query(
45+
`SELECT * FROM ${this.tableName} WHERE ${this.pk1Name}=$1 AND ${this.pk2Name}=$2`,
46+
[pk1, pk2]
47+
);
48+
if (queryResponse.rows.length === 1) {
49+
return queryResponse.rows[0];
50+
} else {
51+
return null;
52+
}
53+
};
54+
55+
/**
56+
* Reads the database for all entries of the object.
57+
* @returns Promise resolving to all entries of the object in its table in the database
58+
*/
59+
public readAll = async (): Promise<T[]> => {
60+
const queryResponse = await DB.query(`SELECT * FROM ${this.tableName}`);
61+
return queryResponse.rows;
62+
};
63+
64+
/**
65+
* Updates an existing object with the given composite key.
66+
* @param pk1 First foreign key of the object that forms the composite key
67+
* @param pk2 Second foreign key of the object that forms the composite key
68+
* @param mutateProps Mutator of the object containing desired new properties
69+
* @returns Promise resolving to the updated object, or null if no object is found
70+
*/
71+
public update = async (pk1: PK1, pk2: PK2, mutateObject: TMut): Promise<T> => {
72+
if (Object.keys(mutateObject).length === 0) {
73+
return null;
74+
}
75+
76+
// Use i+3 for parameter so that $1 and $2 are reserved for the PKs
77+
const keys = Object.keys(mutateObject).map((prop, i) => `${prop}=$${i + 3}`);
78+
const queryResponse = await DB.query(
79+
`UPDATE ${this.tableName} SET ${keys.join(",")}` +
80+
` WHERE ${this.pk1Name}=$1 AND ${this.pk2Name}=$2 RETURNING *`,
81+
[pk1, pk2, ...Object.values(mutateObject)]
82+
);
83+
if (queryResponse.rows.length === 1) {
84+
return queryResponse.rows[0];
85+
} else {
86+
return null;
87+
}
88+
};
89+
90+
/**
91+
* Deletes the object in the database with the given composite key.
92+
* @param pk1 First foreign key of the object that forms the composite key
93+
* @param pk2 Second foreign key of the object that forms the composite key
94+
* @returns Promise resolving to boolean indicating whether any rows were deleted
95+
*/
96+
public delete = async (pk1: PK1, pk2: PK2): Promise<boolean> => {
97+
const queryResponse = await DB.query(
98+
`DELETE FROM ${this.tableName} WHERE ${this.pk1Name}=$1 AND ${this.pk2Name}=$2`,
99+
[pk1, pk2]
100+
);
101+
return queryResponse.rowCount === 1;
102+
};
103+
}

src/db/Queryable.ts

Lines changed: 0 additions & 76 deletions
This file was deleted.

src/db/SimpleCrudQueryable.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import * as DB from "../db/DB";
2+
3+
export class SimpleCrudQueryable<T, TInit, TMut, PK> {
4+
protected tableName: string;
5+
protected pkName: string;
6+
7+
constructor(tableName: string, pkName: string) {
8+
this.tableName = tableName;
9+
this.pkName = pkName;
10+
}
11+
12+
/**
13+
* Creates a new entry for the given object in the database.
14+
* @param object Initializer of the object
15+
* @returns Promise resolving to the given initialized object if successful
16+
*/
17+
public create = async (object: TInit): Promise<T> => {
18+
// Object.keys and Object.values return things in the same order so this is safe
19+
const keys = Object.keys(object);
20+
const values = Object.values(object);
21+
22+
const queryResponse = await DB.query(
23+
`INSERT INTO ${this.tableName} (${keys.join(",")})` +
24+
`VALUES (${keys.map((prop, i) => `$${i + 1}`).join(",")})` +
25+
"RETURNING *",
26+
values
27+
);
28+
if (queryResponse.rows.length === 1) {
29+
return queryResponse.rows[0];
30+
} else {
31+
return null;
32+
}
33+
};
34+
35+
/**
36+
* Reads the database and returns the object with the given primary key.
37+
* @param primaryKey Primary key of the object
38+
* @returns Promise resolving to object with given primary key, or null if no object is found
39+
*/
40+
public read = async (primaryKey: PK): Promise<T> => {
41+
const queryResponse = await DB.query(
42+
`SELECT * FROM ${this.tableName} WHERE ${this.pkName}=$1`,
43+
[primaryKey]
44+
);
45+
if (queryResponse.rows.length === 1) {
46+
return queryResponse.rows[0];
47+
} else {
48+
return null;
49+
}
50+
};
51+
52+
/**
53+
* Reads the database for all entries of the object.
54+
* @returns Promise resolving to all entries of the object in its table in the database
55+
*/
56+
public readAll = async (): Promise<T[]> => {
57+
const queryResponse = await DB.query(`SELECT * FROM ${this.tableName}`);
58+
return queryResponse.rows;
59+
};
60+
61+
/**
62+
* Updates an existing object with the given primary key.
63+
* @param primaryKey Primary key of the object
64+
* @param mutateProps Mutator of the object containing desired new properties
65+
* @returns Promise resolving to the updated object, or null if no object is found
66+
*/
67+
public update = async (primaryKey: PK, mutateObject: TMut): Promise<T> => {
68+
if (Object.keys(mutateObject).length === 0) {
69+
return null;
70+
}
71+
72+
// Use i+2 for parameter so that $1 is reserved for the item id
73+
const keys = Object.keys(mutateObject).map((prop, i) => `${prop}=$${i + 2}`);
74+
const queryResponse = await DB.query(
75+
`UPDATE ${this.tableName} SET ${keys.join(",")} WHERE ${this.pkName}=$1 RETURNING *`,
76+
[primaryKey, ...Object.values(mutateObject)]
77+
);
78+
if (queryResponse.rows.length === 1) {
79+
return queryResponse.rows[0];
80+
} else {
81+
return null;
82+
}
83+
};
84+
85+
/**
86+
* Deletes the object in the database with the given primary key.
87+
* @param primaryKey Primary key of the object
88+
* @returns Promise resolving to boolean indicating whether any rows were deleted
89+
*/
90+
public delete = async (primaryKey: PK): Promise<boolean> => {
91+
const queryResponse = await DB.query(
92+
`DELETE FROM ${this.tableName} WHERE ${this.pkName}=$1`,
93+
[primaryKey]
94+
);
95+
return queryResponse.rowCount === 1;
96+
};
97+
}

src/db/queries/CsssUserQuery.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,34 @@
11

22
import CsssUser,
33
{UserId, CsssUserInitializer, CsssUserMutator} from "../../types/db/public/CsssUser";
4-
import {SimpleCrudQueryable} from "../Queryable";
4+
import {SimpleCrudQueryable} from "../SimpleCrudQueryable";
5+
import * as DB from "../../db/DB";
56

6-
const simpleCrudQueries:
7-
SimpleCrudQueryable<CsssUser, CsssUserInitializer, CsssUserMutator, UserId> = {
8-
async create(object: CsssUserInitializer): Promise<CsssUser> {
9-
throw new Error("Method not implemented.");
10-
},
7+
const tableName = "csss_user";
8+
const pkName = "user_id";
119

12-
async read(userId: UserId): Promise<CsssUser> {
13-
throw new Error("Method not implemented.");
14-
},
15-
16-
// should we allow?
17-
async readAll(): Promise<CsssUser[]> {
18-
throw new Error("Method not implemented.");
19-
},
20-
21-
async update(userId: UserId, mutateObject: CsssUserMutator): Promise<CsssUser> {
22-
throw new Error("Method not implemented.");
23-
},
24-
25-
async delete(userId: UserId): Promise<boolean> {
26-
throw new Error("Method not implemented.");
10+
class CsssUserQuery extends SimpleCrudQueryable<CsssUser, CsssUserInitializer, CsssUserMutator, UserId> {
11+
constructor() {
12+
super(tableName, pkName);
2713
}
28-
};
2914

30-
const csssUserQueries = {
3115
/**
3216
* Tries to authenticate the user with the given credentials.
3317
* @param email Email of the given user
3418
* @param password Password hash of the given user
3519
* @returns Promise resolving to the CsssUser with the given credentials, or null if no user found
3620
*/
37-
async authenticateUser(email: string, password: string): Promise<CsssUser> {
38-
throw new Error("Method not implemented.");
39-
}
40-
};
41-
42-
export default {
43-
...simpleCrudQueries,
44-
...csssUserQueries
45-
};
21+
public authenticateUser = async (email: string, password: string): Promise<CsssUser> => {
22+
const queryResponse = await DB.query(
23+
`SELECT * FROM ${this.tableName} WHERE email=$1 AND password=$2`,
24+
[email, password]
25+
);
26+
if (queryResponse.rows.length === 1) {
27+
return queryResponse.rows[0];
28+
} else {
29+
return null;
30+
}
31+
};
32+
}
33+
34+
export default new CsssUserQuery();

src/db/queries/ItemBoxQuery.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,14 @@
11

22
import ItemBox, {ItemBoxId, ItemBoxInitializer, ItemBoxMutator} from "../../types/db/public/ItemBox";
3-
import {SimpleCrudQueryable} from "../Queryable";
3+
import {SimpleCrudQueryable} from "../SimpleCrudQueryable";
44

5-
const simpleCrudQueries:
6-
SimpleCrudQueryable<ItemBox, ItemBoxInitializer, ItemBoxMutator, ItemBoxId> = {
7-
async create(object: ItemBoxInitializer): Promise<ItemBox> {
8-
throw new Error("Method not implemented.");
9-
},
5+
const tableName = "item_box";
6+
const pkName = "item_box_id";
107

11-
async read(itemBoxId: ItemBoxId): Promise<ItemBox> {
12-
throw new Error("Method not implemented.");
13-
},
14-
15-
async readAll(): Promise<ItemBox[]> {
16-
throw new Error("Method not implemented.");
17-
},
18-
19-
async update(itemBoxId: ItemBoxId, mutateObject: ItemBoxMutator): Promise<ItemBox> {
20-
throw new Error("Method not implemented.");
21-
},
22-
23-
async delete(itemBoxId: ItemBoxId): Promise<boolean> {
24-
throw new Error("Method not implemented.");
8+
class ItemBoxQuery extends SimpleCrudQueryable<ItemBox, ItemBoxInitializer, ItemBoxMutator, ItemBoxId> {
9+
constructor() {
10+
super(tableName, pkName);
2511
}
26-
};
12+
}
2713

28-
export default {
29-
...simpleCrudQueries
30-
};
14+
export default new ItemBoxQuery();

0 commit comments

Comments
 (0)