Skip to content

Commit a733d24

Browse files
authored
Merge 29ab765 into c4aadc9
2 parents c4aadc9 + 29ab765 commit a733d24

13 files changed

+428
-6
lines changed

src/Controllers/ParseGraphQLController.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class ParseGraphQLController {
2424
`ParseGraphQLController requires a "databaseController" to be instantiated.`
2525
);
2626
this.cacheController = params.cacheController;
27-
this.isMounted = !!params.mountGraphQL;
27+
this.isMounted = !!params.mountGraphQL || !!params.mountSubscriptions;
2828
this.configCacheKey = GraphQLConfigKey;
2929
}
3030

@@ -145,7 +145,14 @@ class ParseGraphQLController {
145145
if (!isValidSimpleObject(classConfig)) {
146146
return 'it must be a valid object';
147147
} else {
148-
const { className, type = null, query = null, mutation = null, ...invalidKeys } = classConfig;
148+
const {
149+
className,
150+
type = null,
151+
query = null,
152+
mutation = null,
153+
subscription = null,
154+
...invalidKeys
155+
} = classConfig;
149156
if (Object.keys(invalidKeys).length) {
150157
return `"invalidKeys" [${Object.keys(invalidKeys)}] should not be present`;
151158
}
@@ -287,6 +294,20 @@ class ParseGraphQLController {
287294
return `"mutation" must be a valid object`;
288295
}
289296
}
297+
if (subscription !== null) {
298+
if (isValidSimpleObject(subscription)) {
299+
const { enabled = null, alias = null, ...invalidKeys } = query;
300+
if (Object.keys(invalidKeys).length) {
301+
return `"subscription" contains invalid keys, [${Object.keys(invalidKeys)}]`;
302+
} else if (enabled !== null && typeof enabled !== 'boolean') {
303+
return `"subscription.enabled" must be a boolean`;
304+
} else if (alias !== null && typeof alias !== 'string') {
305+
return `"subscription.alias" must be a string`;
306+
}
307+
} else {
308+
return `"subscription" must be a valid object`;
309+
}
310+
}
290311
}
291312
}
292313
}
@@ -355,6 +376,11 @@ export interface ParseGraphQLClassConfig {
355376
updateAlias: ?String,
356377
destroyAlias: ?String,
357378
};
379+
/* The `subscription` object contains options for which class subscriptions are generated */
380+
subscription: ?{
381+
enabled: ?boolean,
382+
alias: ?String,
383+
};
358384
}
359385

360386
export default ParseGraphQLController;

src/Controllers/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export function getParseGraphQLController(
127127
): ParseGraphQLController {
128128
return new ParseGraphQLController({
129129
mountGraphQL: options.mountGraphQL,
130+
mountSubscriptions: options.mountSubscriptions,
130131
...controllerDeps,
131132
});
132133
}

src/GraphQL/ParseGraphQLSchema.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as defaultGraphQLTypes from './loaders/defaultGraphQLTypes';
77
import * as parseClassTypes from './loaders/parseClassTypes';
88
import * as parseClassQueries from './loaders/parseClassQueries';
99
import * as parseClassMutations from './loaders/parseClassMutations';
10+
import * as parseClassSubscriptions from './loaders/parseClassSubscriptions';
1011
import * as defaultGraphQLQueries from './loaders/defaultGraphQLQueries';
1112
import * as defaultGraphQLMutations from './loaders/defaultGraphQLMutations';
1213
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
@@ -58,6 +59,7 @@ const RESERVED_GRAPHQL_MUTATION_NAMES = [
5859
'updateClass',
5960
'deleteClass',
6061
];
62+
const RESERVED_GRAPHQL_SUBSCRIPTION_NAMES = [];
6163

6264
class ParseGraphQLSchema {
6365
databaseController: DatabaseController;
@@ -66,6 +68,7 @@ class ParseGraphQLSchema {
6668
log: any;
6769
appId: string;
6870
graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]);
71+
liveQueryClassNames: any;
6972

7073
constructor(
7174
params: {
@@ -74,6 +77,7 @@ class ParseGraphQLSchema {
7477
log: any,
7578
appId: string,
7679
graphQLCustomTypeDefs: ?(string | GraphQLSchema | DocumentNode | GraphQLNamedType[]),
80+
liveQueryClassNames: any,
7781
} = {}
7882
) {
7983
this.parseGraphQLController =
@@ -85,6 +89,7 @@ class ParseGraphQLSchema {
8589
this.log = params.log || requiredParameter('You must provide a log instance!');
8690
this.graphQLCustomTypeDefs = params.graphQLCustomTypeDefs;
8791
this.appId = params.appId || requiredParameter('You must provide the appId!');
92+
this.liveQueryClassNames = params.liveQueryClassNames;
8893
}
8994

9095
async load() {
@@ -132,6 +137,9 @@ class ParseGraphQLSchema {
132137
parseClassTypes.load(this, parseClass, parseClassConfig);
133138
parseClassQueries.load(this, parseClass, parseClassConfig);
134139
parseClassMutations.load(this, parseClass, parseClassConfig);
140+
if (this.liveQueryClassNames && this.liveQueryClassNames.includes(parseClass.className)) {
141+
parseClassSubscriptions.load(this, parseClass, parseClassConfig);
142+
}
135143
}
136144
);
137145

@@ -342,6 +350,22 @@ class ParseGraphQLSchema {
342350
return field;
343351
}
344352
353+
addGraphQLSubscription(fieldName, field, throwError = false, ignoreReserved = false) {
354+
if (
355+
(!ignoreReserved && RESERVED_GRAPHQL_SUBSCRIPTION_NAMES.includes(fieldName)) ||
356+
this.graphQLSubscriptions[fieldName]
357+
) {
358+
const message = `Subscription ${fieldName} could not be added to the auto schema because it collided with an existing field.`;
359+
if (throwError) {
360+
throw new Error(message);
361+
}
362+
this.log.warn(message);
363+
return undefined;
364+
}
365+
this.graphQLSubscriptions[fieldName] = field;
366+
return field;
367+
}
368+
345369
handleError(error) {
346370
if (error instanceof Parse.Error) {
347371
this.log.error('Parse error: ', error);

src/GraphQL/ParseGraphQLServer.js

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { SubscriptionServer } from 'subscriptions-transport-ws';
88
import { handleParseErrors, handleParseHeaders } from '../middlewares';
99
import requiredParameter from '../requiredParameter';
1010
import defaultLogger from '../logger';
11+
import { ParseLiveQueryServer } from '../LiveQuery/ParseLiveQueryServer';
1112
import { ParseGraphQLSchema } from './ParseGraphQLSchema';
1213
import ParseGraphQLController, { ParseGraphQLConfig } from '../Controllers/ParseGraphQLController';
14+
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
1315

1416
class ParseGraphQLServer {
1517
parseGraphQLController: ParseGraphQLController;
@@ -29,6 +31,8 @@ class ParseGraphQLServer {
2931
log: this.log,
3032
graphQLCustomTypeDefs: this.config.graphQLCustomTypeDefs,
3133
appId: this.parseServer.config.appId,
34+
liveQueryClassNames:
35+
this.parseServer.config.liveQuery && this.parseServer.config.liveQuery.classNames,
3236
});
3337
}
3438

@@ -114,12 +118,132 @@ class ParseGraphQLServer {
114118
}
115119

116120
createSubscriptions(server) {
121+
const wssAdapter = new WSSAdapter();
122+
123+
new ParseLiveQueryServer(
124+
undefined,
125+
{
126+
...this.parseServer.config.liveQueryServerOptions,
127+
wssAdapter,
128+
},
129+
this.parseServer.config
130+
);
131+
117132
SubscriptionServer.create(
118133
{
119134
execute,
120135
subscribe,
121-
onOperation: async (_message, params, webSocket) =>
122-
Object.assign({}, params, await this._getGraphQLOptions(webSocket.upgradeReq)),
136+
onConnect: async connectionParams => {
137+
const keyPairs = {
138+
applicationId: connectionParams['X-Parse-Application-Id'],
139+
sessionToken: connectionParams['X-Parse-Session-Token'],
140+
masterKey: connectionParams['X-Parse-Master-Key'],
141+
installationId: connectionParams['X-Parse-Installation-Id'],
142+
clientKey: connectionParams['X-Parse-Client-Key'],
143+
javascriptKey: connectionParams['X-Parse-Javascript-Key'],
144+
windowsKey: connectionParams['X-Parse-Windows-Key'],
145+
restAPIKey: connectionParams['X-Parse-REST-API-Key'],
146+
};
147+
148+
const listeners = [];
149+
150+
let connectResolve, connectReject;
151+
let connectIsResolved = false;
152+
const connectPromise = new Promise((resolve, reject) => {
153+
connectResolve = resolve;
154+
connectReject = reject;
155+
});
156+
157+
const liveQuery = {
158+
OPEN: 'OPEN',
159+
readyState: 'OPEN',
160+
on: () => {},
161+
ping: () => {},
162+
onmessage: () => {},
163+
onclose: () => {},
164+
send: message => {
165+
message = JSON.parse(message);
166+
if (message.op === 'connected') {
167+
connectResolve();
168+
connectIsResolved = true;
169+
return;
170+
} else if (message.op === 'error' && !connectIsResolved) {
171+
connectReject({
172+
code: message.code,
173+
message: message.error,
174+
});
175+
return;
176+
}
177+
const requestId = message && message.requestId;
178+
if (
179+
requestId &&
180+
typeof requestId === 'number' &&
181+
requestId > 0 &&
182+
requestId <= listeners.length
183+
) {
184+
const listener = listeners[requestId - 1];
185+
if (listener) {
186+
listener(message);
187+
}
188+
}
189+
},
190+
subscribe: async (query, sessionToken, listener) => {
191+
await connectPromise;
192+
listeners.push(listener);
193+
liveQuery.onmessage(
194+
JSON.stringify({
195+
op: 'subscribe',
196+
requestId: listeners.length,
197+
query,
198+
sessionToken,
199+
})
200+
);
201+
},
202+
unsubscribe: async listener => {
203+
await connectPromise;
204+
const index = listeners.indexOf(listener);
205+
if (index > 0) {
206+
liveQuery.onmessage(
207+
JSON.stringify({
208+
op: 'unsubscribe',
209+
requestId: index + 1,
210+
})
211+
);
212+
listeners[index] = null;
213+
}
214+
},
215+
};
216+
217+
wssAdapter.onConnection(liveQuery);
218+
219+
liveQuery.onmessage(
220+
JSON.stringify({
221+
op: 'connect',
222+
...keyPairs,
223+
})
224+
);
225+
226+
await connectPromise;
227+
228+
return { liveQuery, keyPairs };
229+
},
230+
onDisconnect: (_webSocket, context) => {
231+
const { liveQuery } = context;
232+
233+
if (liveQuery) {
234+
liveQuery.onclose();
235+
}
236+
},
237+
onOperation: async (_message, params) => {
238+
return {
239+
...params,
240+
schema: await this.parseGraphQLSchema.load(),
241+
formatError: error => {
242+
// Allow to console.log here to debug
243+
return error;
244+
},
245+
};
246+
},
123247
},
124248
{
125249
server,

src/GraphQL/loaders/defaultGraphQLTypes.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,29 @@ const loadArrayResult = (parseGraphQLSchema, parseClasses) => {
12221222
parseGraphQLSchema.graphQLTypes.push(ARRAY_RESULT);
12231223
};
12241224

1225+
const EVENT_KIND = new GraphQLEnumType({
1226+
name: 'EventKind',
1227+
description: 'The EventKind enum type is used in subscriptions to identify listened events.',
1228+
values: {
1229+
create: { value: 'create' },
1230+
enter: { value: 'enter' },
1231+
update: { value: 'update' },
1232+
leave: { value: 'leave' },
1233+
delete: { value: 'delete' },
1234+
all: { value: 'all' },
1235+
},
1236+
});
1237+
1238+
const EVENT_KIND_ATT = {
1239+
description: 'The event kind that was fired.',
1240+
type: new GraphQLNonNull(EVENT_KIND),
1241+
};
1242+
1243+
const EVENT_KINDS_ATT = {
1244+
description: 'The event kinds to be listened in the subscription.',
1245+
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EVENT_KIND))),
1246+
};
1247+
12251248
const load = parseGraphQLSchema => {
12261249
parseGraphQLSchema.addGraphQLType(GraphQLUpload, true);
12271250
parseGraphQLSchema.addGraphQLType(ANY, true);
@@ -1266,6 +1289,7 @@ const load = parseGraphQLSchema => {
12661289
parseGraphQLSchema.addGraphQLType(PUBLIC_ACL, true);
12671290
parseGraphQLSchema.addGraphQLType(SUBQUERY_INPUT, true);
12681291
parseGraphQLSchema.addGraphQLType(SELECT_INPUT, true);
1292+
parseGraphQLSchema.addGraphQLType(EVENT_KIND, true);
12691293
};
12701294

12711295
export {
@@ -1358,6 +1382,9 @@ export {
13581382
USER_ACL,
13591383
ROLE_ACL,
13601384
PUBLIC_ACL,
1385+
EVENT_KIND,
1386+
EVENT_KIND_ATT,
1387+
EVENT_KINDS_ATT,
13611388
load,
13621389
loadArrayResult,
13631390
};

0 commit comments

Comments
 (0)