46 |
47 |
48 |
49 |
60 |
61 |
97 |
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [ARCHIVED] GraphQL for Microsoft Graph (DEMO)
2 |
3 | **Note:** This repo is archived and no longer actively maintained. Security vulnerabilities may exist in the project, or its dependencies. If you plan to reuse or run any code from this repo, be sure to perform appropriate security checks on the code or dependencies first.
4 |
5 | ## Call to action
6 | We are looking for feedback from developers interested in integrating with Microsoft Graph via GraphQL. To share your thoughts and scenarios, please leave a comment on [UserVoice](https://officespdev.uservoice.com/forums/224641-feature-requests-and-feedback/suggestions/16819672-graphql-api-for-the-microsoft-graph).
7 |
8 | ## About
9 | This is a *demo* that enables basic, read-only querying of the [Microsoft Graph API](https://developer.microsoft.com/en-us/graph/) using [GraphQL query syntax](http://graphql.org/learn/queries/). GraphQL enables clients to request exactly the resources and properties that they need instead of making REST requests for each resource and consolidating the responses. To create a GraphQL service, this demo translates the [Microsoft Graph OData $metadata document](https://graph.microsoft.com/v1.0/$metadata) to a GraphQL schema and generates the necessary resolvers. Please note we are providing this demo code for evaluation as-is.
10 |
11 | 
12 |
13 | ## Live demo
14 | [Try the Microsoft Graph GraphQL Demo here](https://graphql-demo.azurewebsites.net/)
15 |
16 | ## Installation
17 | 1. Clone the repo
18 | 2. Install dependencies (`npm install`)
19 | 3. Generate schema description and resolver code using `npm run build`
20 | 4. Navigate to the [App Registration Portal](https://apps.dev.microsoft.com/), set up a [new web app](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-app-registration)
21 | 5. Configure App Id and redirect URIs in the AppConfiguration of build/index.html
22 | 6. Run `npm start` and go to `localhost:1337`
23 |
24 | ## Sample requests
25 | #### Fetch recent emails
26 |
27 | ```graphql
28 |
29 | {
30 | me {
31 | displayName
32 | officeLocation
33 | skills
34 | messages {
35 | subject
36 | isRead
37 | from {
38 | emailAddress {
39 | address
40 | }
41 | }
42 | }
43 | }
44 | }
45 | ```
46 |
47 |
48 | #### Fetch groups and members
49 | ```graphql
50 | {
51 | groups {
52 | displayName
53 | description
54 | members {
55 | id
56 | }
57 | }
58 | }
59 | ```
60 |
61 | #### Fetch files from OneDrive
62 | ```graphql
63 | {
64 | me {
65 | drives {
66 | quota {
67 | used
68 | remaining
69 | }
70 | root {
71 | children {
72 | name
73 | size
74 | lastModifiedDateTime
75 | webUrl
76 | }
77 | }
78 | }
79 | }
80 | }
81 | ```
82 |
83 | ## How it works
84 | * src/setup.js reads in a well-formed $metadata CSDL, parses it and builds up a GraphQL schema
85 | * src/setup.js code generates resolvers that naively issues requests to the Graph service when the previous (parent) resolver doesn't have the data at hand
86 |
87 | ## Limitations/to-dos
88 | * [x] Translate OData inheritance relationships
89 | * [x] Enable passing arguments (id for indexing into collections)
90 | * [ ] Support pagination
91 | * [ ] Implement mutations
92 | * [ ] Enable passing arguments for sort, filter
93 | * [ ] Add heuristics for $expand to reduce number of service calls made
94 |
95 | ## Code of conduct
96 |
97 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
98 |
99 | ## Copyright
100 | Copyright (c) 2017 Microsoft Corporation.
101 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribute to this code sample
2 |
3 | Thank you for your interest in this sample! Your contributions and improvements will help the developer community.
4 |
5 | ## Ways to contribute
6 |
7 | There are several ways you can contribute to this sample: providing better code comments, fixing open issues, and adding new features.
8 |
9 | ### Provide better code comments
10 |
11 | Code comments make code samples even better by helping developers learn to use the code correctly in their own applications. If you spot a class, method, or section of code that you think could use better descriptions, then create a pull request with your code comments.
12 | In general we want our code comments to follow these guidelines:
13 |
14 | - Any code that has associated documentation displayed in an IDE (such as IntelliSense, or JavaDocs) has code comments.
15 | - Classes, methods, parameters, and return values have clear descriptions.
16 | - Exceptions and errors are documented.
17 | - Remarks exist for anything special or notable about the code.
18 | - Sections of code that have complex algorithms have appropriate comments describing what they do.
19 | - Code added from Stack Overflow, or any other source, is clearly attributed.
20 |
21 | ### Fix open issues
22 |
23 | Sometimes we get a lot of issues, and it can be hard to keep up. If you have a solution to an open issue that hasn't been addressed, fix the issue and then submit a pull request.
24 |
25 | ### Add a new feature
26 |
27 | New features are great! Be sure to check with the repository admin first to be sure the feature will fit the intent of the sample. Start by opening an issue in the repository that proposes and describes the feature. The repository admin will respond and may ask for more information. If the admin agrees to the new feature, create the feature and submit a pull request.
28 |
29 | ## Contribution guidelines
30 |
31 | We have some guidelines to help maintain a healthy repo and code for everyone.
32 |
33 | ### The Contribution License Agreement
34 |
35 | For most contributions, you'll be asked to sign a Contribution License Agreement (CLA). This will happen when you submit a pull request. Microsoft will send a link to the CLA to sign via email. Once you sign the CLA, your pull request can proceed. Read the CLA carefully, because you may need to have your employer sign it.
36 |
37 | ### Code contribution checklist
38 |
39 | Be sure to satisfy all of the requirements in the following list before submitting a pull request:
40 |
41 | - Follow the code style that is appropriate for the platform and language in this repo. For example, Android code follows the style conventions found in the [Code Style for Contributors guide](https://source.android.com/source/code-style.html).
42 | - Test your code.
43 | - Test the UI thoroughly to be sure nothing has been broken by your change.
44 | - Keep the size of your code change reasonable. if the repository owner cannot review your code change in 4 hours or less, your pull request may not be reviewed and approved quickly.
45 | - Avoid unnecessary changes. The reviewer will check differences between your code and the original code. Whitespace changes are called out along with your code. Be sure your changes will help improve the content.
46 |
47 | ### Submit a pull request to the master branch
48 |
49 | When you're finished with your work and are ready to have it merged into the master repository, follow these steps. Note: pull requests are typically reviewed within 10 business days. If your pull request is accepted you will be credited for your submission.
50 |
51 | 1. Submit your pull request against the master branch.
52 | 2. Sign the CLA, if you haven't already done so.
53 | 3. One of the repo admins will process your pull request, including performing a code review. If there are questions, discussions, or change requests in the pull request, be sure to respond.
54 | 4. When the repo admins are satisfied, they will accept and merge the pull request.
55 |
56 | Congratulations, you have successfully contributed to the sample!
57 |
58 | ## FAQ
59 |
60 | ### Where do I get a Contributor's License Agreement?
61 |
62 | You will automatically be sent a notice that you need to sign the Contributor's License Agreement (CLA) if your pull request requires one.
63 |
64 | As a community member, you must sign the CLA before you can contribute large submissions to this project. You only need complete and submit the CLA document once. Carefully review the document. You may be required to have your employer sign the document.
65 |
66 | ### What happens with my contributions?
67 |
68 | When you submit your changes, via a pull request, our team will be notified and will review your pull request. You will receive notifications about your pull request from GitHub; you may also be notified by someone from our team if we need more information. We reserve the right to edit your submission for legal, style, clarity, or other issues.
69 |
70 | ### Who approves pull requests?
71 |
72 | The admin of the repository approves pull requests.
73 |
74 | ### How soon will I get a response about my change request or issue?
75 |
76 | We typically review pull requests and respond to issues within 10 business days.
77 |
78 | ## More resources
79 |
80 | - To learn more about Markdown, see [Daring Fireball](http://daringfireball.net/).
81 | - To learn more about using Git and GitHub, check out the [GitHub Help section](http://help.github.com/).
82 |
--------------------------------------------------------------------------------
/src/setup.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3 | * See LICENSE in the project root for license information.
4 | */
5 |
6 | const _ = require('lodash');
7 | const fs = require('fs');
8 | const acorn = require('acorn');
9 | const astring = require('astring');
10 | const request = require('request');
11 | const xmlConverter = require('xml-js');
12 |
13 | const { graphNameSpace, graphRoot } = require('./constants');
14 |
15 | const primitiveTypeMapping = {
16 | 'Edm.Binary' : 'String',
17 | 'Edm.Stream' : 'String',
18 | 'Edm.String' : 'String',
19 | 'Edm.Int16' : 'Int',
20 | 'Edm.Byte' : 'Int',
21 | 'Edm.Int32' : 'Int',
22 | 'Edm.Int64' : 'String',
23 | 'Edm.Double' : 'Float',
24 | 'Edm.Boolean' : 'Boolean',
25 | 'Edm.Guid': 'String',
26 | 'Edm.DateTimeOffset': 'String',
27 | 'Edm.Date': 'String',
28 | 'Edm.Duration': 'String',
29 | 'Edm.TimeOfDay': 'String',
30 | 'Edm.Single': 'Float'
31 | };
32 |
33 | const additionalScalarTypes = ['\nscalar Date', '\nscalar DateTimeOffset'];
34 |
35 | fetchMetadata();
36 |
37 | function fetchMetadata(){
38 | request(graphRoot+'/$metadata', function (error, response, body) {
39 | if (!error){
40 | parseMetadata(body);
41 | }
42 | });
43 | }
44 |
45 | function parseMetadata(metadata){
46 | let result = xmlConverter.xml2json(metadata, {compact: true, spaces: 4});
47 | let json = JSON.parse(result);
48 | let schema = json['edmx:Edmx']['edmx:DataServices']['Schema'];
49 | let enums = parseEnums(schema['EnumType']); //mapping from name to definition (node -> def)
50 | let complexResponse = parseTypes(schema['ComplexType'], false); //mapping from name to schema (node -> set of properties)
51 | let entityResponse = parseTypes(schema['EntityType'], true);
52 | let complexTypes = complexResponse['complexMapping'];
53 | let complexDependence = complexResponse['complexDependence'];
54 | let entities = entityResponse['entityMapping']
55 | let entityDependence = entityResponse['entityDependence'];
56 | let entityContainer = schema['EntityContainer'];
57 | let entityAnnotations = entityResponse['entityAnnotation'];
58 | let complexAnnotations = complexResponse['complexAnnotation'];
59 | let entitySets = parseEntitySets(entityContainer['EntitySet']); //mapping from name to type of root level entity set
60 | let singletons = parseSingletons(entityContainer['Singleton']); //mapping from name to type of root level singletons
61 | let dependence = _.merge(complexDependence, entityDependence);
62 | let annotations = _.merge(complexAnnotations, entityAnnotations);
63 | generator(dependence, entities, complexTypes, enums, entitySets, singletons, annotations);
64 | }
65 |
66 | function parseEnums(enums){
67 | let enumMapping = {};
68 | for (let graphEnum of enums) {
69 | let name = graphEnum['_attributes']['Name'];
70 | let defs = graphEnum['Member']
71 | let mapping = {};
72 | for (let def of defs){
73 | let name = def['_attributes']['Name'];
74 | let value = def['_attributes']['Value'];
75 | mapping[name] = value;
76 | }
77 | enumMapping[name] = mapping;
78 | }
79 | return enumMapping;
80 | }
81 |
82 | function parseTypes(types, hasIdentity){
83 | let typeDependence = {}; //key is child type
84 | let typeMapping = {};
85 | let typeAnnotationMapping = {};
86 | for (let type of types){
87 | let name = type['_attributes']['Name'];
88 | let baseType = type['_attributes']['BaseType'];
89 | let properties = type['Property'];
90 | let navigationProperties = type['NavigationProperty'];
91 | if (baseType != null){
92 | typeDependence[name] = convertTypeToGQL(false, baseType);
93 | }
94 | let mapping = {};
95 | let annotationMapping = {};
96 | if (properties != null && properties.length > 0){
97 | for (let property of properties){
98 | let name = property['_attributes']['Name'];
99 | let dataType = property['_attributes']['Type'];
100 | mapping[name] = dataType;
101 | let annotation = null;
102 | if (property['Annotation']){
103 | annotation = property['Annotation']['_attributes']['String'];
104 | }
105 | annotationMapping[name] = annotation;
106 | }
107 | } else if (properties != null){
108 | let name = properties['_attributes']['Name'];
109 | let dataType = properties['_attributes']['Type'];
110 | mapping[name] = dataType;
111 | let annotation = null;
112 | if (properties['Annotation']){
113 | annotation = properties['Annotation']['_attributes']['String'];
114 | }
115 | annotationMapping[name] = annotation;
116 | } else {
117 | mapping['extension'] = 'String'
118 | }
119 | if (navigationProperties != null && navigationProperties.length > 0){
120 | for (let navigationProperty of navigationProperties){
121 | let name = navigationProperty['_attributes']['Name'];
122 | let dataType = navigationProperty['_attributes']['Type'];
123 | mapping[name] = dataType;
124 | let annotation = null;
125 | if (navigationProperty['Annotation']){
126 | annotation = navigationProperty['Annotation']['_attributes']['String'];
127 | }
128 | annotationMapping[name] = annotation;
129 | }
130 | }
131 | if (hasIdentity){
132 | mapping['id'] = 'ID';
133 | }
134 | typeAnnotationMapping[name] = annotationMapping;
135 | typeMapping[name] = mapping;
136 | }
137 | if (hasIdentity){
138 | return {
139 | "entityMapping": typeMapping,
140 | "entityDependence": typeDependence,
141 | "entityAnnotation": typeAnnotationMapping
142 | };
143 | } else {
144 | return {
145 | "complexMapping": typeMapping,
146 | "complexDependence": typeDependence,
147 | "complexAnnotation": typeAnnotationMapping
148 | };
149 | }
150 | }
151 |
152 | function parseEntitySets(entitySets){
153 | let entitySetMapping = {};
154 | for (let entitySet of entitySets){
155 | let name = entitySet['_attributes']['Name'];
156 | let type = entitySet['_attributes']['EntityType'];
157 | entitySetMapping[name] = type;
158 | }
159 | return entitySetMapping;
160 | }
161 |
162 | function parseSingletons(singletons){
163 | let singletonMapping = {};
164 | for (let singleton of singletons){
165 | let name = singleton['_attributes']['Name'];
166 | let type = singleton['_attributes']['Type'];
167 | singletonMapping[name] = type;
168 | }
169 | return singletonMapping;
170 | }
171 |
172 | function generator(dependence, entities, complexTypes, enums, entitySets, singletons, annotations){
173 | enumDefs = generateEnumDefs(enums);
174 | entityDefs = generateTypeDefs(dependence, entities, annotations);
175 | complexDefs = generateTypeDefs(dependence, complexTypes, annotations);
176 | queryDefs = generateQueryDefs(entitySets, singletons);
177 | let defs = _.union(enumDefs, entityDefs);
178 | defs = _.union(defs, complexDefs);
179 | defs = _.union(defs, queryDefs);
180 | defs = _.union(defs, additionalScalarTypes);
181 | saveCodeToFile('src/build/schema.graphql', false, defs.join(' '));
182 | const resolverStr = generateResolverStr(dependence, entitySets, singletons, entities, complexTypes);
183 | saveCodeToFile('src/build/schema.js', true, resolverStr);
184 | }
185 |
186 | function saveCodeToFile(filename, format, code) {
187 | if (format) {
188 | let ast = acorn.parse(code, { ecmaVersion: 6 });
189 | code = astring.generate(ast);
190 | }
191 |
192 | fs.writeFile(filename, code, (err) => {
193 | if (err) throw err;
194 | console.log(`Saved code to ${filename}`);
195 | });
196 | }
197 |
198 | function generateQueryDefs(entitySets, singletons){
199 | entitySets = typeAdjustments(entitySets, true, {}, {}, null);
200 | singletons = typeAdjustments(singletons, false, {}, {}, null);
201 | let entitySetSubDefinitions = Object.keys(entitySets).map((key)=>' '+key+'(id: ID): '+entitySets[key]+' ');
202 | let singletonSubDefinitions = Object.keys(singletons).map((key)=>' '+key+': '+singletons[key]+' ');
203 | let subDefs = entitySetSubDefinitions.concat(singletonSubDefinitions);
204 | let queryStr = '\ntype Query {\n'+subDefs.join('\n')+'\n}'
205 | return [queryStr];
206 | }
207 |
208 | function generateEnumDefs(enums){
209 | let defs = [];
210 | for (let graphEnumName of Object.keys(enums)){
211 | let graphEnum = enums[graphEnumName];
212 | let keyNames = Object.keys(graphEnum);
213 | let enumStr = '\nenum '+graphEnumName+' {\n'+keyNames.map((key)=>' '+key+' ').join('\n')+'\n}'
214 | defs.push(enumStr);
215 | }
216 | return defs;
217 | }
218 |
219 | function generateTypeDefs(dependence, types, annotations){
220 | let defs = [];
221 | for (let typeName of Object.keys(types)){
222 | let type = types[typeName];
223 | let adjustedTypes = typeAdjustments(type, false, types, dependence, typeName);
224 | let subDefinitions = Object.keys(adjustedTypes).map((key)=>{
225 | let name = key;
226 | let value = adjustedTypes[key];
227 | let isCollection = value.includes('[') && value.includes(']');
228 | let isEntityCollection = isCollection && checkIfEntity(value.replace('[', '').replace(']', ''), types, dependence);
229 | if (isEntityCollection){
230 | name = name + "(id: ID)"
231 | }
232 | if (annotations[typeName] && annotations[typeName][key]){
233 | return '#'+annotations[typeName][key]+'\n '+name+': '+value+''
234 | } else {
235 | return ' '+name+': '+value+''
236 | }});
237 | let typeStr = '\ntype ' + typeName+' {\n'+subDefinitions.join('\n')+'\n} '
238 | defs.push(typeStr);
239 | }
240 | return defs;
241 | }
242 |
243 | function typeAdjustments(obj, implicitCollection, typeMapping, dependence, name){
244 | let mapping = {};
245 | if (obj != null && typeof Object.keys(obj) !== 'undefined'){
246 | for (let propertyName of Object.keys(obj)){
247 | let propertyValue = obj[propertyName];
248 | mapping[propertyName] = convertTypeToGQL(implicitCollection, propertyValue);
249 | }
250 | }
251 | if (Object.keys(dependence).length > 0){
252 | let currentDependency = dependence[name];
253 | while (currentDependency != null){
254 | let typeDef = typeMapping[currentDependency];
255 | for (let propertyName of Object.keys(typeDef)){
256 | let propertyValue = typeDef[propertyName];
257 | mapping[propertyName] = convertTypeToGQL(implicitCollection, propertyValue);
258 | }
259 | currentDependency = dependence[currentDependency];
260 | }
261 | }
262 | return mapping;
263 | }
264 |
265 | function checkIfEntity(name, typeMapping, dependence){
266 | if (Object.keys(dependence).length > 0){
267 | let currentDependency = dependence[name];
268 | while (currentDependency != null){
269 | let typeDef = typeMapping[currentDependency];
270 | if (typeDef != null){
271 | if (Object.keys(typeDef).includes('id')){
272 | return true;
273 | }
274 | currentDependency = dependence[currentDependency];
275 | } else {
276 | break;
277 | }
278 | }
279 | }
280 | return false;
281 | }
282 |
283 | function convertTypeToGQL(implicitCollection, propertyValue){
284 | let collection = implicitCollection;
285 | if (propertyValue.includes('Collection')){
286 | propertyValue = propertyValue
287 | .replace('Collection', '')
288 | .replace('(', '')
289 | .replace(')', '');
290 |
291 | collection = true;
292 | }
293 | if (Object.keys(primitiveTypeMapping).includes(propertyValue)){
294 | propertyValue = propertyValue.replace(propertyValue, primitiveTypeMapping[propertyValue]);
295 | }
296 | if (propertyValue.includes(graphNameSpace)){
297 | propertyValue = propertyValue.replace(graphNameSpace, '');
298 | }
299 | if (collection){
300 | propertyValue = '['+propertyValue+']';
301 | }
302 | return propertyValue
303 | }
304 |
305 | function generateResolverStr(dependence, entitySets, singletons, entities, complexTypes) {
306 | queryDefResolvers = generateQueryDefResolvers(entitySets, singletons);
307 | typeDefResolvers = generateTypeDefResolvers(dependence, entities, complexTypes);
308 | resolvers = _.union(queryDefResolvers, typeDefResolvers);
309 | return `
310 | const { graphqlResolve, parseOrRequest, makeRequest, graphRoot } = require('../resolverHelpers');
311 | const resolvers = {
312 | ${resolvers.join(',')}
313 | };
314 | module.exports = {
315 | resolvers
316 | }
317 | `;
318 | }
319 |
320 | function generateQueryDefResolvers(entitySets, singletons){
321 | entitySets = typeAdjustments(entitySets, true, {}, {}, null);
322 | singletons = typeAdjustments(singletons, false, {}, {}, null);
323 | let defs = _.merge(entitySets, singletons);
324 | let subDefinitions = Object.keys(defs).map((key)=>
325 | ` ${key} (obj, args, context){
326 | const name = \'${key}\';
327 | const requestUrl = graphRoot + "/" + name;
328 | return makeRequest(requestUrl, obj['__session'], args, false);
329 | }`);
330 | let queryStr = `\nQuery: {\n ${subDefinitions.join(',\n')} \n }`;
331 | return [queryStr];
332 | }
333 |
334 | function generateTypeDefResolvers(dependence, entities, complexTypes){
335 | let defs = [];
336 | generateTypeDefResolversHelper(entities, dependence, defs);
337 | generateTypeDefResolversHelper(complexTypes, dependence, defs);
338 | return defs;
339 | }
340 |
341 | function generateTypeDefResolversHelper(typeCollection, dependence, defs){
342 | for (let typeName of Object.keys(typeCollection)){
343 | let type = typeCollection[typeName];
344 | let adjustedTypes = typeAdjustments(type, false, typeCollection, dependence, typeName);
345 | let subDefinitions = Object.keys(adjustedTypes).map((key)=>
346 | ` ${key} (obj, args, context){
347 | const name = \'${key}\';
348 | return graphqlResolve(obj, name, args);\n }\n`);
349 | let typeStr = `\n${typeName}: {\n ${subDefinitions.join(',\n')}} `;
350 | defs.push(typeStr);
351 | }
352 | }
353 |
--------------------------------------------------------------------------------