Skip to content
This repository was archived by the owner on Oct 11, 2022. It is now read-only.

Commit 0e5c074

Browse files
authored
Merge pull request #4015 from withspectrum/2.4.42
2.4.42
2 parents 09fc604 + 81a5cff commit 0e5c074

File tree

82 files changed

+1981
-1134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+1981
-1134
lines changed

.circleci/config.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,21 @@ jobs:
102102
at: ~/spectrum
103103
- run: yarn run db:migrate
104104
- run: yarn run db:seed
105+
- run:
106+
name: Run Unit Tests
107+
command: yarn run test:ci
105108
- run: *setup-and-build-web
106109
- run: *build-api
107110
- run: *start-api
108111
- run: *start-web
109112
# Wait for the API and webserver to start
110113
- run: ./node_modules/.bin/wait-on http://localhost:3000 http://localhost:3001
111114
- run:
112-
name: Run Unit Tests
113-
command: yarn run test:ci
115+
name: Run E2E Tests
116+
command: test $CYPRESS_RECORD_KEY && yarn run test:e2e -- --record || yarn run test:e2e
114117
- run:
115118
name: Build desktop apps
116119
command: yarn run build:desktop
117-
- run:
118-
name: Run E2E Tests
119-
command: test $CYPRESS_RECORD_KEY && yarn run test:e2e -- --record || yarn run test:e2e
120120

121121
deploy_alpha:
122122
<<: *js_defaults

admin/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"apollo-link-http": "^1.3.2",
2626
"apollo-link-ws": "^1.0.4",
2727
"apollo-upload-client": "6.x",
28-
"apollo-upload-server": "^2.0.4",
2928
"apollo-utilities": "^1.0.4",
3029
"d3-array": "^1.2.0",
3130
"graphql": "^0.12.3",

api/apollo-server.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// @flow
2+
import { ApolloServer } from 'apollo-server-express';
3+
import depthLimit from 'graphql-depth-limit';
4+
import costAnalysis from 'graphql-cost-analysis';
5+
import createLoaders from './loaders';
6+
import createErrorFormatter from './utils/create-graphql-error-formatter';
7+
import schema from './schema';
8+
import { getUser, setUserOnline } from './models/user';
9+
import { getUserIdFromReq } from './utils/session-store';
10+
import UserError from './utils/UserError';
11+
import type { DBUser } from 'shared/types';
12+
13+
// NOTE(@mxstbr): Evil hack to make graphql-cost-analysis work with Apollo Server v2
14+
// @see pa-bru/graphql-cost-analysis#12
15+
// @author @arianon
16+
class ProtectedApolloServer extends ApolloServer {
17+
async createGraphQLServerOptions(
18+
req: express$Request,
19+
res: express$Response
20+
): Promise<*> {
21+
const options = await super.createGraphQLServerOptions(req, res);
22+
23+
return {
24+
...options,
25+
validationRules: [
26+
...options.validationRules,
27+
costAnalysis({
28+
maximumCost: 750,
29+
defaultCost: 1,
30+
variables: req.body.variables,
31+
createError: (max, actual) => {
32+
const err = new UserError(
33+
`GraphQL query exceeds maximum complexity, please remove some nesting or fields and try again. (max: ${max}, actual: ${actual})`
34+
);
35+
return err;
36+
},
37+
}),
38+
],
39+
};
40+
}
41+
}
42+
43+
const server = new ProtectedApolloServer({
44+
schema,
45+
formatError: createErrorFormatter(),
46+
// For subscriptions, this gets passed "connection", for everything else "req" and "res"
47+
context: ({ req, res, connection, ...rest }, ...other) => {
48+
if (connection) {
49+
return {
50+
...(connection.context || {}),
51+
};
52+
}
53+
54+
const loaders = createLoaders();
55+
let currentUser = req.user && !req.user.bannedAt ? req.user : null;
56+
57+
return {
58+
loaders,
59+
updateCookieUserData: (data: DBUser) =>
60+
new Promise((res, rej) =>
61+
req.login(data, err => (err ? rej(err) : res()))
62+
),
63+
user: currentUser,
64+
};
65+
},
66+
subscriptions: {
67+
path: '/websocket',
68+
onOperation: (_: any, params: Object) => {
69+
const errorFormatter = createErrorFormatter();
70+
params.formatError = errorFormatter;
71+
return params;
72+
},
73+
onDisconnect: rawSocket => {
74+
return getUserIdFromReq(rawSocket.upgradeReq)
75+
.then(id => id && setUserOnline(id, false))
76+
.catch(err => {
77+
console.error(err);
78+
});
79+
},
80+
onConnect: (connectionParams, rawSocket) =>
81+
getUserIdFromReq(rawSocket.upgradeReq)
82+
.then(id => (id ? setUserOnline(id, true) : null))
83+
.then(user => {
84+
return {
85+
user: user || null,
86+
loaders: createLoaders({ cache: false }),
87+
};
88+
})
89+
.catch(err => {
90+
console.error(err);
91+
return {
92+
loaders: createLoaders({ cache: false }),
93+
};
94+
}),
95+
},
96+
playground: {
97+
settings: {
98+
'editor.theme': 'light',
99+
},
100+
tabs: [
101+
{
102+
endpoint: 'http://localhost:3001/api',
103+
query: `{
104+
user(username: "mxstbr") {
105+
id
106+
username
107+
}
108+
}`,
109+
},
110+
],
111+
},
112+
maxFileSize: 25 * 1024 * 1024, // 25MB
113+
engine: false,
114+
tracing: true,
115+
cacheControl: true,
116+
validationRules: [depthLimit(10)],
117+
});
118+
119+
export default server;

api/index.js

Lines changed: 28 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,63 +14,51 @@ import toobusy from 'shared/middlewares/toobusy';
1414
import addSecurityMiddleware from 'shared/middlewares/security';
1515
import csrf from 'shared/middlewares/csrf';
1616
import { init as initPassport } from './authentication.js';
17+
import apolloServer from './apollo-server';
18+
import { corsOptions } from 'shared/middlewares/cors';
19+
import errorHandler from 'shared/middlewares/error-handler';
20+
import middlewares from './routes/middlewares';
21+
import authRoutes from './routes/auth';
22+
import apiRoutes from './routes/api';
1723
import type { DBUser } from 'shared/types';
24+
import type { Loader } from './loaders/types';
25+
26+
export type GraphQLContext = {
27+
user: DBUser,
28+
updateCookieUserData: (data: DBUser) => Promise<void>,
29+
loaders: {
30+
[key: string]: Loader,
31+
},
32+
};
1833

1934
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
2035

21-
// Initialize authentication
2236
initPassport();
2337

24-
// API server
2538
const app = express();
2639

2740
// Trust the now proxy
2841
app.set('trust proxy', true);
29-
30-
// Return the request if the server is too busy
3142
app.use(toobusy);
3243

3344
// Security middleware.
3445
addSecurityMiddleware(app);
35-
3646
if (process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV) {
3747
app.use(csrf);
3848
}
3949

40-
// Send all responses as gzip
50+
// All other middlewares
4151
app.use(compression());
42-
43-
import middlewares from './routes/middlewares';
4452
app.use(middlewares);
4553

46-
import authRoutes from './routes/auth';
54+
// Routes
4755
app.use('/auth', authRoutes);
48-
49-
import apiRoutes from './routes/api';
5056
app.use('/api', apiRoutes);
5157

52-
// $FlowIssue
53-
app.use(
54-
(
55-
err: Error,
56-
req: express$Request,
57-
res: express$Response,
58-
next: express$NextFunction
59-
) => {
60-
if (err) {
61-
console.error(err);
62-
res
63-
.status(500)
64-
.send(
65-
'Oops, something went wrong! Our engineers have been alerted and will fix this asap.'
66-
);
67-
Raven.captureException(err);
68-
} else {
69-
return next();
70-
}
71-
}
72-
);
58+
// GraphQL middleware
59+
apolloServer.applyMiddleware({ app, path: '/api', cors: corsOptions });
7360

61+
// Redirect a request to the root path to the main app
7462
app.use('/', (req: express$Request, res: express$Response) => {
7563
res.redirect(
7664
process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV
@@ -79,20 +67,12 @@ app.use('/', (req: express$Request, res: express$Response) => {
7967
);
8068
});
8169

82-
import type { Loader } from './loaders/types';
83-
export type GraphQLContext = {
84-
user: DBUser,
85-
updateCookieUserData: (data: DBUser) => Promise<void>,
86-
loaders: {
87-
[key: string]: Loader,
88-
},
89-
};
90-
91-
const server = createServer(app);
70+
// $FlowIssue
71+
app.use(errorHandler);
9272

93-
// Create subscriptions server at /websocket
94-
import createSubscriptionsServer from './routes/create-subscription-server';
95-
const subscriptionsServer = createSubscriptionsServer(server, '/websocket');
73+
// We need to create a separate HTTP server to handle GraphQL subscriptions via websockets
74+
const httpServer = createServer(app);
75+
apolloServer.installSubscriptionHandlers(httpServer);
9676

9777
// Start API wrapped in Apollo Engine
9878
const engine = new ApolloEngine({
@@ -119,10 +99,11 @@ const engine = new ApolloEngine({
11999

120100
engine.listen({
121101
port: PORT,
122-
httpServer: server,
102+
httpServer: httpServer,
123103
graphqlPaths: ['/api'],
124104
});
125-
debug(`GraphQL server running at http://localhost:${PORT}/api`);
105+
106+
debug(`GraphQL API running at http://localhost:${PORT}/api`);
126107

127108
process.on('unhandledRejection', async err => {
128109
console.error('Unhandled rejection', err);

api/loaders/channel.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export const __createChannelThreadCountLoader = createLoader(
1717
'group'
1818
);
1919

20-
export const __createChannelMemberCountLoader = createLoader(
21-
channels => getChannelsMemberCounts(channels),
20+
export const __createChannelPendingMembersLoader = createLoader(
21+
channels => getPendingUsersInChannels(channels),
2222
'group'
2323
);
2424

25-
export const __createChannelPendingMembersLoader = createLoader(
26-
channels => getPendingUsersInChannels(channels),
25+
export const __createChannelMemberCountLoader = createLoader(
26+
channels => getChannelsMemberCounts(channels),
2727
'group'
2828
);
2929

api/loaders/community.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import {
33
getCommunities,
44
getCommunitiesBySlug,
5-
getCommunitiesMemberCounts,
65
getCommunitiesChannelCounts,
76
getCommunitiesOnlineMemberCounts,
7+
getCommunitiesMemberCounts,
88
} from '../models/community';
99
import { getCommunitiesSettings } from '../models/communitySettings';
1010
import createLoader from './create-loader';

api/loaders/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ import {
1515
import { __createNotificationLoader } from './notification';
1616
import {
1717
__createChannelLoader,
18-
__createChannelMemberCountLoader,
1918
__createChannelThreadCountLoader,
19+
__createChannelMemberCountLoader,
2020
__createChannelPendingMembersLoader,
2121
__createChannelSettingsLoader,
2222
} from './channel';
2323
import {
2424
__createCommunityLoader,
2525
__createCommunityBySlugLoader,
26-
__createCommunityMemberCountLoader,
2726
__createCommunityChannelCountLoader,
2827
__createCommunitySettingsLoader,
28+
__createCommunityMemberCountLoader,
2929
__createCommunityOnlineMemberCountLoader,
3030
} from './community';
3131
import {

api/loaders/types.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export type Loader = {
44
load: (key: string | Array<string>) => Promise<any>,
5-
loadMany: (keys: Array<string>) => Promise<any>,
5+
loadMany: (keys: Array<*>) => Promise<any>,
66
clear: (key: string | Array<string>) => void,
77
};
88

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
exports.up = function(r, conn) {
2+
return Promise.all([
3+
r
4+
.table('communities')
5+
.update(
6+
{
7+
memberCount: r
8+
.table('usersCommunities')
9+
.getAll(r.row('id'), { index: 'communityId' })
10+
.filter(row => row('isMember').eq(true))
11+
.count()
12+
.default(1),
13+
},
14+
{
15+
nonAtomic: true,
16+
}
17+
)
18+
.run(conn),
19+
r
20+
.table('channels')
21+
.update(
22+
{
23+
memberCount: r
24+
.table('usersChannels')
25+
.getAll(r.row('id'), { index: 'channelId' })
26+
.filter(row => row('isMember').eq(true))
27+
.count()
28+
.default(1),
29+
},
30+
{
31+
nonAtomic: true,
32+
}
33+
)
34+
.run(conn),
35+
]).catch(err => console.error(err));
36+
};
37+
exports.down = function(r, conn) {
38+
return Promise.all([
39+
r
40+
.table('communities')
41+
.update({
42+
memberCount: r.literal(),
43+
})
44+
.run(conn),
45+
r
46+
.table('channels')
47+
.update({
48+
memberCount: r.literal(),
49+
})
50+
.run(conn),
51+
]);
52+
};

0 commit comments

Comments
 (0)