Skip to content

Commit a8892a6

Browse files
authored
Merge pull request #34 from x-team/develop
Deploy to Staging
2 parents 4d36d0c + 5b259ba commit a8892a6

39 files changed

+2906
-136
lines changed

.mocharc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
require: ['./test/setup-env.js', 'source-map-support/register'],
3-
file: ['./src/tests/integrationTestsUtils.ts'],
3+
file: ['./test/test-utils.ts'],
44
timeout: 600000,
55
bail: true,
66
exit: true,

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"test": "cross-env NODE_ENV=test ENV=test npm run test:all",
1111
"test:integration": "cross-env NODE_ENV=test ENV=test npm run test:_integration",
1212
"test:integration:single": "cross-env NODE_ENV=test ENV=test npm run test:prepare:integration && npm run mocha",
13-
"test:unit": "cross-env NODE_ENV=test ENV=test npm run mocha \"test/**/*.test.ts\" --exclude \"test/**/*.integration.test.ts\"",
13+
"test:unit": "cross-env NODE_ENV=test ENV=test npm run mocha \"test/**/*.test.ts\" --exclude \"test/**/*.integration.test.ts\" ",
1414
"test:unit:single": "cross-env NODE_ENV=test ENV=test npm run mocha",
1515
"mocha": "cross-env ENV=test mocha --unhandled-rejections=strict",
1616
"eslint": "eslint src --fix",
@@ -28,6 +28,7 @@
2828
"prepare-db": "echo preparing db for $NODE_ENV env && npm run db:seed:up && npm run db:migrate:up",
2929
"clean": "rm -rf dist",
3030
"db:test:create": "docker-compose -p gameshq-api-test-project -f 'docker-compose-test.yml' up -d",
31+
"db:test:clear": "docker-compose -p gameshq-api-test-project -f 'docker-compose-test.yml' down",
3132
"test:all": "concurrently --kill-others-on-fail --names *typescript,*****eslint,*tests:unit,integration --prefix-colors blue.inverse,blue,yellow,green 'npm run tsc' 'npm run eslint --quiet' 'npm run test:unit' 'npm run test:integration'",
3233
"test:prepare:integration": "npm run db:test:create && cross-env NODE_ENV=test ENV=test npm run prepare-db",
3334
"test:_integration": "npm run test:prepare:integration && npm run mocha \"test/**/*.integration.test.ts\"",
@@ -58,8 +59,8 @@
5859
"node-fetch": "2.6.1",
5960
"pg": "8.6.0",
6061
"reflect-metadata": "0.1.13",
61-
"sequelize": "6.6.4",
62-
"sequelize-typescript": "2.1.0",
62+
"sequelize": "6.20.1",
63+
"sequelize-typescript": "2.1.3",
6364
"sql-log-prettifier": "^0.1.2",
6465
"uuid": "8.3.2",
6566
"winston": "3.3.3"

src/api-utils/appendUserToRequest.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Boom from '@hapi/boom';
2+
import type { Request, ResponseToolkit } from '@hapi/hapi';
3+
4+
import { findSessionByToken } from '../models/Session';
5+
import type { User } from '../models/User';
6+
import { findUserById } from '../models/User';
7+
8+
export async function appendUserToRequest(req: Request, _h: ResponseToolkit): Promise<User> {
9+
const sessionToken = req.headers['xtu-session-token'];
10+
if (!sessionToken) {
11+
throw Boom.forbidden('Only Auth users can access here - send session token');
12+
}
13+
const userSession = await findSessionByToken(sessionToken);
14+
if (!userSession) {
15+
throw Boom.forbidden('Only Auth users can access here - user is not logged in');
16+
}
17+
const user = await findUserById(userSession._userId);
18+
if (!user) {
19+
throw Boom.notFound('User not found');
20+
}
21+
return user;
22+
// return h.continue;
23+
}

src/api-utils/getAuthUser.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import Boom from '@hapi/boom';
22
import type { Request, ResponseToolkit } from '@hapi/hapi';
3+
34
import { ZERO } from '../games/consts/global';
45
import { findSessionByToken } from '../models/Session';
6+
import type { User } from '../models/User';
57
import { findUserById } from '../models/User';
6-
import { CustomRequestThis } from './interfaceAndTypes';
78

8-
export async function getAuthUser(this: CustomRequestThis, req: Request, _h: ResponseToolkit) {
9+
import type { CustomRequestThis } from './interfaceAndTypes';
10+
11+
export async function getAuthUser(
12+
this: CustomRequestThis,
13+
req: Request,
14+
_h: ResponseToolkit
15+
): Promise<User> {
916
// console.log({ something: this }); // Get capabilities from this
1017
const sessionToken = req.headers['xtu-session-token'];
1118
if (!sessionToken) {
@@ -17,13 +24,13 @@ export async function getAuthUser(this: CustomRequestThis, req: Request, _h: Res
1724
}
1825
const user = await findUserById(userSession._userId);
1926
if (!user) {
20-
return Boom.notFound('User not found');
27+
throw Boom.notFound('User not found');
2128
}
2229
const capabilityHeight = this.requiredCapabilities.shift() ?? ZERO;
2330

2431
// This works for now while we design and code the capabilities system
2532
if ((user._roleId ?? ZERO) < capabilityHeight) {
26-
return Boom.unauthorized('Only authorized users can access here');
33+
throw Boom.unauthorized('Only authorized users can access here');
2734
}
2835
return user;
2936
// return h.continue;

src/api-utils/responseSchemas/gamedev.ts src/api-utils/schemas/gameDev/game.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import Joi from 'joi';
22

3+
import { leaderboardSchema } from './leaderboardSchemas';
4+
35
const gameItemSchema = Joi.object({
46
id: Joi.number(),
57
name: Joi.string(),
68
clientSecret: Joi.string(),
79
signingSecret: Joi.string(),
810
_createdById: Joi.number(),
11+
_leaderboards: Joi.array().items(leaderboardSchema),
912
}).optional(); //.options({ stripUnknown: true });
1013

1114
export const sigleGameItemSchema = Joi.object({ game: gameItemSchema }).required(); //.options({ stripUnknown: true });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Joi from 'joi';
2+
3+
import { ScoreStrategy, ResetStrategy } from '../../../models/LeaderboardEntry';
4+
5+
export const getLeaderboardRankResponseSchema = Joi.array()
6+
.items(
7+
Joi.object({
8+
displayName: Joi.string().allow(null).required(),
9+
email: Joi.string().required(),
10+
score: Joi.number().required(),
11+
})
12+
)
13+
.required();
14+
15+
export const getUserLeaderboardResultScoreResponseSchema = Joi.object({
16+
id: Joi.number().required(),
17+
_leaderboardEntryId: Joi.number().required(),
18+
_userId: Joi.number().required(),
19+
score: Joi.number().required(),
20+
}).required();
21+
22+
export const postLeaderboardResultScoreResquestSchema = Joi.object({
23+
id: Joi.number().optional(),
24+
score: Joi.number().required(),
25+
_leaderboardResultsMeta: Joi.array()
26+
.items(
27+
Joi.object({
28+
attribute: Joi.string().required(),
29+
value: Joi.string().required(),
30+
})
31+
)
32+
.optional(),
33+
}).required();
34+
35+
export const postLeaderboardResultScoreResponseSchema = Joi.object({
36+
newEntry: Joi.boolean().required(),
37+
}).required();
38+
39+
export const postLeaderboardSchema = Joi.object({
40+
id: Joi.number().optional(),
41+
name: Joi.string().required(),
42+
scoreStrategy: Joi.string()
43+
.valid(...Object.values(ScoreStrategy))
44+
.optional(),
45+
resetStrategy: Joi.string()
46+
.valid(...Object.values(ResetStrategy))
47+
.optional(),
48+
}).required();
49+
50+
export const leaderboardSchema = Joi.object({
51+
id: Joi.number(),
52+
name: Joi.string(),
53+
scoreStrategy: Joi.string(),
54+
resetStrategy: Joi.string(),
55+
_gameTypeId: Joi.number(),
56+
createdAt: Joi.date(),
57+
updatedAt: Joi.date(),
58+
}).optional();
59+
60+
export const multipleLeaderboardSchema = Joi.array().items(leaderboardSchema).optional();

src/api-utils/responseSchemas/user.ts src/api-utils/schemas/user.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const sessionTokenSchema = Joi.object({
1010
}).options({ stripUnknown: true });
1111

1212
const userSessionSchema = Joi.object({
13-
displayName: Joi.string().required(),
13+
displayName: Joi.string().allow(null).required(),
1414
email: Joi.string().email().required(),
1515
slackId: Joi.string().allow(null).optional(),
1616
firebaseUserUid: Joi.string().allow(null).optional(),

src/api-utils/webhookValidations.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Boom from '@hapi/boom';
22
import type { Request, ResponseToolkit } from '@hapi/hapi';
33
import { isEmpty, isNaN } from 'lodash';
4+
45
import { TWO } from '../games/consts/global';
56
import { findGameTypeByClientSecret } from '../models/GameType';
67
import { isRequestFresh } from '../modules/slack/utils';
@@ -10,7 +11,7 @@ export async function webhookValidation(request: Request, _h: ResponseToolkit) {
1011
if (!isEmpty(request.payload) && !Buffer.isBuffer(request.payload)) {
1112
throw Boom.internal('Payload is not a Buffer');
1213
}
13-
const bodyString = request.payload ? request.payload.toString('utf-8') : {};
14+
const bodyString = request.payload ? request.payload.toString('utf-8') : '{}';
1415
const timestamp = Number(request.headers['xtu-request-timestamp']);
1516
if (isNaN(timestamp) || !isRequestFresh(timestamp, TWO)) {
1617
throw Boom.unauthorized('Invalid timestamp');

src/db.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import type { Transaction } from 'sequelize';
12
import type { Model } from 'sequelize-typescript';
23
import { Sequelize } from 'sequelize-typescript';
34
import { prettify } from 'sql-log-prettifier';
45

5-
import { getConfig, prettifyConfig } from './config';
6+
import { getConfig, prettifyConfig, logger } from './config';
67
import * as models from './models';
78

89
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -44,3 +45,14 @@ export function getAllModels() {
4445
export function getModelByName(name: string) {
4546
return getAllModels()[name];
4647
}
48+
49+
export function withTransaction<T>(fn: (transaction: Transaction) => Promise<T>) {
50+
return sequelize
51+
.transaction((transaction) => {
52+
return fn(transaction);
53+
})
54+
.catch(async (error) => {
55+
logger.error(error);
56+
throw error;
57+
});
58+
}

src/migrations/20220509105325-add-key-pair-for-GameType.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ module.exports = {
8989
return queryInterface.sequelize.transaction(async (transaction) => {
9090
await queryInterface.removeColumn('GameType', 'clientSecret', { transaction });
9191
await queryInterface.removeColumn('GameType', 'signingSecret', { transaction });
92-
await queryInterface.removeColumn('GameType', '_createdBy', { transaction });
92+
await queryInterface.removeColumn('GameType', '_createdById', { transaction });
9393
});
9494
},
9595
};

src/migrations/20220512141820-create-Achievements-table.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { DataTypes, QueryInterface, Sequelize } from 'sequelize';
1+
import type { QueryInterface, Sequelize } from 'sequelize';
2+
import { DataTypes } from 'sequelize';
23

34
interface SequelizeContext {
45
context: {
@@ -99,8 +100,8 @@ module.exports = {
99100

100101
async down({ context: { queryInterface } }: SequelizeContext) {
101102
return queryInterface.sequelize.transaction(async (transaction) => {
102-
await queryInterface.dropTable('Achievement', { transaction });
103103
await queryInterface.dropTable('AchievementUnlocked', { transaction });
104+
await queryInterface.dropTable('Achievement', { transaction });
104105
});
105106
},
106107
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { QueryInterface, Sequelize } from 'sequelize';
2+
import { DataTypes } from 'sequelize';
3+
4+
interface SequelizeContext {
5+
context: {
6+
queryInterface: QueryInterface;
7+
Sequelize: Sequelize;
8+
};
9+
}
10+
11+
module.exports = {
12+
async up({ context: { queryInterface } }: SequelizeContext) {
13+
return queryInterface.sequelize.transaction(async (transaction) => {
14+
await queryInterface.createTable(
15+
'LeaderboardEntry',
16+
{
17+
id: {
18+
allowNull: false,
19+
autoIncrement: true,
20+
primaryKey: true,
21+
type: DataTypes.INTEGER,
22+
},
23+
_gameTypeId: {
24+
type: DataTypes.INTEGER,
25+
allowNull: false,
26+
references: {
27+
model: 'GameType',
28+
key: 'id',
29+
},
30+
},
31+
name: {
32+
allowNull: false,
33+
type: DataTypes.TEXT,
34+
},
35+
scoreStrategy: {
36+
allowNull: false,
37+
type: DataTypes.ENUM('highest', 'lowest', 'sum', 'latest'),
38+
defaultValue: 'highest',
39+
},
40+
resetStrategy: {
41+
allowNull: false,
42+
type: DataTypes.ENUM('daily', 'weekly', 'monthly', 'never'),
43+
defaultValue: 'never',
44+
},
45+
createdAt: {
46+
allowNull: false,
47+
type: DataTypes.DATE,
48+
},
49+
updatedAt: {
50+
allowNull: false,
51+
type: DataTypes.DATE,
52+
},
53+
},
54+
{ transaction }
55+
);
56+
57+
await queryInterface.createTable(
58+
'LeaderboardResults',
59+
{
60+
id: {
61+
allowNull: false,
62+
autoIncrement: true,
63+
primaryKey: true,
64+
type: DataTypes.INTEGER,
65+
},
66+
_leaderboardEntryId: {
67+
type: DataTypes.INTEGER,
68+
allowNull: false,
69+
references: {
70+
model: 'LeaderboardEntry',
71+
key: 'id',
72+
},
73+
},
74+
_userId: {
75+
type: DataTypes.INTEGER,
76+
allowNull: false,
77+
references: {
78+
model: 'User',
79+
key: 'id',
80+
},
81+
},
82+
score: {
83+
allowNull: false,
84+
type: DataTypes.INTEGER,
85+
},
86+
meta: {
87+
allowNull: true,
88+
type: DataTypes.JSON, //perhaps a LeaderboardResults table?
89+
},
90+
createdAt: {
91+
allowNull: false,
92+
type: DataTypes.DATE,
93+
},
94+
updatedAt: {
95+
allowNull: false,
96+
type: DataTypes.DATE,
97+
},
98+
},
99+
{ transaction }
100+
);
101+
});
102+
},
103+
104+
async down({ context: { queryInterface } }: SequelizeContext) {
105+
return queryInterface.sequelize.transaction(async (transaction) => {
106+
await queryInterface.dropTable('LeaderboardResults', { transaction });
107+
await queryInterface.dropTable('LeaderboardEntry', { transaction });
108+
});
109+
},
110+
};

0 commit comments

Comments
 (0)