Skip to content

Commit a0af7f1

Browse files
committed
Added fgraphql hook.
1 parent a87e7df commit a0af7f1

File tree

6 files changed

+1114
-0
lines changed

6 files changed

+1114
-0
lines changed

lib/services/fgraphql.js

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
2+
const makeDebug = require('debug');
3+
const getItems = require('./get-items');
4+
const replaceItems = require('./replace-items');
5+
6+
const debug = makeDebug('fgraphql');
7+
const graphqlActions = ['Query', 'Mutation', 'Subscription'];
8+
9+
module.exports = function fgraphql (options1 = {}) {
10+
debug('init call');
11+
const { parse, recordType, resolvers, runTime } = options1;
12+
let { schema, query: query1 } = options1;
13+
14+
let ourResolvers; // will be initialized when hook is first called
15+
16+
const options = Object.assign({}, {
17+
skipHookWhen: context => !!(context.params || {}).graphql,
18+
inclAllFieldsServer: true,
19+
inclAllFieldsClient: true,
20+
inclAllFields: null, // Will be initialized each hook call.
21+
extraAuthProps: []
22+
}, options1.options || {});
23+
24+
schema = isFunction(schema) ? schema() : schema;
25+
26+
if (!isObject(schema) && !isString(schema)) {
27+
throwError(`Resolved schema is typeof ${typeof schema} rather than string or object. (fgraphql)`, 101);
28+
}
29+
30+
if (!isObject(runTime)) {
31+
throwError(`option runTime is typeof ${typeof runTime} rather than an object. (fgraphql)`, 106);
32+
}
33+
34+
if (!isString(recordType)) {
35+
throwError(`recordType is typeof ${typeof recordType} rather than string. (fgraphql)`, 103);
36+
}
37+
38+
if (!isArray(options.extraAuthProps)) {
39+
throwError(`option extraAuthProps is typeof ${typeof options.extraAuthProps} rather than array. (fgraphql)`, 105);
40+
}
41+
42+
const feathersSdl = isObject(schema) ? schema : convertSdlToFeathersSchemaObject(schema, parse);
43+
debug('schema now in internal form');
44+
45+
// Return the hook.
46+
return async (context) => {
47+
const contextParams = context.params;
48+
const optSkipHookWhen = options.skipHookWhen;
49+
const skipHookWhen = isFunction(optSkipHookWhen) ? optSkipHookWhen(context) : optSkipHookWhen;
50+
debug(`\n.....hook called. type ${context.type} method ${context.method} resolved skipHookWhen ${skipHookWhen}`);
51+
52+
if (context.params.$populate) return context; // populate or fastJoin are running
53+
if (skipHookWhen) return context;
54+
55+
const query = isFunction(query1) ? query1(context) : query1;
56+
57+
if (!isObject(query)) {
58+
throwError(`Resolved query is typeof ${typeof query} rather than object. (fgraphql)`, 102);
59+
}
60+
61+
if (!ourResolvers) {
62+
ourResolvers = resolvers(context.app, runTime);
63+
debug(`ourResolvers has Types ${Object.keys(ourResolvers)}`);
64+
}
65+
66+
if (!ourResolvers[recordType]) {
67+
throwError(`recordType ${recordType} not found in resolvers. (fgraphql)`, 104);
68+
}
69+
70+
options.inclAllFields = contextParams.provider
71+
? options.inclAllFieldsClient : options.inclAllFieldsServer;
72+
debug(`inclAllField ${options.inclAllFields}`);
73+
74+
// Build content parameter passed to resolver functions.
75+
const resolverContent = {
76+
app: context.app,
77+
provider: contextParams.provider,
78+
user: contextParams.user,
79+
authenticated: contextParams.authenticated,
80+
batchLoaders: {},
81+
cache: {}
82+
};
83+
84+
(options.extraAuthProps || []).forEach(name => {
85+
if (name in contextParams && !(name in resolverContent)) {
86+
resolverContent[name] = contextParams[name];
87+
}
88+
});
89+
90+
// Static values used by fgraphql functions.
91+
const store = {
92+
feathersSdl,
93+
ourResolvers,
94+
options,
95+
resolverContent
96+
};
97+
98+
// Populate data.
99+
const recs = getItems(context);
100+
101+
await processRecords(store, query, recs, recordType);
102+
103+
replaceItems(context, recs);
104+
return context;
105+
};
106+
};
107+
108+
// Process records recursively.
109+
async function processRecords (store, query, recs, type, depth = 0) {
110+
if (!recs) return; // Catch no data to populate.
111+
112+
recs = isArray(recs) ? recs : [recs];
113+
debug(`\nvvvvvvvvvv enter ${depth}`);
114+
debug(`processRecords depth ${depth} #recs ${recs.length} Type ${type}`);
115+
116+
const storeOurResolversType = store.ourResolvers[type];
117+
if (!isObject(storeOurResolversType)) {
118+
throwError(`Resolvers for Type ${type} are typeof ${typeof storeOurResolversType} not object. (fgraphql)`, 201);
119+
}
120+
121+
if (!isObject(query)) {
122+
throwError(`query at Type ${type} are typeof ${typeof query} not object. (fgraphql)`, 202);
123+
}
124+
125+
for (let j = 0, jlen = recs.length; j < jlen; j++) {
126+
await processRecord(store, query, depth, recs[j], type, j);
127+
}
128+
129+
debug(`^^^^^^^^^^ exit ${depth}\n`);
130+
}
131+
132+
// Process the a record.
133+
async function processRecord (store, query, depth, rec, type, j) {
134+
debug(`processRecord rec# ${j} typeof ${typeof rec} Type ${type}`);
135+
if (!rec) return; // Catch any null values from resolvers.
136+
137+
const allNamesInQuery = [];
138+
const recFieldNamesInQuery = [];
139+
140+
// Process every query item.
141+
const queryPropNames = Object.keys(query);
142+
for (let i = 0, ilen = queryPropNames.length; i < ilen; i++) {
143+
const fieldName = queryPropNames[i];
144+
await processRecordQuery(store, query, depth, rec, fieldName, type, allNamesInQuery, recFieldNamesInQuery, j, i);
145+
}
146+
147+
// Retain only record fields selected
148+
debug(`field names found ${allNamesInQuery}`);
149+
if (recFieldNamesInQuery.length || !store.options.inclAllFields || queryPropNames.includes('_none')) {
150+
// recs[0] may have been created by [rec] so can't replace array elem
151+
Object.keys(rec).forEach(key => {
152+
if (!allNamesInQuery.includes(key)) {
153+
delete rec[key];
154+
}
155+
});
156+
}
157+
}
158+
159+
// Process one query field for a record.
160+
async function processRecordQuery (store, query, depth, rec, fieldName, type, allNamesInQuery, recFieldNamesInQuery, j, i) {
161+
debug(`\nprocessRecordQuery rec# ${j} Type ${type} field# ${i} name ${fieldName}`);
162+
163+
// One way to include/exclude rec fields is to give their names a falsey value.
164+
// _args and _none are not record field names but special purpose
165+
if (query[fieldName] && fieldName !== '_args' && fieldName !== '_none') {
166+
allNamesInQuery.push(fieldName);
167+
168+
if (store.ourResolvers[type][fieldName]) {
169+
await processRecordFieldResolver(store, query, depth, rec, fieldName, type);
170+
} else {
171+
debug('is not resolver call');
172+
recFieldNamesInQuery.push(fieldName);
173+
}
174+
}
175+
}
176+
177+
// Process a resolver call.
178+
async function processRecordFieldResolver (store, query, depth, rec, fieldName, type) {
179+
debug('is resolver call');
180+
const ourQuery = store.feathersSdl[type][fieldName];
181+
const ourResolver = store.ourResolvers[type][fieldName];
182+
let args;
183+
184+
if (!isFunction(ourResolver)) {
185+
throwError(`Resolver for Type ${type} fieldName ${fieldName} is typeof ${typeof ourResolver} not function. (fgraphql)`, 203);
186+
}
187+
188+
args = isObject(query[fieldName]) ? query[fieldName]._args : undefined;
189+
debug(`resolver listType ${ourQuery.listType} args ${JSON.stringify(args)}`);
190+
191+
// Call resolver function.
192+
const rawResult = await ourResolver(rec, args || {}, store.resolverContent);
193+
debug(`resolver returned typeof ${isArray(rawResult) ? `array #recs ${rawResult.length}` : typeof rawResult}`);
194+
195+
// Convert rawResult to query requirements.
196+
const result = convertResolverResult(rawResult, ourQuery, fieldName, type);
197+
if (isArray(rawResult !== isArray(result) || typeof rawResult !== typeof result)) {
198+
debug(`.....resolver result converted to typeof ${isArray(result) ? `array #recs ${result.length}` : typeof result}`);
199+
}
200+
rec[fieldName] = result;
201+
202+
const nextType = ourQuery.typeof;
203+
debug(`Type ${type} fieldName ${fieldName} next Type ${nextType}`);
204+
205+
// Populate returned records if their query defn has more fields or Types.
206+
// Ignore resolvers returning base values like string.
207+
if (store.ourResolvers[nextType] && isObject(query[fieldName])) {
208+
await processRecords(store, query[fieldName], result, nextType, depth + 1);
209+
} else {
210+
debug('no population of results required');
211+
}
212+
}
213+
214+
// Convert result of resolver function to match query field requirements.
215+
function convertResolverResult (result, ourQuery, fieldName, type) {
216+
if (result === null || result === undefined) {
217+
return ourQuery.listType ? [] : null;
218+
}
219+
220+
if (ourQuery.listType) {
221+
if (!isArray(result)) return [result];
222+
} else if (isArray(result)) {
223+
if (result.length > 1) {
224+
throwError(`Query listType true. Resolver for Type ${type} fieldName ${fieldName} result is array len ${result.length} (fgraphql)`, 204);
225+
}
226+
227+
return result[0];
228+
}
229+
230+
return result;
231+
}
232+
233+
function convertSdlToFeathersSchemaObject (schemaDefinitionLanguage, parse) {
234+
const graphQLSchemaObj = parse(schemaDefinitionLanguage);
235+
// inspector('graphQLSchemaObj', graphQLSchemaObj)
236+
return convertDocument(graphQLSchemaObj);
237+
}
238+
239+
function convertDocument (ast) {
240+
const result = {};
241+
242+
if (ast.kind !== 'Document' || !isArray(ast.definitions)) {
243+
throw new Error('Not a valid GraphQL Document.');
244+
}
245+
246+
ast.definitions.forEach((definition, definitionIndex) => {
247+
const [objectName, converted] = convertObjectTypeDefinition(definition, definitionIndex);
248+
249+
if (objectName) {
250+
result[objectName] = converted;
251+
}
252+
});
253+
254+
return result;
255+
}
256+
257+
function convertObjectTypeDefinition (definition, definitionIndex) {
258+
const converted = {};
259+
260+
if (definition.kind !== 'ObjectTypeDefinition' || !isArray(definition.fields)) {
261+
throw new Error(`Type# ${definitionIndex} is not a valid ObjectTypeDefinition`);
262+
}
263+
264+
const objectTypeName = convertName(definition.name, `Type# ${definitionIndex}`);
265+
if (graphqlActions.includes(objectTypeName)) return [null, null];
266+
267+
definition.fields.forEach(field => {
268+
const [fieldName, fieldDefinition] = convertFieldDefinition(field, `Type ${objectTypeName}`);
269+
converted[fieldName] = fieldDefinition;
270+
});
271+
272+
return [objectTypeName, converted];
273+
}
274+
275+
function convertName (nameObj, errDesc) {
276+
if (!isObject(nameObj) || !isString(nameObj.value)) {
277+
throw new Error(`${errDesc} does not have a valid name prop.`);
278+
}
279+
280+
return nameObj.value;
281+
}
282+
283+
function convertFieldDefinition (field, errDesc) {
284+
if (field.kind !== 'FieldDefinition' || !isObject(field.type)) {
285+
throw new Error(`${errDesc} is not a valid ObjectTypeDefinition`);
286+
}
287+
288+
const fieldName = convertName(field.name, errDesc);
289+
const converted = convertFieldDefinitionType(field.type, errDesc);
290+
converted.inputValues = field.arguments && field.arguments.length !== 0;
291+
292+
return [fieldName, converted];
293+
}
294+
295+
function convertFieldDefinitionType (fieldDefinitionType, errDesc, converted) {
296+
converted = converted || { nonNullTypeList: false, listType: false, nonNullTypeField: false, typeof: null };
297+
298+
if (!isObject(fieldDefinitionType)) {
299+
throw new Error(`${errDesc} is not a valid Fielddefinition "type".`);
300+
}
301+
302+
switch (fieldDefinitionType.kind) {
303+
case 'NamedType':
304+
converted.typeof = convertName(fieldDefinitionType.name);
305+
return converted;
306+
case 'NonNullType':
307+
if (fieldDefinitionType.type.kind === 'NamedType') {
308+
converted.nonNullTypeField = true;
309+
} else {
310+
converted.nonNullTypeList = true;
311+
}
312+
313+
return convertFieldDefinitionType(fieldDefinitionType.type, errDesc, converted);
314+
case 'ListType':
315+
converted.listType = true;
316+
return convertFieldDefinitionType(fieldDefinitionType.type, errDesc, converted);
317+
}
318+
}
319+
320+
function throwError (msg, code) {
321+
const err = new Error(msg);
322+
err.code = code;
323+
throw err;
324+
}
325+
326+
function isObject (obj) {
327+
return typeof obj === 'object' && obj !== null;
328+
}
329+
330+
function isString (str) {
331+
return typeof str === 'string';
332+
}
333+
334+
function isFunction (func) {
335+
return typeof func === 'function';
336+
}
337+
338+
function isArray (array) {
339+
return Array.isArray(array);
340+
}

lib/services/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const discard = require('./discard');
2121
const discardQuery = require('./discard-query');
2222
const existsByDot = require('../common/exists-by-dot');
2323
const fastJoin = require('./fast-join');
24+
const fgraphql = require('./fgraphql');
2425
const getByDot = require('../common/get-by-dot');
2526
const getItems = require('./get-items');
2627
const isProvider = require('./is-provider');
@@ -85,6 +86,7 @@ module.exports = Object.assign({ callbackToPromise,
8586
discardQuery,
8687
existsByDot,
8788
fastJoin,
89+
fgraphql,
8890
getByDot,
8991
getItems,
9092
isProvider,

0 commit comments

Comments
 (0)