semantic-graphql
Advanced tools
Comparing version 0.4.0 to 0.5.0
# Changelog | ||
# 0.5.0 | ||
**Breaking changes:** | ||
- `getIriLocalName` is not exposed/exported by the package anymore. | ||
**New features:** | ||
- On in-graph properties, new `shouldAlwaysUseInverseOf` and `shouldNeverUseInverseOf` config keys to modify the resolver's behavior. | ||
**Bug fixes:** | ||
- `isGraphqlList` is now also infered from `isGraphqlConnection`. | ||
- `preventIdField` config option can now also prevent the Relay id field. | ||
**Miscellaneous:** | ||
- Tests! :tada: (very basic for now) | ||
- Better docs | ||
# 0.4.0 | ||
**Breaking changes:** | ||
- resolveSourceTypes must now be sync. | ||
- `resolvers.resolveSourceTypes` must now be sync. | ||
- Interface type resolving was rolled back on InterfaceTypes (instead of ObjectTypes). Better support, although still incomplete. External ObjectTypes must not provide an `isTypeOf` method anymore, instead external InterfaceTypes must provide a `resolveType` method. | ||
@@ -18,3 +34,3 @@ | ||
**Bug fixes:** | ||
- Warn when traversing graph and encoutering missing vertices | ||
- Warn when traversing graph and encoutering missing vertices. | ||
@@ -27,3 +43,3 @@ # 0.3.0 | ||
**Bug fixes:** | ||
- Bug concerning the creation of Relay types | ||
- Bug concerning the creation of Relay types. | ||
@@ -33,3 +49,3 @@ # 0.2.1 | ||
**Bug fixes:** | ||
- Fixed a bug concerning the inference of owl:inverseOf | ||
- Fixed a bug concerning the inference of owl:inverseOf. | ||
@@ -39,20 +55,20 @@ # 0.2.0 | ||
**Breaking changes:** | ||
- Removed resolvers.resolveSourceClassIri | ||
- Added resolvers.resolveSourceTypes | ||
- Removed `resolvers.resolveSourceClassIri`. | ||
- Added `resolvers.resolveSourceTypes`. | ||
- Interface type resolving now happens on GraphQLObjectTypes. This means that your external GraphQLObjectTypes must provide an `isTypeOf` method. | ||
**New features:** | ||
- Promise support for all resolvers | ||
- Promise support for all resolvers. | ||
**Bug fixes:** | ||
- Fixed requireGraphqlRelay behavior | ||
- Fixed a bug on SemanticGraph#addFieldOnObjectType | ||
- Fixed a bug that happened when inferring owl:inverseOf on properties that are a rdfs:subProperty with no rdfs:range | ||
- Fixed a circular dependency in ./src/graphql | ||
- Fixed `requireGraphqlRelay` behavior. | ||
- Fixed a bug on `SemanticGraph#addFieldOnObjectType`. | ||
- Fixed a bug that happened when inferring owl:inverseOf on properties that are a rdfs:subProperty with no rdfs:range. | ||
- Fixed a circular dependency in ./src/graphql. | ||
**Miscellaneous:** | ||
- Add MIT license | ||
- Add .npmignore file | ||
- id field now appears on top when introspecting | ||
- graphqlDescription are now created from the locale in config | ||
- Add MIT license. | ||
- Add .npmignore file. | ||
- id field now appears on top when introspecting. | ||
- graphqlDescription are now created from the locale in config. | ||
@@ -59,0 +75,0 @@ # 0.1.0 |
@@ -1,6 +0,1 @@ | ||
const SemanticGraph = require('./src/SemanticGraph'); | ||
const getIriLocalName = require('./src/utils/getIriLocalName'); | ||
SemanticGraph.getIriLocalName = getIriLocalName; | ||
module.exports = SemanticGraph; | ||
module.exports = require('./src/SemanticGraph'); |
{ | ||
"name": "semantic-graphql", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "Create GraphQL schemas from RDF-based ontologies", | ||
@@ -19,8 +19,8 @@ "main": "index.js", | ||
}, | ||
"//test": "./node_modules/.bin/mocha $npm_package_options_mocha", | ||
"//prepublish": "./scripts/checkgit.sh && npm test && npm run build", | ||
"//coverage": "./node_modules/.bin/istanbul cover _mocha -- $npm_package_options_mocha", | ||
"//coverage:serve": "cd coverage/lcov-report && python -m SimpleHTTPServer", | ||
"//coverage:all": "npm run coverage && npm run coverage:serve", | ||
"scripts": { | ||
"test": "./node_modules/.bin/mocha $npm_package_options_mocha", | ||
"coverage": "./node_modules/.bin/istanbul cover _mocha -- $npm_package_options_mocha", | ||
"coverage:serve": "cd coverage/lcov-report && python -m SimpleHTTPServer", | ||
"coverage:all": "npm run coverage && npm run coverage:serve", | ||
"dev": "cd examples/basic && npm run watch" | ||
@@ -27,0 +27,0 @@ }, |
276
README.md
@@ -10,7 +10,14 @@ # Semantic GraphQL | ||
**Table of contents**: | ||
- x | ||
- y | ||
- z | ||
**Table of contents:** | ||
- [Installation](#installation) | ||
- [Getting started](#getting-started) | ||
- [SemanticGraph API](#semanticgraph-api) | ||
- [Resolvers](#resolvers) | ||
- [Overriding default values](#overriding-default-values) | ||
- [Using Relay](#using-relay) | ||
- [OWL features roadmap](#owl-features-roadmap) | ||
- [Contributing](#contributing) | ||
- [License](#license) | ||
## Installation | ||
@@ -24,5 +31,5 @@ | ||
Semantic-graphql makes no assumption about the shape of your data and only passes it around. You have to provide six functions to resolve it in different ways. See the [resolvers section](#). | ||
Semantic-graphql makes no assumption about the shape of your data and only passes it around. You have to provide six functions to resolve it in different ways. See the [resolvers section](#resolvers). | ||
```javascript | ||
```js | ||
const resolvers = { /* Choose how to resolve data */ }; | ||
@@ -33,3 +40,3 @@ ``` | ||
```javascript | ||
```js | ||
const SemanticGraph = require('semantic-graphql'); | ||
@@ -42,3 +49,3 @@ | ||
```javascript | ||
```js | ||
_.addTriple({ | ||
@@ -55,5 +62,5 @@ subject: 'http://foo.com#MyClass', | ||
When Semantic-graphql translates a rdf:Property to a GraphQLFieldConfig, the resulting type will be wrapped in a GraphQLList unless the property is a owl:FunctionalProperty. Therefore you may have to do some adjustments in order to prevent that. Almost anything can be overriden, see the [override section](#). | ||
When Semantic-graphql translates a rdf:Property to a GraphQLFieldConfig, the resulting type will be wrapped in a GraphQLList unless the property is a owl:FunctionalProperty. Therefore you may have to do some adjustments in order to prevent that. Almost anything can be overriden, see the [override section](#overriding-default-values). | ||
```javascript | ||
```js | ||
// We do not want rdfs:label to resolve arrays | ||
@@ -65,3 +72,3 @@ _['http://www.w3.org/2000/01/rdf-schema#label'].isGraphqlList = false; | ||
```javascript | ||
```js | ||
const schema = new GraphQLSchema({ | ||
@@ -89,35 +96,214 @@ query: new GraphQLObjectType({ | ||
Have a look at the [examples folder](#) to see a complete setup. | ||
Have a look at the [examples folder](examples/) to see a complete setup. | ||
## Using relay | ||
## SemanticGraph API | ||
By using the `relay: true` option: | ||
``` | ||
class SemanticGraph { | ||
constructor(resolvers: Resolvers, config: ?SemanticGraphConfig), | ||
[subject: Iri]: object, | ||
# Public methods: | ||
addTriple: AddTripleFn, | ||
parse: ParseFn, | ||
parseFile: ParseFileFn, | ||
getObjectType: GetObjectTypeFn, | ||
getInterfaceType: GetInterfaceTypeFn, | ||
getEdgeType: GetEdgeTypeFn, | ||
getConnectionType: GetConnectionTypeFn, | ||
addFieldOnObjectType: AddFieldOnObjectTypeFn, | ||
extendFieldOnObjectType: ExtendFieldOnObjectTypeFn, | ||
# When the relay option is on: | ||
nodeField: GraphQLFieldConfig, | ||
nodeInterface: GraphQLInterfaceType, | ||
} | ||
- The `_.nodeField` and `_.nodeInterface` object are available | ||
- The `_.getConnectionType` and `_.getEdgeType` methods are available | ||
- All ObjectTypes exhibit the Node interface | ||
- The id field is a globalIdField | ||
type SemanticGraphConfig = { | ||
prefixes?: PrefixConfig, | ||
# Activates the Relay features | ||
relay?: boolean, | ||
# Your reprefered locale when inferring names and descriptions from rdfs:label and rdfs:comment | ||
locale?: string = 'en', | ||
# Prevents the id field to be automatically added to every GraphlQLFieldConfigMap | ||
preventIdField?: boolean, | ||
} | ||
``` | ||
See the [Relay example](#). | ||
To prevent GraphQL names collisions, you can edit the name directly (see the [override section](#overriding-default-values)) | ||
or specifify prefixes for ontology namespaces. | ||
The names of the generated GraphQL objects will be prefixed. | ||
RDF, RDFS and OWL ontologies are by default prefixed with "Rdf", "Rdfs" and "Owl". | ||
So a fragment on rdfs:Resource should be "on RdfsResource". | ||
## API | ||
``` | ||
type PrefixConfig = { | ||
[prefix: string]: Iri, | ||
} | ||
### SemanticGraph | ||
# Must represent a valid IRI | ||
type Iri = string | ||
``` | ||
### Resolvers | ||
### addTriple | ||
#### resolveSourceId | ||
``` | ||
type AddTripleFn(triple: Triple) => undefined | ||
#### resolveSourceTypes | ||
type Triple = { | ||
subject: Iri, | ||
predicate: Iri, | ||
object: Iri | string, | ||
} | ||
``` | ||
Must be sync. See [this GraphQL issue](https://github.com/graphql/graphql-js/issues/398). | ||
Appends a triple to the graph. No-op if the subject or predicate IRI is invalid, or if the triple already exists. | ||
#### resolveSourcePropertyValue | ||
### parse | ||
#### resolveResource | ||
Will be removed someday | ||
#### resolveResources | ||
### parseFile | ||
#### resolveResourcesByPredicate | ||
Will be removed someday | ||
### getObjectType | ||
``` | ||
type GetObjectTypeFn = (classIri: Iri) => GraphQLObjectType | ||
``` | ||
Returns the GraphQLObjectType corresponding to a given individual of rdfs:Class or its sub-classes (like owl:Class). Throws if the IRI is not found in the graph. | ||
### getInterfaceType | ||
``` | ||
type GetInterfaceTypeFn = (classIri: Iri) => GraphQLInterfaceType | ||
``` | ||
### getEdgeType | ||
``` | ||
type GetEdgeTypeFn = (classIri: Iri) => ?RelayEdgeType | ||
``` | ||
Returns a value only when the `relay: true` option is on. | ||
### getConnectionType | ||
``` | ||
type GetConnectionTypeFn = (classIri: Iri) => ?RelayConnectionType | ||
``` | ||
Returns a value only when the `relay: true` option is on. | ||
### addFieldOnObjectType | ||
``` | ||
type AddFieldOnObjectTypeFn = ( | ||
classIri: Iri, | ||
fieldName: string, | ||
graphqlFieldConfig: GraphQLFieldConfig | ||
) => Iri | ||
``` | ||
Adds a custom field "fieldName" on the GraphQLObjectType and GraphQLInterfaceType representing "classIri". | ||
Throws if "classIri" is not found in the graph. | ||
Returns a random IRI referencing the new virtual rdf:Property. | ||
### extendFieldOnObjectType | ||
``` | ||
type AddFieldOnObjectTypeFn = ( | ||
classIri: Iri, | ||
propertyIri: Iri, | ||
graphqlFieldConfig: PseudoGraphQLFieldConfig | ||
) => undefined | ||
``` | ||
Similar to overriding a field using `_['http://foo.com/someProperty'].graphqlFieldConfigExtension = /* ... */` but only for a particular class, not for all of the classes on the property's domain. | ||
A `PseudoGraphQLFieldConfig` is just a `GraphQLFieldConfig` where every key is optional. The other keys will be infered. | ||
Throws if "classIri" is not found in the graph. | ||
## Resolvers | ||
``` | ||
type Resolvers = { | ||
resolveSourceId: ResolveSourceIdFn, | ||
resolveSourceTypes: ResolveSourceTypesFn, | ||
resolveSourcePropertyValue: ResolveSourcePropertyValueFn, | ||
resolveResource: ResolveResourceFn, | ||
resolveResources: ResolveResourcesFn, | ||
resolveResourcesByPredicate: ResolveResourcesByPredicateFn, | ||
} | ||
type ResolverOutput<x> = x | Array<x> | Promise<x> | Promise<Array<x>> | ||
``` | ||
### resolveSourceId | ||
``` | ||
type ResolveSourceIdFn = ( | ||
source?: any, | ||
context?: any, | ||
info?: GraphQLResolveInfo | ||
) => ?ID | ?Promise<ID> | ||
``` | ||
Must be sync if you use Relay. | ||
See [`globalIdField` source code](https://github.com/graphql/graphql-relay-js/blob/master/src/node/node.js#L107) | ||
### resolveSourceTypes | ||
``` | ||
type ResolveSourceTypesFn = ( | ||
source?: any, | ||
info?: GraphQLResolveInfo | ||
) => Iri | Array<Iri> | ||
``` | ||
Must be sync. | ||
See [this GraphQL issue](https://github.com/graphql/graphql-js/issues/398). | ||
### resolveSourcePropertyValue | ||
``` | ||
type ResolveSourcePropertyValueFn = ( | ||
source?: any, | ||
propertyIri?: Iri, | ||
context?: any, | ||
info?: GraphQLResolveInfo | ||
) => ?ResolverOutput<any> | ||
``` | ||
### resolveResource | ||
``` | ||
type ResolveResourceFn = ( | ||
resourceIri?: Iri, | ||
context?: any, | ||
info?: GraphQLResolveInfo | ||
) => ?ResolverOutput<any> | ||
``` | ||
### resolveResources | ||
``` | ||
type ResolveResourcesFn = ( | ||
resourceIris?: Array<Iri>, | ||
context?: any, | ||
info?: GraphQLResolveInfo | ||
) => ?ResolverOutput<any> | ||
``` | ||
### resolveResourcesByPredicate | ||
``` | ||
type ResolveResourcesByPredicateFn = ( | ||
typeIri?: Array<Iri>, | ||
predicateIri?: Iri, | ||
value?: any, | ||
context?: any, | ||
info?: GraphQLResolveInfo | ||
) => ?ResolverOutput<any> | ||
``` | ||
## Overriding default values | ||
@@ -127,3 +313,3 @@ | ||
```javascript | ||
```js | ||
_['http://the.resource.to/be#altered'].key = value; | ||
@@ -146,7 +332,9 @@ ``` | ||
| property | graphqlFieldConfigExtension | partial GraphQLFieldConfig, to modify only parts of it | | ||
| property | shouldAlwaysUseInverseOf | Boolean | | ||
| property | shouldNeverUseInverseOf | Boolean | | ||
Note that the following overrides must be performed *before* invoking `getObjectType` or `getInterfaceType` or `getEdgeType` or `getConnectionType` since those methods create the GraphQL objects you want to override. | ||
Example: | ||
```javascript | ||
Examples: | ||
```js | ||
_['http://foo.com#worksForCompany'].graphqlName = 'company'; | ||
@@ -156,8 +344,22 @@ // Now the field name for foo:worksForCompany will be 'company' | ||
// Partial modifications to fields are achieved using | ||
_['http://foo/com#worksForCompany'].graphqlFieldConfigExtension = { | ||
_['http://foo.com#worksForCompany'].graphqlFieldConfigExtension = { | ||
args: /* Look Ma', arguments! */ | ||
resolve: customResolveFn, | ||
}; | ||
// Completly overriding a GraphQLObject can be done with | ||
_['http://foo.com/MyClass'].graphqlObjectType = new GraphQLObjectType({ /* ... */}); | ||
``` | ||
## Using Relay | ||
By using the `relay: true` option: | ||
- The `_.nodeField` and `_.nodeInterface` object are available | ||
- The `_.getConnectionType` and `_.getEdgeType` methods are available | ||
- All ObjectTypes exhibit the Node interface | ||
- The id field is a globalIdField | ||
See the [Relay example](examples/relay). | ||
## OWL features roadmap | ||
@@ -187,3 +389,3 @@ | ||
- Some items of the preceding list might be impossible to implement and end up here | ||
- Some items of the preceding list might be impossible to implement and end up here. | ||
@@ -194,6 +396,8 @@ ## Contributing | ||
## Licence | ||
## License | ||
Semantic GraphQL is released under the MIT License. | ||
GraphQL is released by Facebook, inc. under the [BSD-license](https://github.com/graphql/graphql-js/blob/master/LICENSE) with an additional [patent grant](https://github.com/graphql/graphql-js/blob/master/PATENTS). | ||
GraphQL is released by Facebook, inc. under the [BSD-license](https://github.com/graphql/graphql-js/blob/master/LICENSE) | ||
with an additional | ||
[patent grant](https://github.com/graphql/graphql-js/blob/master/PATENTS). |
@@ -20,4 +20,2 @@ const { GraphQLList } = require('graphql'); | ||
function getGraphqlFieldConfig(g, iri) { | ||
// console.log('getGraphqlFieldConfig', iri); | ||
// Look for a range, return it if found | ||
@@ -24,0 +22,0 @@ // Otherwise for each super-property, look for a range, if not found, check their super-properties and so on |
@@ -27,12 +27,14 @@ const { GraphQLID } = require('graphql'); | ||
// Add id field | ||
if (g.config.relay) { | ||
fieldConfigMap.id = requireGraphqlRelay().globalIdField(getGraphqlName(g, iri), g.resolvers.resolveSourceId); | ||
if (!g.config.preventIdField) { | ||
if (g.config.relay) { | ||
fieldConfigMap.id = requireGraphqlRelay().globalIdField(getGraphqlName(g, iri), g.resolvers.resolveSourceId); | ||
} | ||
else { | ||
fieldConfigMap.id = { | ||
type: GraphQLID, | ||
description: 'A unique identifier for the resource.', | ||
resolve: (source, args, context, info) => g.resolvers.resolveSourceId(source, context, info), | ||
}; | ||
} | ||
} | ||
else if (!g.config.preventIdField) { | ||
fieldConfigMap.id = { | ||
type: GraphQLID, | ||
description: 'A unique identifier for the resource.', | ||
resolve: (source, args, context, info) => g.resolvers.resolveSourceId(source, context, info), | ||
}; | ||
} | ||
@@ -39,0 +41,0 @@ // Add other fields |
@@ -9,3 +9,2 @@ const { owlInverseOf, _owlInverseOf, rdfsDomain, _rdfsSubClassOf } = require('../constants'); | ||
function getGraphqlObjectResolver(g, iri, ranges) { | ||
const { resolvers } = g; | ||
const isList = isGraphqlList(g, iri); | ||
@@ -16,3 +15,3 @@ | ||
// If inverseProperties exists, we can use them to retrieve missing remote data | ||
if (g[iri][owlInverseOf] || g[iri][_owlInverseOf]) { | ||
if (!g[iri].shouldNeverUseInverseOf && ([iri][owlInverseOf] || g[iri][_owlInverseOf])) { | ||
const extendedRanges = new Set(); | ||
@@ -49,41 +48,52 @@ const inverseProperties = new Set(); | ||
// XXX: put outside of scope to avoid re-allocation ? | ||
// The actual resolve function | ||
const resolver = (source, args, context, info) => Promise.resolve(resolvers.resolveSourcePropertyValue(source, iri, context, info)) | ||
.then(ref => { | ||
const hasNoInverseOf = !(inverseOfMap && inverseOfMap.size); | ||
const resolveNothing = () => isList ? [] : null; | ||
const resolveResource = isList ? g.resolvers.resolveResources : g.resolvers.resolveResource; | ||
if (!isNil(ref)) { | ||
return (isList ? resolvers.resolveResources : resolvers.resolveResource)(castArrayShape(ref, isList), context, info); | ||
} | ||
// A resolver for inverseOf properties | ||
const inverseOfResolver = (source, args, context, info) => { | ||
if (hasNoInverseOf) return resolveNothing(); | ||
// No reference(s) to data was resolved, maybe the data is on an inverse Property | ||
if (inverseOfMap && inverseOfMap.size) { | ||
return Promise.resolve(g.resolvers.resolveSourceId(source, context, info)) | ||
.then(sourceId => { | ||
return Promise.resolve(resolvers.resolveSourceId(source, context, info)) | ||
.then(sourceId => { | ||
const promises = []; | ||
const promises = []; | ||
inverseOfMap.forEach((admitingRanges, propertyIri) => { | ||
promises.push(g.resolvers.resolveResourcesByPredicate(admitingRanges, propertyIri, sourceId, context, info)); | ||
}); | ||
inverseOfMap.forEach((admitingRanges, propertyIri) => { | ||
promises.push(resolvers.resolveResourcesByPredicate(admitingRanges, propertyIri, sourceId, context, info)); | ||
}); | ||
return Promise.all(promises) | ||
.then(results => { | ||
const finalResult = results.reduce((a, b) => a.concat(b), []); | ||
return Promise.all(promises) | ||
.then(results => { | ||
const finalResult = results.reduce((a, b) => a.concat(b), []); | ||
return isList ? finalResult : finalResult[0]; | ||
}); | ||
return isList ? finalResult : finalResult[0]; | ||
}); | ||
} | ||
}); | ||
}; | ||
// Give up | ||
return isList ? [] : null; | ||
}); | ||
// XXX: put outside of scope to avoid re-allocation ? | ||
// The actual resolve function | ||
const resolver = (source, args, context, info) => { | ||
if (g[iri].shouldAlwaysUseInverseOf) return inverseOfResolver(source, args, context, info); | ||
return Promise.resolve(g.resolvers.resolveSourcePropertyValue(source, iri, context, info)) | ||
.then(ref => { | ||
// A reference to data was resolved, we resolve the underlying resources | ||
// NOTE: this does not aggregate both direct and inverse data | ||
if (!isNil(ref)) return resolveResource(castArrayShape(ref, isList), context, info); | ||
// No reference to data was resolved, maybe the data is on an inverse Property | ||
if (!g[iri].shouldNeverUseInverseOf) return inverseOfResolver(source, args, context, info); | ||
// Give up | ||
return resolveNothing(); | ||
}); | ||
}; | ||
if (g.config.relay && g[iri].isRelayConnection) { | ||
const { connectionFromArray, connectionFromPromisedArray } = requireGraphqlRelay(); | ||
return (node, args, context, info) => { | ||
const results = resolver(node, args, context, info); | ||
return (source, args, context, info) => { | ||
const results = resolver(source, args, context, info); | ||
@@ -90,0 +100,0 @@ return (Array.isArray(results) ? connectionFromArray : connectionFromPromisedArray)(results, args); |
@@ -5,5 +5,5 @@ const { rdfType, owlFunctionalProperty } = require('../constants'); | ||
function isGraphqlList(g, iri) { | ||
return !(g[iri][rdfType] && g[iri][rdfType].includes(owlFunctionalProperty)); | ||
return g[iri].isRelayConnection || !(g[iri][rdfType] && g[iri][rdfType].includes(owlFunctionalProperty)); | ||
} | ||
module.exports = memorize(isGraphqlList, 'isGraphqlList'); |
let graphqlRelay; | ||
// optionnalPeerDependencies still don't exist, so graphql-relay is a ghost dep | ||
// optionalPeerDependencies still don't exist, so graphql-relay is a ghost dep | ||
// let's find it | ||
@@ -5,0 +5,0 @@ function requireGraphqlRelay() { |
@@ -86,2 +86,4 @@ const path = require('path'); | ||
/* Private methods */ | ||
function indexTriple(g, { subject, predicate, object }) { | ||
@@ -88,0 +90,0 @@ if (!(isIri(subject) && isIri(predicate)) || g[subject] && g[subject][predicate] && g[subject][predicate].includes(object)) return; |
function warn(message) { | ||
console.log(`[semantic-graphql warning]: ${message}`); | ||
console.warn(`[semantic-graphql] ${message}`); | ||
} | ||
module.exports = warn; |
402174
84
1333
392