Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.

Commit 0c6910a

Browse files
authored
Merge pull request #303 from redpwn/refactor/stricter-typechecking
refactor(server): add and enforce stricter ts config
2 parents 163e25b + 596b8d9 commit 0c6910a

File tree

17 files changed

+99
-55
lines changed

17 files changed

+99
-55
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"@types/mailgun-js": "0.22.10",
6464
"@types/mustache": "4.0.1",
6565
"@types/nodemailer": "6.4.0",
66+
"@types/pg": "7.14.4",
6667
"@types/uuid": "8.0.1",
6768
"@typescript-eslint/eslint-plugin": "3.7.0",
6869
"@typescript-eslint/parser": "3.8.0",

server/.eslintrc.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ module.exports = {
1616
overrides: [{
1717
files: ['*.ts'],
1818
extends: [
19-
'plugin:@typescript-eslint/recommended'
19+
'plugin:@typescript-eslint/recommended',
20+
'plugin:@typescript-eslint/recommended-requiring-type-checking'
2021
],
2122
plugins: [
2223
'@typescript-eslint'
23-
]
24+
],
25+
rules: {
26+
'@typescript-eslint/no-unnecessary-condition': 'error',
27+
'@typescript-eslint/require-await': 'off'
28+
}
2429
}, {
2530
files: ['.eslintrc.js'],
2631
parser: 'espree'

server/challenges/Provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export interface Provider extends EventEmitter {
99
1010
Challenge equality is determined by chall.id.
1111
*/
12-
updateChallenge(chall: Challenge): void;
13-
deleteChallenge(id: string): void;
12+
updateChallenge(chall: Challenge): Promise<void>;
13+
deleteChallenge(id: string): Promise<void>;
1414
cleanup (): void;
1515
}
1616

server/challenges/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path'
33
import { Challenge, CleanedChallenge } from './types'
44
import { Provider, ProviderConstructor } from './Provider'
55
import { challUpdateEmitter, publishChallUpdate } from '../cache/challs'
6+
import { EventEmitter } from 'events'
67

78
let provider: Provider
89

@@ -41,7 +42,8 @@ void import(path.join('../providers', config.challengeProvider.name))
4142
provider.on('update', onUpdate)
4243
})
4344

44-
challUpdateEmitter.on('update', () => {
45+
// FIXME: remove cast once cache is typed
46+
;(challUpdateEmitter as EventEmitter).on('update', () => {
4547
provider.forceUpdate()
4648
})
4749

server/config/server.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,27 @@ export type ServerConfig = {
8080
loginTimeout: number;
8181
}
8282

83-
const fileConfigLoaders: Map<string, (file: string) => unknown> = new Map([
84-
['json', file => JSON.parse(file)],
85-
['yaml', file => yaml.parse(file)],
86-
['yml', file => yaml.parse(file)]
83+
const jsonLoader = (file: string) => JSON.parse(file) as PartialDeep<ServerConfig>
84+
const yamlLoader = (file: string) => yaml.parse(file) as PartialDeep<ServerConfig>
85+
86+
const fileConfigLoaders: Map<string, (file: string) => PartialDeep<ServerConfig>> = new Map([
87+
['json', jsonLoader],
88+
['yaml', yamlLoader],
89+
['yml', yamlLoader]
8790
])
8891

8992
const configPath = process.env.RCTF_CONF_PATH ?? path.join(__dirname, '../../../conf.d')
9093
const fileConfigs: PartialDeep<ServerConfig>[] = []
9194
fs.readdirSync(configPath).sort().forEach((name) => {
92-
const matched = name.match(/\.(.+)$/)
95+
const matched = /\.(.+)$/.exec(name)
9396
if (matched === null) {
9497
return
9598
}
9699
const loader = fileConfigLoaders.get(matched[1])
97100
if (loader === undefined) {
98101
return
99102
}
100-
const config = loader(fs.readFileSync(path.join(configPath, name)).toString()) as PartialDeep<ServerConfig>
103+
const config = loader(fs.readFileSync(path.join(configPath, name)).toString())
101104
fileConfigs.push(config)
102105
})
103106

server/database/challenges.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ export interface DatabaseChallenge {
77
}
88

99
export const getAllChallenges = (): Promise<DatabaseChallenge[]> => {
10-
return db.query('SELECT * FROM challenges')
10+
return db.query<DatabaseChallenge>('SELECT * FROM challenges')
1111
.then(res => res.rows)
1212
}
1313

1414
export const getChallengeById = ({ id }: Pick<DatabaseChallenge, 'id'>): Promise<DatabaseChallenge | undefined> => {
15-
return db.query('SELECT * FROM challenges WHERE id = $1', [id])
15+
return db.query<DatabaseChallenge>('SELECT * FROM challenges WHERE id = $1', [id])
1616
.then(res => res.rows[0])
1717
}
1818

1919
export const createChallenge = ({ id, data }: DatabaseChallenge): Promise<DatabaseChallenge> => {
20-
return db.query('INSERT INTO challenges ($1, $2) RETURNING *',
20+
return db.query<DatabaseChallenge>('INSERT INTO challenges ($1, $2) RETURNING *',
2121
[id, data]
2222
)
2323
.then(res => res.rows[0])
2424
}
2525

2626
export const removeChallengeById = ({ id }: Pick<DatabaseChallenge, 'id'>): Promise<DatabaseChallenge | undefined> => {
27-
return db.query('DELETE FROM challenges WHERE id = $1 RETURNING *', [id])
27+
return db.query<DatabaseChallenge>('DELETE FROM challenges WHERE id = $1 RETURNING *', [id])
2828
.then(res => res.rows[0])
2929
}
3030

server/database/members.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import db from './db'
2+
import { User } from './users'
23

34
export interface UserMember {
45
id: string;
5-
userid: string; // FIXME: type via User type
6+
userid: User['id'];
67
email: string;
78
}
89

910
export const getMembers = ({ userid }: Pick<UserMember, 'userid'>): Promise<UserMember[]> => {
10-
return db.query('SELECT * FROM user_members WHERE userid = $1', [userid])
11+
return db.query<UserMember>('SELECT * FROM user_members WHERE userid = $1', [userid])
1112
.then(res => res.rows)
1213
}
1314

1415
export const makeMember = ({ id, userid, email }: UserMember): Promise<UserMember> => {
15-
return db.query('INSERT INTO user_members (id, userid, email) VALUES ($1, $2, $3) RETURNING *',
16+
return db.query<UserMember>('INSERT INTO user_members (id, userid, email) VALUES ($1, $2, $3) RETURNING *',
1617
[id, userid, email]
1718
)
1819
.then(res => res.rows[0])
1920
}
2021

2122
export const removeMember = ({ id, userid }: Pick<UserMember, 'id' | 'userid'>): Promise<UserMember | undefined> => {
22-
return db.query('DELETE FROM user_members WHERE id = $1 and userid = $2 RETURNING *', [id, userid])
23+
return db.query<UserMember>('DELETE FROM user_members WHERE id = $1 and userid = $2 RETURNING *', [id, userid])
2324
.then(res => res.rows[0])
2425
}

server/database/solves.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import db from './db'
22
import { Challenge } from '../challenges/types'
33
import { User } from './users'
4+
import { ExtractQueryType } from './util'
45

56
export interface Solve {
67
id: string;
@@ -10,27 +11,27 @@ export interface Solve {
1011
}
1112

1213
export const getAllSolves = (): Promise<Solve[]> => {
13-
return db.query('SELECT * FROM solves ORDER BY createdat ASC')
14+
return db.query<Solve>('SELECT * FROM solves ORDER BY createdat ASC')
1415
.then(res => res.rows)
1516
}
1617

1718
export const getSolvesByUserId = ({ userid }: Pick<Solve, 'userid'>): Promise<Solve[]> => {
18-
return db.query('SELECT * FROM solves WHERE userid = $1 ORDER BY createdat DESC', [userid])
19+
return db.query<Solve>('SELECT * FROM solves WHERE userid = $1 ORDER BY createdat DESC', [userid])
1920
.then(res => res.rows)
2021
}
2122

2223
export const getSolvesByChallId = ({ challengeid, limit, offset }: Pick<Solve, 'challengeid'> & { limit: number; offset: number; }): Promise<(Solve & Pick<User, 'name'>)[]> => {
23-
return db.query('SELECT solves.id, solves.userid, solves.createdat, users.name FROM solves INNER JOIN users ON solves.userid = users.id WHERE solves.challengeid=$1 ORDER BY solves.createdat ASC LIMIT $2 OFFSET $3', [challengeid, limit, offset])
24+
return db.query<ExtractQueryType<typeof getSolvesByChallId>>('SELECT solves.id, solves.userid, solves.createdat, users.name FROM solves INNER JOIN users ON solves.userid = users.id WHERE solves.challengeid=$1 ORDER BY solves.createdat ASC LIMIT $2 OFFSET $3', [challengeid, limit, offset])
2425
.then(res => res.rows)
2526
}
2627

2728
export const getSolveByUserIdAndChallId = ({ userid, challengeid }: Pick<Solve, 'userid' | 'challengeid'>): Promise<Solve | undefined> => {
28-
return db.query('SELECT * FROM solves WHERE userid = $1 AND challengeid = $2 ORDER BY createdat DESC', [userid, challengeid])
29+
return db.query<Solve>('SELECT * FROM solves WHERE userid = $1 AND challengeid = $2 ORDER BY createdat DESC', [userid, challengeid])
2930
.then(res => res.rows[0])
3031
}
3132

3233
export const newSolve = ({ id, userid, challengeid, createdat }: Solve): Promise<Solve> => {
33-
return db.query('INSERT INTO solves (id, challengeid, userid, createdat) VALUES ($1, $2, $3, $4) RETURNING *', [id, challengeid, userid, createdat])
34+
return db.query<Solve>('INSERT INTO solves (id, challengeid, userid, createdat) VALUES ($1, $2, $3, $4) RETURNING *', [id, challengeid, userid, createdat])
3435
.then(res => res.rows[0])
3536
}
3637

server/database/users.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import db from './db'
22
import * as util from '../util'
33
import config, { ServerConfig } from '../config/server'
44
import { DivisionACLError } from '../errors'
5+
import { ExtractQueryType } from './util'
56

67
export interface User {
78
id: string;
@@ -13,67 +14,67 @@ export interface User {
1314
}
1415

1516
export const getAllUsers = (): Promise<Pick<User, 'id' | 'name' | 'division'>[]> => {
16-
return db.query('SELECT id, name, division FROM users')
17+
return db.query<ExtractQueryType<typeof getAllUsers>>('SELECT id, name, division FROM users')
1718
.then(res => res.rows)
1819
}
1920

2021
export const getUserById = ({ id }: Pick<User, 'id'>): Promise<User | undefined> => {
21-
return db.query('SELECT * FROM users WHERE id = $1', [id])
22+
return db.query<User>('SELECT * FROM users WHERE id = $1', [id])
2223
.then(res => res.rows[0])
2324
}
2425

2526
export const getUserByEmail = ({ email }: Pick<User, 'email'>): Promise<User | undefined> => {
26-
return db.query('SELECT * FROM users WHERE email = $1', [email])
27+
return db.query<User>('SELECT * FROM users WHERE email = $1', [email])
2728
.then(res => res.rows[0])
2829
}
2930

3031
export const getUserByCtftimeId = ({ ctftimeId }: Pick<User, 'ctftimeId'>): Promise<User | undefined> => {
31-
return db.query('SELECT * FROM users WHERE ctftime_id = $1', [ctftimeId])
32+
return db.query<User>('SELECT * FROM users WHERE ctftime_id = $1', [ctftimeId])
3233
.then(res => res.rows[0])
3334
}
3435

3536
export const getUserByIdAndEmail = ({ id, email }: Pick<User, 'id' | 'email'>): Promise<User | undefined> => {
36-
return db.query('SELECT * FROM users WHERE id = $1 AND email = $2', [id, email])
37+
return db.query<User>('SELECT * FROM users WHERE id = $1 AND email = $2', [id, email])
3738
.then(res => res.rows[0])
3839
}
3940

4041
export const getUserByNameOrEmail = ({ name, email }: Pick<User, 'name' | 'email'>): Promise<User | undefined> => {
41-
return db.query('SELECT * FROM users WHERE name = $1 OR email = $2', [name, email])
42+
return db.query<User>('SELECT * FROM users WHERE name = $1 OR email = $2', [name, email])
4243
.then(res => res.rows[0])
4344
}
4445

4546
export const getUserByNameOrCtftimeId = ({ name, ctftimeId }: Pick<User, 'name' | 'ctftimeId'>): Promise<User | undefined> => {
46-
return db.query('SELECT * FROM users WHERE name = $1 OR ctftime_id = $2', [name, ctftimeId])
47+
return db.query<User>('SELECT * FROM users WHERE name = $1 OR ctftime_id = $2', [name, ctftimeId])
4748
.then(res => res.rows[0])
4849
}
4950

5051
export const removeUserByEmail = ({ email }: Pick<User, 'email'>): Promise<User | undefined> => {
51-
return db.query('DELETE FROM users WHERE email = $1 RETURNING *', [email])
52+
return db.query<User>('DELETE FROM users WHERE email = $1 RETURNING *', [email])
5253
.then(res => res.rows[0])
5354
}
5455

5556
export const removeUserById = ({ id }: Pick<User, 'id'>): Promise<User | undefined> => {
56-
return db.query('DELETE FROM users WHERE id = $1 RETURNING *', [id])
57+
return db.query<User>('DELETE FROM users WHERE id = $1 RETURNING *', [id])
5758
.then(res => res.rows[0])
5859
}
5960

6061
export const makeUser = ({ id, name, email, division, ctftimeId, perms }: User): Promise<User> => {
6162
if (config.email && config.divisionACLs && !util.restrict.divisionAllowed(email, division)) {
6263
throw new DivisionACLError()
6364
}
64-
return db.query('INSERT INTO users (id, name, email, division, ctftime_id, perms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
65+
return db.query<User>('INSERT INTO users (id, name, email, division, ctftime_id, perms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
6566
[id, name, email, division, ctftimeId, perms]
6667
)
6768
.then(res => res.rows[0])
6869
}
6970

7071
export const removeCtftimeId = ({ id }: Pick<User, 'id'>): Promise<User | undefined> => {
71-
return db.query('UPDATE users SET ctftime_id = NULL WHERE id = $1 AND ctftime_id IS NOT NULL RETURNING *', [id])
72+
return db.query<User>('UPDATE users SET ctftime_id = NULL WHERE id = $1 AND ctftime_id IS NOT NULL RETURNING *', [id])
7273
.then(res => res.rows[0])
7374
}
7475

7576
export const removeEmail = ({ id }: Pick<User, 'id'>): Promise<User | undefined> => {
76-
return db.query('UPDATE users SET email = NULL WHERE id = $1 AND email IS NOT NULL RETURNING *', [id])
77+
return db.query<User>('UPDATE users SET email = NULL WHERE id = $1 AND email IS NOT NULL RETURNING *', [id])
7778
.then(res => res.rows[0])
7879
}
7980

@@ -92,7 +93,7 @@ export const updateUser = async ({ id, name, email, division, ctftimeId, perms }
9293
throw new DivisionACLError()
9394
}
9495
}
95-
return db.query(`
96+
return db.query<User>(`
9697
UPDATE users SET
9798
name = COALESCE($1, name),
9899
email = COALESCE($2, email),

server/database/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { PromiseValue } from 'type-fest'
2+
3+
type ArrayValue<Arr> = Arr extends (infer Val)[] ? Val : Arr
4+
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
export type ExtractQueryType<FuncType extends (...args: any) => any> =
7+
NonNullable<ArrayValue<PromiseValue<ReturnType<FuncType>>>>

0 commit comments

Comments
 (0)