├── .eslintrc ├── .gitignore ├── Readme.md ├── example ├── .eslintrc ├── index.js └── package.json ├── package.json ├── resources ├── arbitraryReferenceAttributesQuery.clj ├── batchingIdea.js ├── installExtensionAttributes.clj ├── installedInterfacesQuery.clj ├── installedTypesQuery.clj └── schemaQuery.clj └── src ├── bootstrap ├── index.js └── utils │ ├── applySchemaImpliedTypes.js │ ├── installExtensionAttributes.js │ ├── queryArbitraryReferenceAttributes.js │ ├── queryUnidirectionalReferenceAttributes.js │ ├── resolveArbitraryReferenceAttributesViaCLI.js │ └── resolveUnidirectionalReferenceAttributesViaCLI.js ├── constants ├── datomicCardinalities.js ├── datomicUniques.js ├── datomicValueTypes.js ├── extensionAttributes.js └── queryPredicateOperators.js ├── consumer └── index.js ├── graphQLSchema ├── getNodeDefinitions │ └── index.js ├── getRootMutationType │ ├── index.js │ └── utils │ │ ├── getNewInputArgsForSchemaType.js │ │ └── getPatchInputArgsForSchemaType.js ├── getRootQueryType │ └── index.js ├── index.js └── utils │ ├── getGraphQLConnectionTypeForSchemaType.js │ ├── getGraphQLEnumTypeForAttribute.js │ ├── getGraphQLTypeForAttribute.js │ ├── getGraphQLTypeForReverseReferenceAttribute.js │ ├── getGraphQLTypeForSchemaType.js │ ├── getQueryEdn.js │ ├── getQueryInputArgsForSchemaType.js │ ├── getReferenceFieldClause.js │ ├── resolveConnectionFieldQuery.js │ └── resolveInstanceFieldQuery.js ├── index.js └── utils ├── getObjectFromEntity.js ├── getSchemaTypesAndInterfaces.js ├── inflect.js ├── queryInstalledInterfaces.js ├── queryInstalledTypes.js └── querySchemaImpliedTypes.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "parser": "babel-eslint", 4 | "ecmaFeatures": { 5 | "classes": true, 6 | "spread": true 7 | }, 8 | "rules": { 9 | "no-use-before-define": [2, "nofunc"], 10 | "id-length": 0, 11 | "strict": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # node.js 26 | # 27 | node_modules/ 28 | npm-debug.log 29 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # datomic-graphql 2 | GraphQL interface to datomic. 3 | 4 | ## Status 5 | This works. It's pretty nice! It's unfinished though, and while I plan to make use of it, I am not maintaining it now. If you're interested at all, please open an issue, and I'll respond! 6 | 7 | ### Purpose 8 | Datomic is a very interesting database with a simple design. I discovered it right when GraphQL was released, and wanted to test the potential for aligning GraphQL & Datomic. 9 | 10 | The goal of this project is to enable a GraphQL interface atop any Datomic database with minimal configuration. Furthermore, I'd really like to use GraphQL to mutate the schema itself, such that it could be considered GraphQL as a backend. 11 | 12 | Better notes coming soon. **Please don't hesitate to contact me or open an issue if this interests you!** 13 | 14 | 15 | ## Running the Example 16 | 17 | GraphiQL interface to Mbrainz Datomic sample database. 18 | ![GraphiQL](https://cloud.githubusercontent.com/assets/1638987/12154291/a4d30cda-b48c-11e5-841d-62428f642d2f.png) 19 | 20 | ### Datomic / Mbrainz Setup 21 | First download the datomic-pro trial and follow [this tutorial](http://blog.datomic.com/2013/07/datomic-musicbrainz-sample-database.html) to create the musicbrainz database. 22 | 23 | Then, from within `datomic-pro-0.9.x` (in different Terminal tabs / tmux / bg): 24 | ``` 25 | bin/transactor config/samples/dev-transactor-template.properties 26 | 27 | bin/rest -p 8080 dev datomic:dev://localhost:4334/ 28 | ``` 29 | 30 | Clone this repository, npm install, and cd to the example directory. 31 | ``` 32 | git clone https://github.com/danscan/datomic-graphql.git 33 | cd datomic-graphql 34 | npm install 35 | cd example 36 | npm install 37 | npm start 38 | ``` 39 | 40 | `datomic-graphql` will prompt you to resolve so ambiguities regarding reference attribute targets, enum values, etc. Once ambiguities are resolved, a GraphQL server will start up. 41 | -------------------------------------------------------------------------------- /example/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "rules": { 4 | "no-use-before-define": [2, "nofunc"], 5 | "id-length": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import session from 'express-session'; 3 | import graphQLHTTP from 'express-graphql'; 4 | import getGraphQLSchema from '../src'; 5 | 6 | // (Configuration constants) 7 | const PORT = process.env.PORT || 8000; 8 | const SESSION_SECRET = process.env.SESSION_SECRET || 'keyboard cat'; 9 | const DATOMIC_REST_API_URL = process.env.DATOMIC_REST_API_URL || 'http://localhost:8080'; 10 | const DATOMIC_DB_ALIAS = process.env.DATOMIC_DB_ALIAS || 'dev/mbrainz-1968-1973'; 11 | 12 | // Create HTTP server 13 | const app = express(); 14 | 15 | // Get graphql schema 16 | getGraphQLSchema(DATOMIC_REST_API_URL, DATOMIC_DB_ALIAS) 17 | // .then(graphQLSchema => console.log('graphQLSchema:', graphQLSchema)) 18 | .then(graphQLSchema => { 19 | // Expose session middleware at root 20 | app.use('/', session({ 21 | secret: SESSION_SECRET, 22 | resave: false, 23 | saveUninitialized: false, 24 | cookie: { 25 | maxAge: 1000 * 60 * 60 * 24 * 7, 26 | }, 27 | })); 28 | 29 | // Expose (session-aware) GraphQL interface to schema at root 30 | app.use('/', graphQLHTTP(request => ({ 31 | schema: graphQLSchema, 32 | rootValue: { 33 | session: request.session, 34 | }, 35 | pretty: true, 36 | graphiql: true, 37 | }))); 38 | 39 | // Listen HTTP server on configured port 40 | app.listen(PORT, () => console.log(`App listening on port ${PORT}...`)); 41 | }) 42 | .catch(error => console.error('Error:', error.stack || error)); 43 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "Example GraphQL interface to datomic mbrainz db", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "babel-node index" 9 | }, 10 | "author": "Dan Scanlon", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.13.3", 14 | "express-graphql": "^0.4.4", 15 | "express-session": "^1.12.1" 16 | }, 17 | "devDependencies": { 18 | "babel-eslint": "^4.1.5", 19 | "eslint": "^1.10.1", 20 | "eslint-config-airbnb": "^1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datomic-graphql", 3 | "version": "1.0.0", 4 | "description": "GraphQL interface to datomic", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "graphql", 11 | "relay", 12 | "datomic" 13 | ], 14 | "author": "Dan Scanlon", 15 | "devDependencies": { 16 | "babel-eslint": "^4.1.5", 17 | "eslint": "^1.9.0", 18 | "eslint-config-airbnb": "^1.0.0" 19 | }, 20 | "dependencies": { 21 | "axios": "^0.7.0", 22 | "graphql": "^0.4.14", 23 | "graphql-relay": "^0.3.5", 24 | "i": "^0.3.3", 25 | "inquirer": "^0.11.0", 26 | "jsedn": "^0.3.5", 27 | "underscore": "^1.8.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/arbitraryReferenceAttributesQuery.clj: -------------------------------------------------------------------------------- 1 | ; This query gets arbitrary reference attributes 2 | ; Just copy and paste this into the q field in the Query REST API 3 | [:find ?ident :where 4 | [?e :db/ident ?ident] 5 | [?e :db/valueType :db.type/ref] 6 | [(missing? $ ?e :extGraphQL/refTarget)] 7 | [(missing? $ ?e :extGraphQL/enumValues)]] 8 | -------------------------------------------------------------------------------- /resources/batchingIdea.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | // # Notes 3 | // - parent field (optionally resolved w/ args) 4 | // - scalar field 5 | // - resolve field's attribute where e = parent field's id 6 | // - referece (singular) field 7 | // - query reference from parent field? 8 | // - resolve fields like scalar fields (above) 9 | // - connection field 10 | // - query reference from parent field 11 | // - return connectionFromArray... resolve array 12 | // 13 | // 14 | // unify predicates via union 15 | // unify fields for each predicate via union? 16 | 17 | // # Types 18 | const UserType = new GraphQLObjectType({ 19 | name: 'User', 20 | description: 'Someone who uses the app', 21 | fields: () => ({ 22 | id: globalIdField('User'), 23 | 24 | username: { 25 | type: GraphQLString, 26 | description: 'The user\'s username', 27 | resolve: (parent, args) => { 28 | return resolveScalarField({ type: UserType, fieldName: 'username', parent, args }); // returns promise for username 29 | }, 30 | }, 31 | 32 | email: { 33 | type: GraphQLString, 34 | description: 'The user\'s email', 35 | resolve: (parent, args) => { 36 | return resolveScalarField({ type: UserType, fieldName: 'email', parent, args }); // returns promise for email 37 | }, 38 | }, 39 | 40 | posts: { 41 | type: PostsConnection, 42 | description: 'The posts that reference the user via their "creator" field', 43 | args: { ...postPredicate, ...connectionArgs }, 44 | resolve: (parent, args) => { 45 | return resolveConnectionField({ type: UserType, fieldName: 'posts', parent, args }); // returns promise for posts (how?) 46 | }, 47 | }, 48 | }), 49 | }); 50 | 51 | const PostType = new GraphQLObjectType({ 52 | name: 'Post', 53 | description: 'A post created by a user', 54 | fields: () => ({ 55 | id: globalIdField('Post'), 56 | 57 | creator: { 58 | type: User, 59 | description: 'The user who created the post', 60 | resolve: (parent, args) => { 61 | return resolveReferenceField({ type: PostType, fieldName: 'creator', parent, args }); // returns promise for creator (how?) 62 | }, 63 | }, 64 | 65 | caption: { 66 | type: GraphQLString, 67 | description: 'The post\'s caption', 68 | resolve: (parent, args) => { 69 | return resolveScalarField({ type: PostType, fieldName: 'caption', parent, args }); // returns promise for caption 70 | }, 71 | }, 72 | 73 | imageUri: { 74 | type: GraphQLString, 75 | description: 'The post\'s image\'s uri', 76 | resolve: (parent, args) => { 77 | return resolveScalarField({ type: PostType, fieldName: 'imageUri', parent, args }); // returns promise for imageUri 78 | }, 79 | }, 80 | }), 81 | }) 82 | 83 | // # Query 84 | query UserPosts { 85 | user(id: { is: 2 }) { 86 | username 87 | email 88 | posts { 89 | edges { 90 | node { 91 | caption 92 | imageUri 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | // # Batched... 100 | const batchedResolutions = { 101 | types: [UserType, PostType], 102 | typesPredicateExpressionsMap: { 103 | [UserType.name]: { id: 2 }, 104 | [PostType.name]: { creator: { id: 2 } }, // connection args...? clause/predicate for reference query...? 105 | // IDEA: use reference field from query (User.posts) (? what if it's reverse? how to identify the e of referring node? IDEA: use predicate used to select referring node) 106 | }, 107 | typesAttributesMap: { 108 | [UserType.name]: [ 109 | 'username', 110 | 'email', 111 | ], 112 | [PostType.name]: [ 113 | 'caption', 114 | 'imageUri', 115 | ], 116 | }, 117 | typesAttributesPredicateExpressionsMap: { 118 | [UserType.name]: { 119 | 'username': null, 120 | 'email': null, 121 | }, 122 | [PostType.name]: { 123 | 'caption': null, 124 | 'imageUri': null, 125 | } 126 | }, 127 | }; 128 | // -> 129 | const batchedQueries = [ 130 | { 131 | entityUnificationVariable: '?user', // name of type 132 | findVariables: ['?username', '?email'], // from type attributes map 133 | clauses: [ 134 | ['?user', ':db/id', 2], // from type predicate expressions map 135 | ['?user', ':user/username', '?username'], // from type attributes map 136 | ['?user', ':user/email', '?email'], // from type attributes map 137 | ], 138 | }, 139 | { 140 | entityUnificationVariable: '?post', // name of type 141 | findVariables: ['?caption', '?imageUri'], // from type attributes map 142 | clauses: [ 143 | ['?creator', ':db/id', 2], 144 | ['?post', ':post/creator', '?creator'], 145 | ['?post', ':post/caption', '?caption'], // from type attributes map 146 | ['?post', ':post/imageUri', '?imageUri'], // from type attributes map 147 | ], 148 | }, 149 | ];// -> query edn -> query -> query results -> received by fn that resolves individual resolve*Field promises w/ appropriate values 150 | 151 | // (?) This could even be reduced to one query... probably too complicated to implement for now 152 | // [:find ?user ?post ?user-username ?user-email ?post-caption ?post-imageUri 153 | // :where [?user :user/id 2] 154 | // [?user :user/username ?user-username] 155 | // [?user :user/email ?user-email] 156 | // [?post :post/creator ?user] 157 | // [?post :post/caption ?post-caption] 158 | // [?post :post/imageUri ?post-imageUri]] 159 | 160 | // Resolving bi-directional relations with unidirectional refs in schema... 161 | // will probably need to add a meta-attribute to ref-type attributes for specifying reverse reference attribute field name on target type/interface 162 | // will need to add this to bootstrap & to attribute props 163 | :extGraphQL/reverseRefField // type :db/keyword; unique identity? (only one attribute should point to a given reverseRefField value ... guarantees referring attribute & entity type?) 164 | 165 | // e.g., 166 | post { 167 | creator {} // [?post :post/creator ?creator] ... (meta-attribute [:post/creator :extGraphQL/reverseRefField :user/posts]) 168 | } 169 | 170 | user { 171 | posts {} // [?user :post/_creator ?post] 172 | } 173 | -------------------------------------------------------------------------------- /resources/installExtensionAttributes.clj: -------------------------------------------------------------------------------- 1 | ; This transaction installs the GraphQL extension's schema attributes 2 | ; Just copy and paste this into the tx-data field in the REST API 3 | [ 4 | ; - Type Attributes - 5 | ; Type Name 6 | {:db/id #db/id[:db.part/db] 7 | :db/ident :extGraphQL.type/name 8 | :db/valueType :db.type/string 9 | :db/cardinality :db.cardinality/one 10 | :db/unique :db.unique/value 11 | :db/doc "A type's name in the GraphQL type system" 12 | :db.install/_attribute :db.part/db} 13 | ; Type Namespace 14 | {:db/id #db/id[:db.part/db] 15 | :db/ident :extGraphQL.type/namespace 16 | :db/valueType :db.type/keyword 17 | :db/cardinality :db.cardinality/one 18 | :db/unique :db.unique/value 19 | :db/doc "The namespace of a type's attribute idents in the db" 20 | :db.install/_attribute :db.part/db} 21 | ; Type Description Doc 22 | {:db/id #db/id[:db.part/db] 23 | :db/ident :extGraphQL.type/doc 24 | :db/valueType :db.type/string 25 | :db/cardinality :db.cardinality/one 26 | :db/doc "The description of a type" 27 | :db.install/_attribute :db.part/db} 28 | 29 | ; - Interface Attributes - 30 | ; Interface Name 31 | {:db/id #db/id[:db.part/db] 32 | :db/ident :extGraphQL.interface/name 33 | :db/valueType :db.type/string 34 | :db/cardinality :db.cardinality/one 35 | :db/unique :db.unique/value 36 | :db/doc "An interface's name in the GraphQL type system" 37 | :db.install/_attribute :db.part/db} 38 | ; Interface Implementations 39 | {:db/id #db/id[:db.part/db] 40 | :db/ident :extGraphQL.interface/implementations 41 | :db/valueType :db.type/ref 42 | :db/cardinality :db.cardinality/many 43 | :db/doc "The types that implement an interface" 44 | :db.install/_attribute :db.part/db} 45 | ; Interface Description Doc 46 | {:db/id #db/id[:db.part/db] 47 | :db/ident :extGraphQL.interface/doc 48 | :db/valueType :db.type/string 49 | :db/cardinality :db.cardinality/one 50 | :db/doc "The description of an interface" 51 | :db.install/_attribute :db.part/db} 52 | 53 | ; - Meta Attributes - 54 | ; Ref Target 55 | {:db/id #db/id[:db.part/db] 56 | :db/ident :extGraphQL/refTarget 57 | :db/valueType :db.type/ref 58 | :db/cardinality :db.cardinality/one 59 | :db/doc "The type or interface that a reference attribute targets" 60 | :db.install/_attribute :db.part/db} 61 | ; Enum Values 62 | {:db/id #db/id[:db.part/db] 63 | :db/ident :extGraphQL/enumValues 64 | :db/valueType :db.type/ref 65 | :db/cardinality :db.cardinality/many 66 | :db/doc "The possible values of an enumeration-type reference attribute" 67 | :db.install/_attribute :db.part/db} 68 | 69 | ; (Attribute) Required 70 | ; {:db/id #db/id[:db.part/db] 71 | ; :db/ident :extGraphQL/required 72 | ; :db/valueType :db.type/boolean 73 | ; :db/cardinality :db.cardinality/one 74 | ; :db/doc "Whether an attribute must have a value for its entity to be valid" 75 | ; :db.install/_attribute :db.part/db} 76 | ] 77 | -------------------------------------------------------------------------------- /resources/installedInterfacesQuery.clj: -------------------------------------------------------------------------------- 1 | ; This query gets installed interfaces 2 | ; Just copy and paste this into the q field in the Query REST API 3 | [:find 4 | [(pull ?e [ 5 | :extGraphQL.interface/name 6 | {:extGraphQL.interface/implementations [:db/ident :extGraphQL.type/name]} 7 | :extGraphQL.interface/doc 8 | {:extGraphQL/_refTarget [:db/ident]} 9 | ]) ...] :where [?e :extGraphQL.interface/name _]] 10 | -------------------------------------------------------------------------------- /resources/installedTypesQuery.clj: -------------------------------------------------------------------------------- 1 | ; This query gets installed types 2 | ; Just copy and paste this into the q field in the Query REST API 3 | [:find 4 | [(pull ?e [ 5 | :extGraphQL.type/name 6 | :extGraphQL.type/namespace 7 | :extGraphQL.type/doc 8 | {:extGraphQL/_refTarget [:db/ident]} 9 | ]) ...] :where [?e :extGraphQL.type/name _]] 10 | -------------------------------------------------------------------------------- /resources/schemaQuery.clj: -------------------------------------------------------------------------------- 1 | ; This query gets schema meta attributes 2 | ; Just copy and paste this into the q field in the Query REST API 3 | [:find 4 | [(pull ?e [ 5 | :db/id 6 | :db/ident 7 | :db/doc 8 | :db/index 9 | :db/fullText 10 | {:db/valueType [:db/ident]} 11 | {:db/cardinality [:db/ident]} 12 | {:db/unique [:db/ident]} 13 | :extGraphQL/required 14 | {:extGraphQL/refTarget [:db/ident]} 15 | {:extGraphQL/enumValues [:db/ident]} 16 | ]) ...] :where [?e :db/valueType _]] 17 | -------------------------------------------------------------------------------- /src/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | import installExtensionAttributes from './utils/installExtensionAttributes'; 2 | import applySchemaImpliedTypes from './utils/applySchemaImpliedTypes'; 3 | import resolveArbitraryReferenceAttributesViaCLI from './utils/resolveArbitraryReferenceAttributesViaCLI'; 4 | import resolveUnidirectionalReferenceAttributesViaCLI from './utils/resolveUnidirectionalReferenceAttributesViaCLI'; 5 | 6 | export default function bootstrap(apiUrl, dbAlias) { 7 | return installExtensionAttributes(apiUrl, dbAlias) 8 | .then(() => applySchemaImpliedTypes(apiUrl, dbAlias)) 9 | .then(() => resolveArbitraryReferenceAttributesViaCLI(apiUrl, dbAlias)) 10 | .then(() => resolveUnidirectionalReferenceAttributesViaCLI(apiUrl, dbAlias)); 11 | } 12 | -------------------------------------------------------------------------------- /src/bootstrap/utils/applySchemaImpliedTypes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | import querySchemaImpliedTypes from '../../utils/querySchemaImpliedTypes'; 4 | import queryInstalledTypes from '../../utils/queryInstalledTypes'; 5 | import { getTypeNamespaceFromTypeName } from '../../utils/inflect'; 6 | import { difference, isEmpty, keys } from 'underscore'; 7 | 8 | export default function applySchemaImpliedTypes(apiUrl, dbAlias) { 9 | const db = consumer(apiUrl, dbAlias); 10 | 11 | return Promise.all([ 12 | querySchemaImpliedTypes(apiUrl, dbAlias), 13 | queryInstalledTypes(apiUrl, dbAlias), 14 | ]) 15 | .then(([schemaImpliedTypes, installedTypes]) => { 16 | const schemaImpliedTypeNames = keys(schemaImpliedTypes); 17 | const installedTypeNames = keys(installedTypes); 18 | const typesNamesToInstall = difference(schemaImpliedTypeNames, installedTypeNames); 19 | const typesNamesToRetract = difference(installedTypeNames, schemaImpliedTypeNames); 20 | 21 | // If there are no types to install or retract, bail... 22 | if (isEmpty(typesNamesToInstall) && isEmpty(typesNamesToRetract)) { 23 | return null; 24 | } 25 | 26 | console.log('typesNamesToInstall:', typesNamesToInstall); 27 | console.log('typesNamesToRetract:', typesNamesToRetract); 28 | const installTypeTransactionsEdn = typesNamesToInstall.map(typeName => getInstallTypeTransactionEdn(typeName)); 29 | const retractTypeTransactionsEdn = typesNamesToRetract.map(typeName => getRetractTypeTransactionEdn(typeName)); 30 | 31 | const applyTypesTransactionEdn = new edn.Vector([ 32 | ...installTypeTransactionsEdn, 33 | ...retractTypeTransactionsEdn, 34 | ]); 35 | 36 | return db.transact(applyTypesTransactionEdn); 37 | }); 38 | } 39 | 40 | function getInstallTypeTransactionEdn(typeName) { 41 | const typeNamespace = getTypeNamespaceFromTypeName(typeName); 42 | 43 | return new edn.Map([ 44 | edn.kw(':db/id'), new edn.Tagged(new edn.Tag('db/id'), new edn.Vector([edn.kw(':db.part/user')])), 45 | edn.kw(':extGraphQL.type/name'), typeName, 46 | edn.kw(':extGraphQL.type/namespace'), edn.kw(typeNamespace), 47 | ]); 48 | } 49 | 50 | function getRetractTypeTransactionEdn(typeName) { 51 | return new edn.Vector([ 52 | edn.kw(':db.fn/retractEntity'), 53 | new edn.Vector([edn.kw(':extGraphQL.type/name'), typeName]), 54 | ]); 55 | } 56 | -------------------------------------------------------------------------------- /src/bootstrap/utils/installExtensionAttributes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | import { 4 | extensionAttributeIdentKeywords, 5 | extensionAttributeValueTypeKeywords, 6 | extensionAttributeCardinalityKeywords, 7 | extensionAttributeUniqueKeywords, 8 | extensionAttributeDocStrings, 9 | TYPE_NAME, 10 | TYPE_NAMESPACE, 11 | TYPE_DOC, 12 | INTERFACE_NAME, 13 | INTERFACE_IMPLEMENTATIONS, 14 | INTERFACE_DOC, 15 | REF_TARGET, 16 | ENUM_VALUES, 17 | REVERSE_REF_FIELD, 18 | } from '../../constants/extensionAttributes'; 19 | 20 | // (Extension attributes (partials)) 21 | const typeNameAttributePartial = [ 22 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_NAME], 23 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[TYPE_NAME], 24 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[TYPE_NAME], 25 | edn.kw(':db/unique'), extensionAttributeUniqueKeywords[TYPE_NAME], 26 | edn.kw(':db/doc'), extensionAttributeDocStrings[TYPE_NAME], 27 | ]; 28 | const typeNamespaceAttributePartial = [ 29 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_NAMESPACE], 30 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[TYPE_NAMESPACE], 31 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[TYPE_NAMESPACE], 32 | edn.kw(':db/unique'), extensionAttributeUniqueKeywords[TYPE_NAMESPACE], 33 | edn.kw(':db/doc'), extensionAttributeDocStrings[TYPE_NAMESPACE], 34 | ]; 35 | const typeDocAttributePartial = [ 36 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_DOC], 37 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[TYPE_DOC], 38 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[TYPE_DOC], 39 | edn.kw(':db/doc'), extensionAttributeDocStrings[TYPE_DOC], 40 | ]; 41 | const interfaceNameAttributePartial = [ 42 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_NAME], 43 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[INTERFACE_NAME], 44 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[INTERFACE_NAME], 45 | edn.kw(':db/unique'), extensionAttributeUniqueKeywords[INTERFACE_NAME], 46 | edn.kw(':db/doc'), extensionAttributeDocStrings[INTERFACE_NAME], 47 | ]; 48 | const interfaceImplementationsAttributePartial = [ 49 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_IMPLEMENTATIONS], 50 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[INTERFACE_IMPLEMENTATIONS], 51 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[INTERFACE_IMPLEMENTATIONS], 52 | edn.kw(':db/doc'), extensionAttributeDocStrings[INTERFACE_IMPLEMENTATIONS], 53 | ]; 54 | const interfaceDocAttributePartial = [ 55 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_DOC], 56 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[INTERFACE_DOC], 57 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[INTERFACE_DOC], 58 | edn.kw(':db/doc'), extensionAttributeDocStrings[INTERFACE_DOC], 59 | ]; 60 | const refTargetAttributePartial = [ 61 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[REF_TARGET], 62 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[REF_TARGET], 63 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[REF_TARGET], 64 | edn.kw(':db/doc'), extensionAttributeDocStrings[REF_TARGET], 65 | ]; 66 | const enumValuesAttributePartial = [ 67 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[ENUM_VALUES], 68 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[ENUM_VALUES], 69 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[ENUM_VALUES], 70 | edn.kw(':db/doc'), extensionAttributeDocStrings[ENUM_VALUES], 71 | ]; 72 | const reverseRefFieldAttributePartial = [ 73 | edn.kw(':db/ident'), extensionAttributeIdentKeywords[REVERSE_REF_FIELD], 74 | edn.kw(':db/valueType'), extensionAttributeValueTypeKeywords[REVERSE_REF_FIELD], 75 | edn.kw(':db/cardinality'), extensionAttributeCardinalityKeywords[REVERSE_REF_FIELD], 76 | edn.kw(':db/unique'), extensionAttributeUniqueKeywords[REVERSE_REF_FIELD], 77 | edn.kw(':db/doc'), extensionAttributeDocStrings[REVERSE_REF_FIELD], 78 | ]; 79 | 80 | export default function installExtensionAttributes(apiUrl, dbAlias) { 81 | const db = consumer(apiUrl, dbAlias); 82 | const installExtensionAttributesTransactionEdn = new edn.Vector([ 83 | installAttributePartial(typeNameAttributePartial), 84 | installAttributePartial(typeNamespaceAttributePartial), 85 | installAttributePartial(typeDocAttributePartial), 86 | installAttributePartial(interfaceNameAttributePartial), 87 | installAttributePartial(interfaceImplementationsAttributePartial), 88 | installAttributePartial(interfaceDocAttributePartial), 89 | installAttributePartial(refTargetAttributePartial), 90 | installAttributePartial(enumValuesAttributePartial), 91 | installAttributePartial(reverseRefFieldAttributePartial), 92 | ]); 93 | 94 | // Install extesnion attributes if they are not yet installed 95 | return queryWhetherExtensionAttributesAreInstalled(apiUrl, dbAlias) 96 | .then(installed => { 97 | if (!installed) { 98 | return db.transact(installExtensionAttributesTransactionEdn); 99 | } 100 | }) 101 | .catch(error => { 102 | console.error('Error installing extension attributes... This can happen if some, but not all of the extesnion attributes are installed...'); 103 | throw error; 104 | }); 105 | } 106 | 107 | function installAttributePartial(attributePartial) { 108 | return new edn.Map([ 109 | edn.kw(':db/id'), new edn.Tagged(new edn.Tag('db/id'), new edn.Vector([edn.kw(':db.part/db')])), 110 | ...attributePartial, 111 | edn.kw(':db.install/_attribute'), edn.kw(':db.part/db'), 112 | ]); 113 | } 114 | 115 | function queryWhetherExtensionAttributesAreInstalled(apiUrl, dbAlias) { 116 | const db = consumer(apiUrl, dbAlias); 117 | const extensionAttributesQueryPartial = [ 118 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_NAME]]), 119 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_NAMESPACE]]), 120 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[TYPE_DOC]]), 121 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_NAME]]), 122 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_IMPLEMENTATIONS]]), 123 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[INTERFACE_DOC]]), 124 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[REF_TARGET]]), 125 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[ENUM_VALUES]]), 126 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), extensionAttributeIdentKeywords[REVERSE_REF_FIELD]]), 127 | ]; 128 | const installedAttributesQueryEdn = new edn.Vector([ 129 | edn.kw(':find'), edn.sym('?attr'), 130 | edn.kw(':where'), new edn.List([edn.sym('or'), 131 | ...extensionAttributesQueryPartial, 132 | ]), 133 | ]); 134 | 135 | return db.query(installedAttributesQueryEdn) 136 | .then(installedAttributes => !(installedAttributes.length < extensionAttributesQueryPartial.length)); 137 | } 138 | -------------------------------------------------------------------------------- /src/bootstrap/utils/queryArbitraryReferenceAttributes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | 4 | export default function queryArbitraryReferenceAttributes(apiUrl, dbAlias) { 5 | const db = consumer(apiUrl, dbAlias); 6 | const arbitraryReferenceAttributesQuery = new edn.Vector([ 7 | edn.kw(':find'), edn.sym('?ident'), edn.sym('?doc'), 8 | edn.kw(':where'), 9 | new edn.Vector([ 10 | new edn.List([edn.sym('missing?'), edn.sym('$'), edn.sym('?attr'), edn.kw(':extGraphQL/refTarget')]), 11 | ]), 12 | new edn.Vector([ 13 | new edn.List([edn.sym('missing?'), edn.sym('$'), edn.sym('?attr'), edn.kw(':extGraphQL/enumValues')]), 14 | ]), 15 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), edn.sym('?ident')]), 16 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/doc'), edn.sym('?doc')]), 17 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/valueType'), edn.kw(':db.type/ref')]), 18 | ]); 19 | 20 | return db.query(arbitraryReferenceAttributesQuery); 21 | } 22 | -------------------------------------------------------------------------------- /src/bootstrap/utils/queryUnidirectionalReferenceAttributes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | 4 | export default function queryUnidirectionalReferenceAttributes(apiUrl, dbAlias) { 5 | const db = consumer(apiUrl, dbAlias); 6 | const unidirectionalReferenceAttributes = new edn.Vector([ 7 | edn.kw(':find'), edn.sym('?ident'), edn.sym('?doc'), edn.sym('?refTargetTypeName'), 8 | edn.kw(':where'), 9 | new edn.Vector([ 10 | new edn.List([edn.sym('missing?'), edn.sym('$'), edn.sym('?attr'), edn.kw(':extGraphQL/enumValues')]), 11 | ]), 12 | new edn.Vector([ 13 | new edn.List([edn.sym('missing?'), edn.sym('$'), edn.sym('?attr'), edn.kw(':extGraphQL/reverseRefField')]), 14 | ]), 15 | new edn.Vector([edn.sym('?attr'), edn.kw(':extGraphQL/refTarget'), edn.sym('?refTarget')]), 16 | new edn.Vector([edn.sym('?refTarget'), edn.kw(':extGraphQL.type/name'), edn.sym('?refTargetTypeName')]), 17 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/ident'), edn.sym('?ident')]), 18 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/doc'), edn.sym('?doc')]), 19 | new edn.Vector([edn.sym('?attr'), edn.kw(':db/valueType'), edn.kw(':db.type/ref')]), 20 | ]); 21 | 22 | return db.query(unidirectionalReferenceAttributes); 23 | } 24 | -------------------------------------------------------------------------------- /src/bootstrap/utils/resolveArbitraryReferenceAttributesViaCLI.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | import queryArbitraryReferenceAttributes from './queryArbitraryReferenceAttributes'; 4 | import queryInstalledTypes from '../../utils/queryInstalledTypes'; 5 | import queryInstalledInterfaces from '../../utils/queryInstalledInterfaces'; 6 | import { prompt, Separator } from 'inquirer'; 7 | import { isEmpty, keys } from 'underscore'; 8 | 9 | // (Configuration contants) 10 | const SYSTEM_ATTRIBUTE_NAMESPACES = [':db', ':fressian', ':extGraphQL']; 11 | 12 | // (Value constants) 13 | const ENUM = 'ENUM'; 14 | const INTERFACE = 'INTERFACE'; 15 | const NEW_INTERFACE = 'NEW_INTERFACE'; 16 | const SKIP = 'SKIP'; 17 | 18 | export default function resolveArbitraryReferenceAttributesViaCLI(apiUrl, dbAlias) { 19 | const db = consumer(apiUrl, dbAlias); 20 | 21 | return Promise.all([ 22 | queryArbitraryReferenceAttributes(apiUrl, dbAlias), 23 | queryInstalledTypes(apiUrl, dbAlias), 24 | queryInstalledInterfaces(apiUrl, dbAlias), 25 | ]) 26 | .then(([arbitraryReferenceAttributes, installedTypes, installedInterfaces]) => { 27 | return arbitraryReferenceAttributes.reduce((promiseChain, [ident, doc]) => { 28 | const attributeNamespace = edn.kw(ident).ns; 29 | 30 | // Exclude system-namespaced attributes from resolution... 31 | if (SYSTEM_ATTRIBUTE_NAMESPACES.find(ns => attributeNamespace.indexOf(ns) >= 0)) { 32 | return promiseChain; 33 | } 34 | 35 | return promiseChain 36 | .then(aggregateAnswers => { 37 | return promptForArbitraryReferenceTypeSpecification({ ident, doc }, installedTypes, installedInterfaces, aggregateAnswers); 38 | }); 39 | }, Promise.resolve([])); 40 | }) 41 | .then(answers => { 42 | return Promise.all(answers.reduce((aggregateTransactionsEdnArray, { ident, spec }) => { 43 | let transactionEdn; 44 | 45 | if (spec.typeName || spec.interfaceName) { 46 | transactionEdn = buildRefTargetAttributeTransaction(ident, spec); 47 | } 48 | 49 | if (spec.enumNamespace) { 50 | transactionEdn = buildEnumValuesAttributeTransaction(ident, spec, db); 51 | } 52 | 53 | if (spec.newInterface) { 54 | transactionEdn = buildNewInterfaceAttributeTransaction(ident, spec, db); 55 | } 56 | 57 | if (transactionEdn) { 58 | return [ 59 | ...aggregateTransactionsEdnArray, 60 | transactionEdn, 61 | ]; 62 | } 63 | 64 | return aggregateTransactionsEdnArray; 65 | }, [])) 66 | .then(transactionsEdnArray => { 67 | const transactionsEdn = new edn.Vector(transactionsEdnArray); 68 | 69 | // Don't transact if there's nothing to transact... 70 | if (isEmpty(transactionsEdnArray)) { 71 | return null; 72 | } 73 | 74 | return db.transact(transactionsEdn); 75 | }); 76 | }); 77 | } 78 | 79 | // - Transaction builder helpers - 80 | function buildRefTargetAttributeTransaction(attributeIdent, spec) { 81 | const targetLookupRef = spec.typeName 82 | ? new edn.Vector([edn.kw(':extGraphQL.type/name'), spec.typeName]) 83 | : new edn.Vector([edn.kw(':extGraphQL.interface/name'), spec.interfaceName]); 84 | 85 | return new edn.Map([ 86 | edn.kw(':db/id'), edn.kw(attributeIdent), 87 | edn.kw(':extGraphQL/refTarget'), targetLookupRef, 88 | ]); 89 | } 90 | 91 | function buildEnumValuesAttributeTransaction(attributeIdent, spec, db) { 92 | const { enumNamespace } = spec; 93 | 94 | return db.query(new edn.Vector([ 95 | edn.kw(':find'), edn.sym('?ident'), edn.kw(':where'), 96 | new edn.Vector([ 97 | edn.sym('?e'), edn.kw(':db/ident'), edn.sym('?ident'), 98 | ]), 99 | ])) 100 | .then(idents => idents.filter(ident => edn.kw(ident).ns === enumNamespace)) 101 | .then(enumValues => enumValues.map(enumValue => edn.kw(enumValue))) 102 | .then(enumValueKeywords => { 103 | return new edn.Map([ 104 | edn.kw(':db/id'), edn.kw(attributeIdent), 105 | edn.kw(':extGraphQL/enumValues'), new edn.Vector(enumValueKeywords), 106 | ]); 107 | }); 108 | } 109 | 110 | function buildNewInterfaceAttributeTransaction(attributeIdent, spec, db) { 111 | const { newInterface: { name, types } } = spec; 112 | const newInterfaceLookupRef = new edn.Vector([edn.kw(':extGraphQL.interface/name'), name]); 113 | 114 | return db.query(new edn.Vector([ 115 | edn.kw(':find'), edn.sym('?e'), edn.kw(':where'), 116 | new edn.List([edn.sym('or'), 117 | ...types.map(type => new edn.Vector([edn.sym('?e'), edn.kw(':extGraphQL.type/name'), type])), 118 | ]), 119 | ])) 120 | .then(typeVectors => typeVectors.map(vector => vector[0])) 121 | .then(typeEntities => db.transact(new edn.Vector([new edn.Map([ 122 | edn.kw(':db/id'), new edn.Tagged(new edn.Tag('db/id'), new edn.Vector([edn.kw(':db.part/user')])), 123 | edn.kw(':extGraphQL.interface/name'), name, 124 | edn.kw(':extGraphQL.interface/implementations'), new edn.Vector(typeEntities), 125 | ])]))) 126 | .then(() => { 127 | return new edn.Map([ 128 | edn.kw(':db/id'), edn.kw(attributeIdent), 129 | edn.kw(':extGraphQL/refTarget'), newInterfaceLookupRef, 130 | ]); 131 | }); 132 | } 133 | 134 | // - CLI prompt helpers - 135 | function promptForArbitraryReferenceTypeSpecification({ ident, doc }, installedTypes, installedInterfaces, aggregateAnswers) { 136 | return promptWithConfirm([{ 137 | name: 'refTarget', 138 | type: 'list', 139 | message: ` 140 | What type/enum/interface does the attribute "${ident}" target? 141 | [doc: "${doc}"] 142 | `, 143 | choices: [ 144 | ENUM, 145 | INTERFACE, 146 | SKIP, 147 | new Separator(), 148 | ...keys(installedTypes), 149 | ], 150 | }], ({ refTarget }) => { 151 | // if user selected enum for, prompt for prefix 152 | if (refTarget === ENUM) { 153 | return promptForEnumNamespace({ ident, doc }, aggregateAnswers); 154 | } 155 | 156 | // if user selected interface, prompt for types 157 | if (refTarget === INTERFACE) { 158 | return promptForInterfaceSpecification({ ident, doc }, installedTypes, installedInterfaces, aggregateAnswers); 159 | } 160 | 161 | // If user selected to skip, skip... 162 | if (refTarget === SKIP) { 163 | return aggregateAnswers; 164 | } 165 | 166 | return [ 167 | ...aggregateAnswers, 168 | { ident, spec: { typeName: refTarget } }, 169 | ]; 170 | }, aggregateAnswers); 171 | } 172 | 173 | function promptForEnumNamespace({ ident, doc }, aggregateAnswers) { 174 | return promptWithConfirm([{ 175 | name: 'enumNamespace', 176 | type: 'input', 177 | message: ` 178 | Enter the namespace for enum values for the attribute "${ident}" 179 | [doc: "${doc}"] 180 | `, 181 | }], ({ enumNamespace }) => { 182 | return [ 183 | ...aggregateAnswers, 184 | { ident, spec: { enumNamespace } }, 185 | ]; 186 | }, aggregateAnswers); 187 | } 188 | 189 | function promptForInterfaceSpecification({ ident, doc }, installedTypes, installedInterfaces, aggregateAnswers) { 190 | return promptWithConfirm([{ 191 | name: 'interfaceChoice', 192 | type: 'list', 193 | message: `What interface does the attribute "${ident}" target?`, 194 | choices: [ 195 | NEW_INTERFACE, 196 | new Separator(), 197 | ...keys(installedInterfaces), 198 | ], 199 | }], ({ interfaceChoice }) => { 200 | // if user selected enum for, prompt for prefix 201 | if (interfaceChoice === NEW_INTERFACE) { 202 | return promptForNewInterfaceSpecification({ ident, doc }, installedTypes, aggregateAnswers); 203 | } 204 | 205 | return [ 206 | ...aggregateAnswers, 207 | { ident, spec: { interfaceName: interfaceChoice } }, 208 | ]; 209 | }, aggregateAnswers); 210 | } 211 | 212 | function promptForNewInterfaceSpecification({ ident, doc }, installedTypes, aggregateAnswers) { 213 | return promptWithConfirm([{ 214 | name: 'name', 215 | type: 'input', 216 | message: 'Enter the name of the new interface (should be PascalCased):', 217 | }, { 218 | name: 'types', 219 | type: 'checkbox', 220 | message: 'Choose the types that should implement the interface', 221 | choices: [ 222 | ...keys(installedTypes), 223 | ], 224 | }], ({ name, types }) => { 225 | return [ 226 | ...aggregateAnswers, 227 | { ident, spec: { newInterface: { name, types } } }, 228 | ]; 229 | }, aggregateAnswers); 230 | } 231 | 232 | function promptWithConfirm(questions, transformAnswers, aggregateAnswers) { 233 | return new Promise(resolve => { 234 | return prompt([...questions, { 235 | name: 'confirmed', 236 | type: 'confirm', 237 | message: 'Confirm?', 238 | }], (answers) => { 239 | if (!answers.confirmed) { 240 | return resolve(promptWithConfirm(questions, transformAnswers, aggregateAnswers)); 241 | } 242 | 243 | return resolve(transformAnswers(answers)); 244 | }); 245 | }); 246 | } 247 | -------------------------------------------------------------------------------- /src/bootstrap/utils/resolveUnidirectionalReferenceAttributesViaCLI.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import edn from 'jsedn'; 3 | import queryUnidirectionalReferenceAttributes from './queryUnidirectionalReferenceAttributes'; 4 | import queryInstalledTypes from '../../utils/queryInstalledTypes'; 5 | import queryInstalledInterfaces from '../../utils/queryInstalledInterfaces'; 6 | import { getAttributeIdentFromAttributeNameAndTypeName } from '../../utils/inflect'; 7 | import { prompt } from 'inquirer'; 8 | import { isEmpty } from 'underscore'; 9 | 10 | // (Configuration contants) 11 | const SYSTEM_ATTRIBUTE_NAMESPACES = [':db', ':fressian', ':extGraphQL']; 12 | 13 | export default function resolveUnidirectionalReferenceAttributesViaCLI(apiUrl, dbAlias) { 14 | const db = consumer(apiUrl, dbAlias); 15 | 16 | return Promise.all([ 17 | queryUnidirectionalReferenceAttributes(apiUrl, dbAlias), 18 | queryInstalledTypes(apiUrl, dbAlias), 19 | queryInstalledInterfaces(apiUrl, dbAlias), 20 | ]) 21 | .then(([unidirectionalReferenceAttributes, installedTypes, installedInterfaces]) => { 22 | return unidirectionalReferenceAttributes.reduce((promiseChain, [ident, doc, refTargetTypeName]) => { 23 | const attributeNamespace = edn.kw(ident).ns; 24 | 25 | // Exclude system-namespaced attributes from resolution... 26 | if (SYSTEM_ATTRIBUTE_NAMESPACES.find(ns => attributeNamespace.indexOf(ns) >= 0)) { 27 | return promiseChain; 28 | } 29 | 30 | return promiseChain 31 | .then(aggregateAnswers => { 32 | return promptForReverseRefFieldSelection({ ident, doc, refTargetTypeName }, installedTypes, installedInterfaces, aggregateAnswers); 33 | }); 34 | }, Promise.resolve([])); 35 | }) 36 | .then(answers => { 37 | return Promise.all(answers.reduce((aggregateTransactionsEdnArray, { ident, reverseRefField }) => { 38 | const transactionEdn = buildReverseRefFieldAttributeTransaction(ident, reverseRefField); 39 | 40 | return [ 41 | ...aggregateTransactionsEdnArray, 42 | transactionEdn, 43 | ]; 44 | }, [])) 45 | .then(transactionsEdnArray => { 46 | const transactionsEdn = new edn.Vector(transactionsEdnArray); 47 | 48 | // Don't transact if there's nothing to transact... 49 | if (isEmpty(transactionsEdnArray)) { 50 | return null; 51 | } 52 | 53 | return db.transact(transactionsEdn); 54 | }); 55 | }); 56 | } 57 | 58 | // - Transaction builder helpers - 59 | function buildReverseRefFieldAttributeTransaction(attributeIdent, reverseRefField) { 60 | return new edn.Map([ 61 | edn.kw(':db/id'), edn.kw(attributeIdent), 62 | edn.kw(':extGraphQL/reverseRefField'), reverseRefField, 63 | ]); 64 | } 65 | 66 | // - CLI prompt helpers - 67 | function promptForReverseRefFieldSelection({ ident, doc, refTargetTypeName }, installedTypes, installedInterfaces, aggregateAnswers) { 68 | return promptWithConfirm([{ 69 | name: 'reverseRefField', 70 | type: 'input', 71 | message: ` 72 | What field of type ${refTargetTypeName} should provide a reverse reference to ${ident}. 73 | [doc: "${doc}"] 74 | `, 75 | }], ({ reverseRefField }) => { 76 | const reverseRefFieldKeyword = getAttributeIdentFromAttributeNameAndTypeName(reverseRefField, refTargetTypeName); 77 | 78 | return [ 79 | ...aggregateAnswers, 80 | { ident, reverseRefField: reverseRefFieldKeyword }, 81 | ]; 82 | }, aggregateAnswers); 83 | } 84 | 85 | function promptWithConfirm(questions, transformAnswers, aggregateAnswers) { 86 | return new Promise(resolve => { 87 | return prompt([...questions, { 88 | name: 'confirmed', 89 | type: 'confirm', 90 | message: 'Confirm?', 91 | }], (answers) => { 92 | if (!answers.confirmed) { 93 | return resolve(promptWithConfirm(questions, transformAnswers, aggregateAnswers)); 94 | } 95 | 96 | return resolve(transformAnswers(answers)); 97 | }); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /src/constants/datomicCardinalities.js: -------------------------------------------------------------------------------- 1 | import edn from 'jsedn'; 2 | 3 | // (Datomic cardinality ident string constants) 4 | export const ONE = ':db.cardinality/one'; 5 | export const MANY = ':db.cardinality/many'; 6 | 7 | // (Exhaustive map of [datomic cardinality constant] -> [datomic cardinality ident string]) 8 | export const datomicCardinalityIdents = { 9 | ONE, 10 | MANY, 11 | }; 12 | 13 | // (Exhaustive map of [datomic cardinality constant] -> [datomic cardinality ident keyword])) 14 | export const datomicCardinalityIdentKeywords = { 15 | [ONE]: edn.kw(ONE), 16 | [MANY]: edn.kw(MANY), 17 | }; 18 | -------------------------------------------------------------------------------- /src/constants/datomicUniques.js: -------------------------------------------------------------------------------- 1 | import edn from 'jsedn'; 2 | 3 | // (Datomic unique ident string constants) 4 | export const VALUE = ':db.unique/value'; 5 | export const IDENTITY = ':db.unique/identity'; 6 | 7 | // (Exhaustive map of [datomic unique constant] -> [datomic unique ident string]) 8 | export const datomicUniqueIdents = { 9 | VALUE, 10 | IDENTITY, 11 | }; 12 | 13 | // (Exhaustive map of [datomic unique constant] -> [datomic unique ident keyword])) 14 | export const datomicUniqueIdentKeywords = { 15 | [VALUE]: edn.kw(VALUE), 16 | [IDENTITY]: edn.kw(IDENTITY), 17 | }; 18 | -------------------------------------------------------------------------------- /src/constants/datomicValueTypes.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLString } from 'graphql'; 2 | import edn from 'jsedn'; 3 | 4 | // (Boolean type ident string constants) 5 | export const BOOLEAN = ':db.type/boolean'; 6 | // (Float-type ident string constants) 7 | export const FLOAT = ':db.type/float'; 8 | export const DOUBLE = ':db.type/double'; 9 | export const BIG_DEC = ':db.type/bigdec'; 10 | // (Integer-type ident string constants) 11 | export const LONG = ':db.type/long'; 12 | export const BIG_INT = ':db.type/bigint'; 13 | // (String-type ident string constants) 14 | export const KEYWORD = ':db.type/keyword'; 15 | export const INSTANT = ':db.type/instant'; 16 | export const STRING = ':db.type/string'; 17 | export const UUID = ':db.type/uuid'; 18 | export const URI = ':db.type/uri'; 19 | export const BYTES = ':db.type/bytes'; 20 | // (Ref-type ident string constants) 21 | export const REF = ':db.type/ref'; 22 | 23 | // (Exhaustive map of [Datomic value type constant] -> [Datomic value type ident string]) 24 | export const datomicValueTypeIdents = { 25 | BOOLEAN, 26 | FLOAT, 27 | DOUBLE, 28 | BIG_DEC, 29 | LONG, 30 | BIG_INT, 31 | KEYWORD, 32 | INSTANT, 33 | STRING, 34 | UUID, 35 | URI, 36 | BYTES, 37 | REF, 38 | }; 39 | 40 | // (Exhaustive map of [extension attribute constant] -> [extension attribute ident keyword]) 41 | export const datomicValueTypeKeywords = { 42 | [BOOLEAN]: edn.kw(BOOLEAN), 43 | [FLOAT]: edn.kw(FLOAT), 44 | [DOUBLE]: edn.kw(DOUBLE), 45 | [BIG_DEC]: edn.kw(BIG_DEC), 46 | [LONG]: edn.kw(LONG), 47 | [BIG_INT]: edn.kw(BIG_INT), 48 | [KEYWORD]: edn.kw(KEYWORD), 49 | [INSTANT]: edn.kw(INSTANT), 50 | [STRING]: edn.kw(STRING), 51 | [UUID]: edn.kw(UUID), 52 | [URI]: edn.kw(URI), 53 | [BYTES]: edn.kw(BYTES), 54 | [REF]: undefined, 55 | }; 56 | 57 | // (Exhaustive map of [Datomic value type constant] -> [GraphQL type]) 58 | export const datomicValueTypeGraphQLTypes = { 59 | [BOOLEAN]: GraphQLBoolean, 60 | 61 | [FLOAT]: GraphQLFloat, 62 | [DOUBLE]: GraphQLFloat, 63 | [BIG_DEC]: GraphQLFloat, 64 | 65 | [LONG]: GraphQLInt, 66 | [BIG_INT]: GraphQLInt, 67 | 68 | [KEYWORD]: GraphQLString, 69 | [INSTANT]: GraphQLString, 70 | [STRING]: GraphQLString, 71 | [UUID]: GraphQLString, 72 | [URI]: GraphQLString, 73 | [BYTES]: GraphQLString, 74 | 75 | [REF]: undefined, 76 | }; 77 | -------------------------------------------------------------------------------- /src/constants/extensionAttributes.js: -------------------------------------------------------------------------------- 1 | import edn from 'jsedn'; 2 | import { 3 | datomicValueTypeKeywords, 4 | KEYWORD, 5 | STRING, 6 | REF, 7 | } from './datomicValueTypes'; 8 | import { 9 | datomicCardinalityIdentKeywords, 10 | ONE as CARDINALITY_ONE, 11 | MANY as CARDINALITY_MANY, 12 | } from './datomicCardinalities'; 13 | import { 14 | datomicUniqueIdentKeywords, 15 | VALUE as UNIQUE_VALUE, 16 | } from './datomicUniques'; 17 | 18 | // (Extension attribute ident string constants) 19 | export const TYPE_NAME = ':extGraphQL.type/name'; 20 | export const TYPE_NAMESPACE = ':extGraphQL.type/namespace'; 21 | export const TYPE_DOC = ':extGraphQL.type/doc'; 22 | export const INTERFACE_NAME = ':extGraphQL.interface/name'; 23 | export const INTERFACE_IMPLEMENTATIONS = ':extGraphQL.interface/implementations'; 24 | export const INTERFACE_DOC = ':extGraphQL.interface/doc'; 25 | export const REF_TARGET = ':extGraphQL/refTarget'; 26 | export const ENUM_VALUES = ':extGraphQL/enumValues'; 27 | export const REVERSE_REF_FIELD = ':extGraphQL/reverseRefField'; 28 | 29 | // (Exhaustive map of [extension attribute constant] -> [extension attribute ident string]) 30 | export const extensionAttributeIdents = { 31 | TYPE_NAME, 32 | TYPE_NAMESPACE, 33 | TYPE_DOC, 34 | INTERFACE_NAME, 35 | INTERFACE_IMPLEMENTATIONS, 36 | INTERFACE_DOC, 37 | REF_TARGET, 38 | ENUM_VALUES, 39 | REVERSE_REF_FIELD, 40 | }; 41 | 42 | // (Exhaustive map of [extension attribute constant] -> [extension attribute ident keyword]) 43 | export const extensionAttributeIdentKeywords = { 44 | [TYPE_NAME]: edn.kw(TYPE_NAME), 45 | [TYPE_NAMESPACE]: edn.kw(TYPE_NAMESPACE), 46 | [TYPE_DOC]: edn.kw(TYPE_DOC), 47 | [INTERFACE_NAME]: edn.kw(INTERFACE_NAME), 48 | [INTERFACE_IMPLEMENTATIONS]: edn.kw(INTERFACE_IMPLEMENTATIONS), 49 | [INTERFACE_DOC]: edn.kw(INTERFACE_DOC), 50 | [REF_TARGET]: edn.kw(REF_TARGET), 51 | [ENUM_VALUES]: edn.kw(ENUM_VALUES), 52 | [REVERSE_REF_FIELD]: edn.kw(REVERSE_REF_FIELD), 53 | }; 54 | 55 | // (Exhaustive map of [extension attribute constant] -> [datomic value type keyword]) 56 | export const extensionAttributeValueTypeKeywords = { 57 | [TYPE_NAME]: datomicValueTypeKeywords[STRING], 58 | [TYPE_NAMESPACE]: datomicValueTypeKeywords[KEYWORD], 59 | [TYPE_DOC]: datomicValueTypeKeywords[STRING], 60 | [INTERFACE_NAME]: datomicValueTypeKeywords[STRING], 61 | [INTERFACE_IMPLEMENTATIONS]: datomicValueTypeKeywords[REF], 62 | [INTERFACE_DOC]: datomicValueTypeKeywords[STRING], 63 | [REF_TARGET]: datomicValueTypeKeywords[REF], 64 | [ENUM_VALUES]: datomicValueTypeKeywords[REF], 65 | [REVERSE_REF_FIELD]: datomicValueTypeKeywords[KEYWORD], 66 | }; 67 | 68 | // (Exhaustive map of [extension attribute constant] -> [datomic cardinality keyword]) 69 | export const extensionAttributeCardinalityKeywords = { 70 | [TYPE_NAME]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 71 | [TYPE_NAMESPACE]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 72 | [TYPE_DOC]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 73 | [INTERFACE_NAME]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 74 | [INTERFACE_IMPLEMENTATIONS]: datomicCardinalityIdentKeywords[CARDINALITY_MANY], 75 | [INTERFACE_DOC]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 76 | [REF_TARGET]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 77 | [ENUM_VALUES]: datomicCardinalityIdentKeywords[CARDINALITY_MANY], 78 | [REVERSE_REF_FIELD]: datomicCardinalityIdentKeywords[CARDINALITY_ONE], 79 | }; 80 | 81 | // (Exhaustive map of [extension attribute constant] -> [datomic unique keyword OR null]) 82 | export const extensionAttributeUniqueKeywords = { 83 | [TYPE_NAME]: datomicUniqueIdentKeywords[UNIQUE_VALUE], 84 | [TYPE_NAMESPACE]: datomicUniqueIdentKeywords[UNIQUE_VALUE], 85 | [TYPE_DOC]: null, 86 | [INTERFACE_NAME]: datomicUniqueIdentKeywords[UNIQUE_VALUE], 87 | [INTERFACE_IMPLEMENTATIONS]: null, 88 | [INTERFACE_DOC]: null, 89 | [REF_TARGET]: null, 90 | [ENUM_VALUES]: null, 91 | [REVERSE_REF_FIELD]: datomicUniqueIdentKeywords[UNIQUE_VALUE], 92 | }; 93 | 94 | // (Exhaustive map of [extension attribute constant] -> [extension attribute doc string]) 95 | export const extensionAttributeDocStrings = { 96 | [TYPE_NAME]: 'A type\'s name in the GraphQL type system', 97 | [TYPE_NAMESPACE]: 'The namespace of a type\'s attribute idents in the db', 98 | [TYPE_DOC]: 'The description of a type', 99 | [INTERFACE_NAME]: 'An interface\'s name in the GraphQL type system', 100 | [INTERFACE_IMPLEMENTATIONS]: 'The types that implement an interface', 101 | [INTERFACE_DOC]: 'The description of an interface', 102 | [REF_TARGET]: 'The type or interface that a reference attribute targets', 103 | [ENUM_VALUES]: 'The possible values of an enumeration-type reference attribute', 104 | [REVERSE_REF_FIELD]: 'The reverse reference field on the type or interface a reference attribute targets', 105 | }; 106 | -------------------------------------------------------------------------------- /src/constants/queryPredicateOperators.js: -------------------------------------------------------------------------------- 1 | // (Predicate operator input field key constants) 2 | // (Comparison-type predicate operators) 3 | export const IS = 'is'; 4 | export const IS_NOT = 'isNot'; 5 | export const GREATER_THAN = 'greaterThan'; 6 | export const GREATER_THAN_OR_EQUAL_TO = 'greaterThanOrEqualTo'; 7 | export const LESS_THAN = 'lessThan'; 8 | export const LESS_THAN_OR_EQUAL_TO = 'lessThanOrEqualTo'; 9 | // (Existential-type predicate operators) 10 | export const IS_MISSING = 'isMissing'; 11 | // (Predicate composition-type prediate operators) 12 | export const NOT = 'not'; 13 | export const OR = 'or'; 14 | export const AND = 'and'; 15 | 16 | // (Exhaustive map of [predicate operator constant name] -> [predicate operator input field key]) 17 | export const predicateOperators = { 18 | IS, 19 | IS_NOT, 20 | GREATER_THAN, 21 | GREATER_THAN_OR_EQUAL_TO, 22 | LESS_THAN, 23 | LESS_THAN_OR_EQUAL_TO, 24 | IS_MISSING, 25 | NOT, 26 | OR, 27 | AND, 28 | }; 29 | 30 | // (Exhaustive map of [predicate operator constant name] -> [datomic query operator]) 31 | export const datomicQueryOperators = { 32 | [IS]: '=', 33 | [IS_NOT]: '!=', 34 | [GREATER_THAN]: '>', 35 | [GREATER_THAN_OR_EQUAL_TO]: '>=', 36 | [LESS_THAN]: '<', 37 | [LESS_THAN_OR_EQUAL_TO]: '<=', 38 | [IS_MISSING]: 'missing?', 39 | [NOT]: 'not', 40 | [OR]: 'or', 41 | [AND]: 'and', 42 | }; 43 | -------------------------------------------------------------------------------- /src/consumer/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import edn from 'jsedn'; 3 | 4 | // (Value constants) 5 | const EDN_MIME_TYPE = 'application/edn'; 6 | 7 | export default function consumer(apiUrl, dbAlias) { 8 | return { 9 | query(queryEdn) { 10 | const queryUrl = `${apiUrl}/api/query`; 11 | 12 | // Build params 13 | const argsEdn = new edn.Vector([ 14 | new edn.Map([edn.kw(':db/alias'), dbAlias]), 15 | ]); 16 | const queryString = edn.encode(queryEdn); 17 | const argsString = edn.encode(argsEdn); 18 | 19 | return axios({ 20 | method: 'get', 21 | url: queryUrl, 22 | params: { 23 | q: queryString, 24 | args: argsString, 25 | }, 26 | headers: { 27 | 'Accept': EDN_MIME_TYPE, 28 | }, 29 | }) 30 | .then(response => parseEdnResponse(response)); 31 | }, 32 | 33 | transact(txEdn) { 34 | const transactionUrl = `${apiUrl}/data/${dbAlias}/`; 35 | 36 | // Build request body 37 | const ednBody = new edn.Map([edn.kw(':tx-data'), txEdn]); 38 | const stringBody = edn.encode(ednBody); 39 | 40 | return axios({ 41 | method: 'post', 42 | url: transactionUrl, 43 | data: stringBody, 44 | headers: { 45 | 'Accept': EDN_MIME_TYPE, 46 | 'Content-Type': EDN_MIME_TYPE, 47 | }, 48 | }) 49 | .then(response => parseEdnResponse(response)); 50 | }, 51 | 52 | getEntity(e, { basisT = '-', asOf, since } = {}) { 53 | const getEntityUrl = `${apiUrl}/data/${dbAlias}/${basisT}/entity`; 54 | 55 | return axios({ 56 | method: 'get', 57 | url: getEntityUrl, 58 | params: { 59 | e, 60 | asOf, 61 | since, 62 | }, 63 | headers: { 64 | 'Accept': EDN_MIME_TYPE, 65 | }, 66 | }) 67 | .then(response => parseEdnResponse(response)); 68 | }, 69 | }; 70 | } 71 | 72 | function parseEdnResponse(response, responseDefault = []) { 73 | return Promise.resolve(response) 74 | .then(res => res.data) 75 | .then(responseBodyString => edn.parse(responseBodyString)) 76 | .then(responseBodyEdn => edn.toJS(responseBodyEdn)) 77 | .then(parsedResponse => parsedResponse || responseDefault); 78 | } 79 | -------------------------------------------------------------------------------- /src/graphQLSchema/getNodeDefinitions/index.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import { fromGlobalId, nodeDefinitions } from 'graphql-relay'; 3 | import { types } from '../utils/getGraphQLTypeForSchemaType'; 4 | import getObjectFromEntity from '../../utils/getObjectFromEntity'; 5 | import { memoize } from 'underscore'; 6 | 7 | // Export memoized function to avoid creating multiple types with name "Node" 8 | // on schema... 9 | // NOTE: This should *never* return more than one unique value during runtime... 10 | // maybe the memoize is inappropriate... 11 | export default memoize(function getNodeDefinitions(apiUrl, dbAlias) { 12 | const db = consumer(apiUrl, dbAlias); 13 | 14 | return nodeDefinitions( 15 | (globalId) => { 16 | const { type, id } = fromGlobalId(globalId); 17 | 18 | return db.getEntity(id) 19 | .then(entity => getObjectFromEntity(entity, type)); 20 | }, 21 | (object) => { 22 | const type = types[object.__typeName]; 23 | 24 | return type; 25 | } 26 | ); 27 | }, function getHashKey(...args) { 28 | // Use both apiUrl & dbAlias in hash key... 29 | return args.join('__'); 30 | }); 31 | -------------------------------------------------------------------------------- /src/graphQLSchema/getRootMutationType/index.js: -------------------------------------------------------------------------------- 1 | // import consumer from '../../consumer'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import getSchemaTypesAndInterfaces from '../../utils/getSchemaTypesAndInterfaces'; 4 | import { reduce } from 'underscore'; 5 | 6 | export default function getRootMutationType(apiUrl, dbAlias) { 7 | return generateRootMutationFields(apiUrl, dbAlias) 8 | .then(rootMutationFields => { 9 | return new GraphQLObjectType({ 10 | name: 'Mutation', 11 | description: 'Root mutation type', 12 | fields: () => rootMutationFields, 13 | }); 14 | }); 15 | } 16 | 17 | function generateRootMutationFields(apiUrl, dbAlias) { 18 | // const db = consumer(apiUrl, dbAlias); 19 | 20 | return getSchemaTypesAndInterfaces(apiUrl, dbAlias) 21 | .then(({ schemaTypes }) => { 22 | return reduce(schemaTypes, (aggregateFields, schemaType, schemaTypeName) => { 23 | // TODO: Get real field names... 24 | // Field names 25 | const createFieldName = `create${schemaTypeName}`; 26 | const patchFieldName = `patch${schemaTypeName}s`; 27 | const replaceFieldName = `replace${schemaTypeName}`; 28 | const deleteFieldName = `delete${schemaTypeName}s`; 29 | 30 | // TODO: Create real mutation fields... 31 | return { 32 | ...aggregateFields, 33 | [createFieldName]: true, 34 | [patchFieldName]: true, 35 | [replaceFieldName]: true, 36 | [deleteFieldName]: true, 37 | }; 38 | }, {}); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/graphQLSchema/getRootMutationType/utils/getNewInputArgsForSchemaType.js: -------------------------------------------------------------------------------- 1 | export default function getNewInputArgsForSchemaType(schemaType, schemaTypeName) { 2 | console.log('getNewInputArgsForSchemaType... schemaTypeName:', schemaTypeName, 'schemaType:', schemaType); 3 | } 4 | -------------------------------------------------------------------------------- /src/graphQLSchema/getRootMutationType/utils/getPatchInputArgsForSchemaType.js: -------------------------------------------------------------------------------- 1 | export default function getPatchInputArgsForSchemaType(schemaType, schemaTypeName) { 2 | console.log('getPatchInputArgsForSchemaType... schemaTypeName:', schemaTypeName, 'schemaType:', schemaType); 3 | } 4 | -------------------------------------------------------------------------------- /src/graphQLSchema/getRootQueryType/index.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import { connectionArgs } from 'graphql-relay'; 4 | import { getInstanceQueryFieldNameFromTypeName, getConnectionQueryFieldNameFromTypeName } from '../../utils/inflect'; 5 | import getNodeDefinitions from '../getNodeDefinitions'; 6 | import getGraphQLTypeForSchemaType from '../utils/getGraphQLTypeForSchemaType'; 7 | import getGraphQLConnectionTypeForSchemaType from '../utils/getGraphQLConnectionTypeForSchemaType'; 8 | import getSchemaTypesAndInterfaces from '../../utils/getSchemaTypesAndInterfaces'; 9 | import getQueryInputArgsForSchemaType from '../utils/getQueryInputArgsForSchemaType'; 10 | import resolveInstanceFieldQuery from '../utils/resolveInstanceFieldQuery'; 11 | import resolveConnectionFieldQuery from '../utils/resolveConnectionFieldQuery'; 12 | import { reduce } from 'underscore'; 13 | 14 | export default function getRootQueryType(apiUrl, dbAlias) { 15 | const { nodeField } = getNodeDefinitions(apiUrl, dbAlias); 16 | 17 | return generateRootQueryFields(apiUrl, dbAlias) 18 | .then(rootQueryFields => { 19 | return new GraphQLObjectType({ 20 | name: 'Query', 21 | description: 'Root query type', 22 | fields: () => ({ 23 | // Relay root query node field 24 | node: nodeField, 25 | 26 | ...rootQueryFields, 27 | }), 28 | }); 29 | }); 30 | } 31 | 32 | function generateRootQueryFields(apiUrl, dbAlias) { 33 | const db = consumer(apiUrl, dbAlias); 34 | 35 | return getSchemaTypesAndInterfaces(apiUrl, dbAlias) 36 | .then(({ schemaTypes, schemaInterfaces }) => { 37 | return reduce(schemaTypes, (aggregateFields, schemaType, schemaTypeName) => { 38 | // Field names 39 | const instanceQueryFieldName = getInstanceQueryFieldNameFromTypeName(schemaTypeName); 40 | const connectionQueryFieldName = getConnectionQueryFieldNameFromTypeName(schemaTypeName); 41 | 42 | // Field output types 43 | const instanceGraphQLType = getGraphQLTypeForSchemaType({ schemaType, schemaTypeName }, apiUrl, dbAlias); 44 | const connectionGraphQLType = getGraphQLConnectionTypeForSchemaType({ schemaType, schemaTypeName }, apiUrl, dbAlias); 45 | 46 | return { 47 | ...aggregateFields, 48 | [instanceQueryFieldName]: { 49 | type: instanceGraphQLType, 50 | args: getQueryInputArgsForSchemaType(schemaType, schemaTypeName), 51 | resolve: (query, args) => resolveInstanceFieldQuery({ 52 | parent: query, 53 | fieldName: instanceQueryFieldName, 54 | args, 55 | schemaTypeName, 56 | db, 57 | }), 58 | }, 59 | [connectionQueryFieldName]: { 60 | type: connectionGraphQLType, 61 | args: { ...getQueryInputArgsForSchemaType(schemaType, schemaTypeName), ...connectionArgs }, 62 | resolve: (query, args) => resolveConnectionFieldQuery({ 63 | parent: query, 64 | fieldName: connectionQueryFieldName, 65 | args, 66 | schemaTypeName, 67 | db, 68 | }), 69 | }, 70 | }; 71 | }, {}); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/graphQLSchema/index.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import getRootQueryType from './getRootQueryType'; 3 | 4 | export default function getGraphQLSchema(apiUrl, dbAlias) { 5 | return Promise.all([ 6 | getRootQueryType(apiUrl, dbAlias), 7 | ]) 8 | .then(([rootQueryType]) => { 9 | return new GraphQLSchema({ 10 | query: rootQueryType, 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getGraphQLConnectionTypeForSchemaType.js: -------------------------------------------------------------------------------- 1 | import { connectionDefinitions } from 'graphql-relay'; 2 | import getGraphQLTypeForSchemaType from './getGraphQLTypeForSchemaType'; 3 | 4 | export const connectionTypes = {}; 5 | 6 | export default function getGraphQLConnectionTypeForSchemaType({ schemaType, schemaTypeName }, apiUrl, dbAlias) { 7 | connectionTypes[schemaTypeName] = connectionTypes[schemaTypeName] || connectionDefinitions({ 8 | name: schemaTypeName, 9 | nodeType: getGraphQLTypeForSchemaType({ schemaType, schemaTypeName }, apiUrl, dbAlias), 10 | }).connectionType; 11 | 12 | return connectionTypes[schemaTypeName]; 13 | } 14 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getGraphQLEnumTypeForAttribute.js: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType } from 'graphql'; 2 | 3 | const enumTypes = {}; 4 | 5 | export default function getGraphQLEnumTypeForAttribute(attribute, attributeName) { 6 | enumTypes[attributeName] = enumTypes[attributeName] || new GraphQLEnumType({ 7 | name: attributeName, 8 | description: attribute.doc, 9 | values: attribute.enumValues.reduce((aggregateValues, enumValue) => { 10 | return { 11 | ...aggregateValues, 12 | [enumValue]: { value: enumValue }, 13 | }; 14 | }, {}), 15 | }); 16 | 17 | return enumTypes[attributeName]; 18 | } 19 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getGraphQLTypeForAttribute.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLString } from 'graphql'; 2 | import { types } from './getGraphQLTypeForSchemaType'; 3 | import { connectionTypes } from './getGraphQLConnectionTypeForSchemaType'; 4 | import getGraphQLEnumTypeForAttribute from './getGraphQLEnumTypeForAttribute'; 5 | 6 | // (Value constants) 7 | const CARDINALITY_ONE = 'one'; 8 | 9 | export default function getGraphQLTypeForAttribute(attribute, attributeName) { 10 | const typeIsEnumType = attribute.valueType === 'ref' && !!attribute.enumValues; 11 | const typeIsReferenceType = !typeIsEnumType && attribute.valueType === 'ref'; 12 | const typeIsArbitraryReferenceType = typeIsReferenceType && !attribute.refTarget; 13 | const typeIsScalarType = !typeIsEnumType && !typeIsReferenceType; 14 | const typeIsScalarListType = typeIsScalarType && attribute.cardinality !== CARDINALITY_ONE; 15 | let scalarType; 16 | 17 | switch (attribute.valueType) { 18 | case 'boolean': 19 | scalarType = GraphQLBoolean; 20 | break; 21 | case 'float': 22 | case 'double': 23 | case 'bigdec': 24 | scalarType = GraphQLFloat; 25 | break; 26 | case 'long': 27 | case 'bigint': 28 | scalarType = GraphQLInt; 29 | break; 30 | case 'keyword': 31 | case 'instant': 32 | case 'string': 33 | case 'uuid': 34 | case 'uri': 35 | case 'bytes': 36 | default: 37 | scalarType = GraphQLString; 38 | } 39 | 40 | if (typeIsEnumType) { 41 | return getGraphQLEnumTypeForAttribute(attribute, attributeName); 42 | } 43 | 44 | if (typeIsReferenceType) { 45 | // Resolve to target type or connection depending on attribute cardinality 46 | return attribute.cardinality === CARDINALITY_ONE 47 | ? types[attribute.refTarget] 48 | : connectionTypes[attribute.refTarget]; 49 | } else if (typeIsArbitraryReferenceType) { 50 | // FIXME: Resolve to node interface for arbitrary references 51 | return GraphQLString; 52 | } 53 | 54 | if (typeIsScalarListType) { 55 | return new GraphQLList(scalarType); 56 | } 57 | 58 | return scalarType; 59 | } 60 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getGraphQLTypeForReverseReferenceAttribute.js: -------------------------------------------------------------------------------- 1 | import { connectionTypes } from './getGraphQLConnectionTypeForSchemaType'; 2 | 3 | export default function getGraphQLTypeForReverseReferenceAttribute(attributeReverseRefTypeName) { 4 | return connectionTypes[attributeReverseRefTypeName]; 5 | } 6 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getGraphQLTypeForSchemaType.js: -------------------------------------------------------------------------------- 1 | import consumer from '../../consumer'; 2 | import { GraphQLObjectType } from 'graphql'; 3 | import { connectionArgs, globalIdField } from 'graphql-relay'; 4 | import getGraphQLTypeForAttribute from './getGraphQLTypeForAttribute'; 5 | import getQueryInputArgsForSchemaType from './getQueryInputArgsForSchemaType'; 6 | import getNodeDefinitions from '../getNodeDefinitions'; 7 | import resolveInstanceFieldQuery from './resolveInstanceFieldQuery'; 8 | import resolveConnectionFieldQuery from './resolveConnectionFieldQuery'; 9 | import { reduce } from 'underscore'; 10 | 11 | export const types = {}; 12 | 13 | export default function getGraphQLTypeForSchemaType({ schemaType, schemaTypeName }, apiUrl, dbAlias) { 14 | const db = consumer(apiUrl, dbAlias); 15 | const { nodeInterface } = getNodeDefinitions(apiUrl, dbAlias); 16 | 17 | // Initial value for fields reduction should have globalIdField (for node interface implementation) 18 | const initialFields = { id: globalIdField(schemaTypeName) }; 19 | 20 | types[schemaTypeName] = types[schemaTypeName] || new GraphQLObjectType({ 21 | name: schemaTypeName, 22 | description: schemaType.doc, 23 | fields: () => reduce(schemaType.attributes, (aggregateFields, attribute, attributeName) => { 24 | const attributeHasRefTarget = !!attribute.refTarget; 25 | const attributeFieldIsConnection = attribute.cardinality === 'many'; 26 | const attributeQueryInputArgs = attributeHasRefTarget 27 | ? getQueryInputArgsForSchemaType(types[attribute.refTarget], attribute.refTarget) 28 | : {}; 29 | const attributeFieldArgs = attributeHasRefTarget && attributeFieldIsConnection 30 | ? { ...attributeQueryInputArgs, ...connectionArgs } 31 | : null; 32 | let resolveAttribute; 33 | 34 | if (attributeHasRefTarget && attributeFieldIsConnection) { 35 | resolveAttribute = (parent, args) => resolveConnectionFieldQuery({ 36 | parent, 37 | attributeIdent: attribute.ident, 38 | isReverseRef: attribute.isReverseRef, 39 | args, 40 | schemaTypeName: attribute.refTarget, 41 | db, 42 | }); 43 | } else if (attributeHasRefTarget) { 44 | resolveAttribute = (parent, args) => resolveInstanceFieldQuery({ 45 | parent, 46 | attributeIdent: attribute.ident, 47 | isReverseRef: attribute.isReverseRef, 48 | args, 49 | schemaTypeName: attribute.refTarget, 50 | db, 51 | }); 52 | } else { 53 | // Resolve scalar value 54 | resolveAttribute = (parent) => parent[attributeName]; 55 | } 56 | 57 | return { 58 | ...aggregateFields, 59 | [attributeName]: { 60 | type: getGraphQLTypeForAttribute(attribute, attributeName), 61 | args: attributeFieldArgs, 62 | description: attribute.doc, 63 | resolve: resolveAttribute, 64 | }, 65 | }; 66 | }, initialFields), 67 | interfaces: [nodeInterface], 68 | }); 69 | 70 | return types[schemaTypeName]; 71 | } 72 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getQueryEdn.js: -------------------------------------------------------------------------------- 1 | import edn from 'jsedn'; 2 | import { connectionArgs } from 'graphql-relay'; 3 | import { getAttributeIdentFromAttributeNameAndTypeName } from '../../utils/inflect'; 4 | import { isArray, isObject, keys, map, omit, reduce } from 'underscore'; 5 | 6 | export const operators = { 7 | is: 'is', 8 | isNot: 'isNot', 9 | greaterThan: 'greaterThan', 10 | greaterThanOrEqualTo: 'greaterThanOrEqualTo', 11 | lessThan: 'lessThan', 12 | lessThanOrEqualTo: 'lessThanOrEqualTo', 13 | 14 | isMissing: 'isMissing', 15 | 16 | not: 'not', 17 | or: 'or', 18 | and: 'and', 19 | }; 20 | 21 | // Query input field -> expression operator 22 | export const operatorMap = { 23 | is: '=', 24 | isNot: '!=', 25 | greaterThan: '>', 26 | greaterThanOrEqualTo: '>=', 27 | lessThan: '<', 28 | lessThanOrEqualTo: '<=', 29 | 30 | isMissing: 'missing?', 31 | 32 | not: 'not', 33 | or: 'or', 34 | and: 'and', 35 | }; 36 | 37 | // Array of fields to exclude from query edn 38 | export const excludeFields = keys(connectionArgs); 39 | 40 | export default function getQueryEdn({ args, schemaTypeName, referenceFieldClause }) { 41 | const filteredArgs = omit(args, excludeFields); 42 | const predicateVectorArray = map(filteredArgs, (argValue, argKey) => { 43 | const argPredicateExpression = getArgPredicateExpressionEdn({ key: argKey, value: argValue }, schemaTypeName); 44 | console.log('edn.encode(argPredicateExpression):', edn.encode(argPredicateExpression)); 45 | return argPredicateExpression; 46 | }); 47 | const queryClauseArray = referenceFieldClause 48 | ? [referenceFieldClause, ...predicateVectorArray] 49 | : predicateVectorArray; 50 | 51 | const queryEdn = new edn.Vector([ 52 | edn.kw(':find'), 53 | new edn.Vector([ 54 | new edn.List([ 55 | edn.sym('pull'), edn.sym('?e'), new edn.Vector(['*']), 56 | ]), 57 | edn.sym('...'), 58 | ]), 59 | edn.kw(':where'), ...queryClauseArray, 60 | ]); 61 | console.log('edn.encode(queryEdn):', edn.encode(queryEdn)); 62 | 63 | return queryEdn; 64 | } 65 | 66 | function getArgPredicateExpressionEdn({ key, value }, schemaTypeName) { 67 | const mappedOperator = key ? operatorMap[key] : undefined; 68 | const attributeIdent = getAttributeIdentFromAttributeNameAndTypeName(key, schemaTypeName); 69 | console.log('attributeIdent:', attributeIdent); 70 | console.log('key:', key); 71 | console.log('mappedOperator:', mappedOperator); 72 | console.log('value:', value); 73 | console.log('schemaTypeName:', schemaTypeName); 74 | switch (key) { 75 | case operators.isNot: 76 | case operators.greaterThan: 77 | case operators.greaterThanOrEqualTo: 78 | case operators.lessThan: 79 | case operators.lessThanOrEqualTo: 80 | console.log('return null...'); 81 | return null; 82 | case operators.isMissing: 83 | case operators.or: 84 | case operators.and: 85 | console.log('prediate composition... value:', value); 86 | const operandsArray = isArray(value) ? value : [value]; 87 | return new edn.List([ 88 | edn.sym(mappedOperator), 89 | ...operandsArray.map(operand => getArgPredicateExpressionEdn({ value: operand }, schemaTypeName)), 90 | ]); 91 | case operators.is: 92 | return [value]; 93 | default: 94 | return reduce(value, (memo, attributePredicate, attributeName) => { 95 | if (isObject(attributePredicate)) { 96 | return getArgPredicateExpressionEdn({ key: attributeName, value: attributePredicate }, schemaTypeName); 97 | } 98 | 99 | return new edn.Vector([edn.sym('?e'), 100 | attributeIdent, 101 | ...getArgPredicateExpressionEdn({ key: attributeName, value: attributePredicate }, schemaTypeName), 102 | ]); 103 | }, {}); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getQueryInputArgsForSchemaType.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBoolean, GraphQLInputObjectType, GraphQLList } from 'graphql'; 2 | import getGraphQLTypeForAttribute from './getGraphQLTypeForAttribute'; 3 | import { schemaTypes } from './getGraphQLTypeForSchemaType'; 4 | import { getInstanceQueryFieldNameFromTypeName, getConnectionQueryFieldNameFromTypeName } from '../../utils/inflect'; 5 | import { reduce } from 'underscore'; 6 | 7 | export const schemaTypePredicateTypes = {}; 8 | export const attributePredicateTypes = {}; 9 | 10 | export default function getQueryInputArgsForSchemaType(schemaType, schemaTypeName) { 11 | const schemaTypePredicateFields = reduce(schemaType.attributes, (aggregateSchemaTypePredicateFields, attribute, attributeName) => { 12 | const attributePredicateType = getAttributePredicateType(attribute, attributeName, schemaTypeName); 13 | 14 | // Bail if attributePredicateType is falsy 15 | if (!attributePredicateType) { 16 | return aggregateSchemaTypePredicateFields; 17 | } 18 | 19 | return { 20 | ...aggregateSchemaTypePredicateFields, 21 | [attributeName]: { type: attributePredicateType }, 22 | }; 23 | }, {}); 24 | const schemaTypePredicateTypeName = `${schemaTypeName}Predicate`; 25 | schemaTypePredicateTypes[schemaTypePredicateTypeName] = schemaTypePredicateTypes[schemaTypePredicateTypeName] || new GraphQLInputObjectType({ 26 | name: schemaTypePredicateTypeName, 27 | fields: schemaTypePredicateFields, 28 | }); 29 | const schemaTypePredicateInputObject = schemaTypePredicateTypes[schemaTypePredicateTypeName]; 30 | 31 | // Get predicate fields for reverse reference attributes 32 | // console.log(`schemaTypeName "${schemaTypeName}"... schemaType:`, schemaType); 33 | const reverseReferencePredicateFields = reduce(schemaType.reverseReferenceFields, (aggregateReverseReferencePredicateFields, reverseReferenceField) => { 34 | const reverseReferencePredicateFieldName = getConnectionQueryFieldNameFromTypeName(reverseReferenceField.type); 35 | const reverseReferencePredicateFieldType = schemaTypePredicateTypes[`${reverseReferenceField.type}Predicate`]; 36 | // console.log('reverseReferencePredicateFieldType:', reverseReferencePredicateFieldType); 37 | 38 | // TODO: Resolve via a function...? 39 | // NOTE: This workaround causes some reverse reference predicate field types to be omitted :/ 40 | if (!reverseReferencePredicateFieldType) { 41 | return aggregateReverseReferencePredicateFields; 42 | } 43 | 44 | return { 45 | ...aggregateReverseReferencePredicateFields, 46 | [reverseReferencePredicateFieldName]: { type: reverseReferencePredicateFieldType }, 47 | }; 48 | }, {}); 49 | // console.log('schemaTypePredicateFields:', schemaTypePredicateFields); 50 | // console.log('reverseReferencePredicateFields:', reverseReferencePredicateFields); 51 | 52 | return { 53 | ...schemaTypePredicateFields, 54 | 55 | ...reverseReferencePredicateFields, 56 | 57 | not: { type: schemaTypePredicateInputObject }, 58 | or: { type: new GraphQLList(schemaTypePredicateInputObject) }, 59 | and: { type: new GraphQLList(schemaTypePredicateInputObject) }, 60 | }; 61 | } 62 | 63 | function getAttributePredicateType(attribute, attributeName, schemaTypeName) { 64 | const attributeGraphQLType = getGraphQLTypeForAttribute(attribute, attributeName); 65 | 66 | // Bail / recurse if attribute GraphQL type isn't a valid input type 67 | if (attribute.valueType === 'ref' && attribute.refTarget) { 68 | // FIXME: Returns undefined if predicate for refTarget isn't in schemaTypePredicateTypes yet... 69 | return schemaTypePredicateTypes[`${attribute.refTarget}Predicate`]; 70 | } else if (attribute.valueType === 'ref' && !attribute.enumValues) { 71 | return null; 72 | } 73 | 74 | const schemaTypePredicateName = getInstanceQueryFieldNameFromTypeName(`${schemaTypeName}_${attributeName}`); 75 | const attributePredicateTypeName = `${schemaTypePredicateName}Predicate`; 76 | attributePredicateTypes[attributePredicateTypeName] = attributePredicateTypes[attributePredicateTypeName] || new GraphQLInputObjectType({ 77 | name: attributePredicateTypeName, 78 | fields: () => ({ 79 | is: { type: attributeGraphQLType }, 80 | isNot: { type: attributeGraphQLType }, 81 | greaterThan: { type: attributeGraphQLType }, 82 | greaterThanOrEqualTo: { type: attributeGraphQLType }, 83 | lessThan: { type: attributeGraphQLType }, 84 | lessThanOrEqualTo: { type: attributeGraphQLType }, 85 | 86 | isMissing: { type: GraphQLBoolean }, 87 | 88 | not: { type: attributePredicateTypes[attributePredicateTypeName] }, 89 | or: { type: new GraphQLList(attributePredicateTypes[attributePredicateTypeName]) }, 90 | and: { type: new GraphQLList(attributePredicateTypes[attributePredicateTypeName]) }, 91 | }), 92 | }); 93 | 94 | return attributePredicateTypes[attributePredicateTypeName]; 95 | } 96 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/getReferenceFieldClause.js: -------------------------------------------------------------------------------- 1 | import edn from 'jsedn'; 2 | 3 | export default function getReferenceFieldClause({ parentId, attributeIdent, isReverseRef }) { 4 | if (!isReverseRef) { 5 | return new edn.Vector([ 6 | parentId, 7 | attributeIdent, 8 | edn.sym('?e'), 9 | ]); 10 | } 11 | 12 | return new edn.Vector([ 13 | edn.sym('?e'), 14 | attributeIdent, 15 | parentId, 16 | ]); 17 | } 18 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/resolveConnectionFieldQuery.js: -------------------------------------------------------------------------------- 1 | import { connectionFromArray } from 'graphql-relay'; 2 | import getQueryEdn from './getQueryEdn'; 3 | import getReferenceFieldClause from './getReferenceFieldClause'; 4 | import getObjectFromEntity from '../../utils/getObjectFromEntity'; 5 | 6 | export default function resolveConnectionFieldQuery({ parent, attributeIdent, isReverseRef, args, schemaTypeName, db }) { 7 | let referenceFieldClause; 8 | if (parent.__typeName) { 9 | referenceFieldClause = getReferenceFieldClause({ parentId: parent.id, attributeIdent, isReverseRef }); 10 | } 11 | const queryEdn = getQueryEdn({ args, schemaTypeName, referenceFieldClause }); 12 | 13 | return db.query(queryEdn) 14 | .then(results => results.map(entity => getObjectFromEntity(entity, schemaTypeName))) 15 | .then(results => connectionFromArray(results, args)) 16 | .catch(error => console.error(error.stack || error)); 17 | } 18 | -------------------------------------------------------------------------------- /src/graphQLSchema/utils/resolveInstanceFieldQuery.js: -------------------------------------------------------------------------------- 1 | import getQueryEdn from './getQueryEdn'; 2 | import getReferenceFieldClause from './getReferenceFieldClause'; 3 | import getObjectFromEntity from '../../utils/getObjectFromEntity'; 4 | 5 | export default function resolveInstanceFieldQuery({ parent, attributeIdent, isReverseRef, args, schemaTypeName, db }) { 6 | let referenceFieldClause; 7 | if (parent.__typeName) { 8 | referenceFieldClause = getReferenceFieldClause({ parentId: parent.id, attributeIdent, isReverseRef }); 9 | } 10 | // TODO: Add limit 1... 11 | // console.log('resolveInstanceFieldQuery... TODO: Add limit 1...'); 12 | const queryEdn = getQueryEdn({ args, schemaTypeName, referenceFieldClause }); 13 | 14 | return db.query(queryEdn) 15 | .then(results => results.map(entity => getObjectFromEntity(entity, schemaTypeName))) 16 | .then(results => results[0] || null) 17 | .catch(error => console.error(error.stack || error)); 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import bootstrap from './bootstrap'; 2 | import getGraphQLSchema from './graphQLSchema'; 3 | 4 | export default (apiUrl, dbAlias) => { 5 | return bootstrap(apiUrl, dbAlias) 6 | .then(() => getGraphQLSchema(apiUrl, dbAlias)); 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/getObjectFromEntity.js: -------------------------------------------------------------------------------- 1 | import { getAttributeNameFromAttributeIdent } from './inflect'; 2 | import { reduce } from 'underscore'; 3 | 4 | export default function getObjectFromEntity(entity, schemaTypeName) { 5 | const initialObject = { __typeName: schemaTypeName }; 6 | const object = reduce(entity, (aggregateObject, attributeValue, attributeIdent) => { 7 | const attributeName = getAttributeNameFromAttributeIdent(attributeIdent); 8 | 9 | return { 10 | ...aggregateObject, 11 | [attributeName]: attributeValue, 12 | }; 13 | }, initialObject); 14 | 15 | return object; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/getSchemaTypesAndInterfaces.js: -------------------------------------------------------------------------------- 1 | import querySchemaImpliedTypes from '../utils/querySchemaImpliedTypes'; 2 | import queryInstalledTypes from '../utils/queryInstalledTypes'; 3 | import queryInstalledInterfaces from '../utils/queryInstalledInterfaces'; 4 | import { mapObject } from 'underscore'; 5 | 6 | export default (apiUrl, dbAlias) => { 7 | return Promise.all([ 8 | querySchemaImpliedTypes(apiUrl, dbAlias), 9 | queryInstalledTypes(apiUrl, dbAlias), 10 | queryInstalledInterfaces(apiUrl, dbAlias), 11 | ]) 12 | .then(([schemaImpliedTypes, installedTypes, installedInterfaces]) => { 13 | return { 14 | schemaInterfaces: installedInterfaces, 15 | schemaTypes: mapObject(schemaImpliedTypes, (type, typeName) => { 16 | return { 17 | attributes: type, 18 | ...installedTypes[typeName], 19 | }; 20 | }), 21 | }; 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/inflect.js: -------------------------------------------------------------------------------- 1 | import Inflect from 'i'; 2 | import edn from 'jsedn'; 3 | 4 | // Initialize inflector util 5 | const inflect = new Inflect(); 6 | 7 | // - From Attribute Ident - 8 | export function getTypeNameFromAttributeIdent(attributeIdent) { 9 | const attributeIdentEdn = edn.kw(attributeIdent); 10 | const attributeIdentNamespace = attributeIdentEdn.ns; 11 | const attributeTypeName = inflect.camelize(attributeIdentNamespace.replace(':', '')); 12 | 13 | return attributeTypeName; 14 | } 15 | 16 | export function getAttributeNameFromAttributeIdent(attributeIdent) { 17 | const attributeIdentEdn = edn.kw(attributeIdent); 18 | const attributeName = attributeIdentEdn.name; 19 | 20 | return attributeName; 21 | } 22 | 23 | // - From Type Name - 24 | export function getTypeNamespaceFromTypeName(typeName) { 25 | const typeNamespace = `:${inflect.camelize(typeName, false)}`; 26 | 27 | return typeNamespace; 28 | } 29 | 30 | export function getInstanceQueryFieldNameFromTypeName(typeName) { 31 | const instanceQueryFieldName = inflect.camelize(typeName, false); 32 | 33 | return instanceQueryFieldName; 34 | } 35 | 36 | export function getConnectionQueryFieldNameFromTypeName(typeName) { 37 | const instanceQueryFieldName = getInstanceQueryFieldNameFromTypeName(typeName); 38 | const connectionQueryFieldName = inflect.pluralize(instanceQueryFieldName); 39 | 40 | return connectionQueryFieldName; 41 | } 42 | 43 | // - From Type Name and Attribute Name - 44 | export function getAttributeIdentFromAttributeNameAndTypeName(attributeName, typeName) { 45 | const attributeIdentNamespace = `:${inflect.camelize(typeName, false)}`; 46 | const attributeIdentString = `${attributeIdentNamespace}/${attributeName}`; 47 | const attributeIdent = edn.kw(attributeIdentString); 48 | 49 | return attributeIdent; 50 | } 51 | 52 | // - From Reverse Ref Field - 53 | export function getReverseRefTypeNameFromReverseRefField(reverseRefField) { 54 | const reverseRefFieldEdn = edn.kw(reverseRefField); 55 | const reverseRefFieldNamespace = reverseRefFieldEdn.ns; 56 | const reverseRefTypeName = inflect.camelize(reverseRefFieldNamespace.replace(':', '')); 57 | 58 | return reverseRefTypeName; 59 | } 60 | 61 | export function getReverseRefFieldNameFromReverseRefField(reverseRefField) { 62 | const reverseRefFieldEdn = edn.kw(reverseRefField); 63 | const reverseRefFieldName = reverseRefFieldEdn.name; 64 | 65 | return reverseRefFieldName; 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/queryInstalledInterfaces.js: -------------------------------------------------------------------------------- 1 | import consumer from '../consumer'; 2 | import edn from 'jsedn'; 3 | import { getAttributeNameFromAttributeIdent, getTypeNameFromAttributeIdent } from './inflect'; 4 | import { reduce } from 'underscore'; 5 | 6 | export default (apiUrl, dbAlias) => { 7 | const db = consumer(apiUrl, dbAlias); 8 | const installedInterfacesQueryEdn = new edn.Vector([ 9 | edn.kw(':find'), 10 | new edn.Vector([ 11 | new edn.List([ 12 | edn.sym('pull'), edn.sym('?e'), new edn.Vector([ 13 | edn.kw(':extGraphQL.interface/name'), 14 | new edn.Map([edn.kw(':extGraphQL.interface/implementations'), new edn.Vector([edn.kw(':db/ident'), edn.kw(':extGraphQL.type/name')])]), 15 | edn.kw(':extGraphQL.interface/doc'), 16 | new edn.Map([edn.kw(':extGraphQL/_refTarget'), new edn.Vector([edn.kw(':db/ident')])]), 17 | ]), 18 | ]), 19 | edn.sym('...'), 20 | ]), 21 | edn.kw(':where'), 22 | new edn.Vector([edn.sym('?e'), edn.kw(':extGraphQL.interface/name'), edn.sym('_')]), 23 | ]); 24 | 25 | return db.query(installedInterfacesQueryEdn) 26 | .then(installedInterfacesData => reduce(installedInterfacesData, (aggregateInstalledInterfaces, typeData) => { 27 | const name = typeData[':extGraphQL.interface/name']; 28 | const doc = typeData[':extGraphQL.interface/doc']; 29 | const implementationsData = typeData[':extGraphQL.interface/implementations'] || []; 30 | const implementations = implementationsData.map(implementationData => { 31 | const implementationIdent = implementationData[':db/ident']; 32 | const implementationTypeName = implementationData[':extGraphQL.type/name']; 33 | 34 | return { 35 | ident: implementationIdent, 36 | type: implementationTypeName, 37 | }; 38 | }); 39 | const reverseReferenceAttributesData = typeData[':extGraphQL/_refTarget'] || []; 40 | const reverseReferenceFields = reverseReferenceAttributesData.map(reverseReferenceAttributeData => { 41 | const attributeIdent = reverseReferenceAttributeData[':db/ident']; 42 | const originTypeName = getTypeNameFromAttributeIdent(attributeIdent); 43 | const originAttributeName = getAttributeNameFromAttributeIdent(attributeIdent); 44 | 45 | return { 46 | type: originTypeName, 47 | field: originAttributeName, 48 | }; 49 | }); 50 | 51 | return { 52 | ...aggregateInstalledInterfaces, 53 | [name]: { 54 | implementations, 55 | doc, 56 | reverseReferenceFields, 57 | }, 58 | }; 59 | }, {})); 60 | }; 61 | -------------------------------------------------------------------------------- /src/utils/queryInstalledTypes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../consumer'; 2 | import edn from 'jsedn'; 3 | import { reduce } from 'underscore'; 4 | 5 | export default (apiUrl, dbAlias) => { 6 | const db = consumer(apiUrl, dbAlias); 7 | const installedTypesQueryEdn = new edn.Vector([ 8 | edn.kw(':find'), 9 | new edn.Vector([ 10 | new edn.List([ 11 | edn.sym('pull'), edn.sym('?e'), new edn.Vector([ 12 | edn.kw(':extGraphQL.type/name'), 13 | edn.kw(':extGraphQL.type/namespace'), 14 | edn.kw(':extGraphQL.type/doc'), 15 | ]), 16 | ]), 17 | edn.sym('...'), 18 | ]), 19 | edn.kw(':where'), 20 | new edn.Vector([edn.sym('?e'), edn.kw(':extGraphQL.type/name'), edn.sym('_')]), 21 | ]); 22 | 23 | return db.query(installedTypesQueryEdn) 24 | .then(installedTypesData => reduce(installedTypesData, (aggregateInstalledTypes, typeData) => { 25 | const name = typeData[':extGraphQL.type/name']; 26 | const namespace = typeData[':extGraphQL.type/namespace']; 27 | const doc = typeData[':extGraphQL.type/doc']; 28 | 29 | return { 30 | ...aggregateInstalledTypes, 31 | [name]: { 32 | namespace, 33 | doc, 34 | }, 35 | }; 36 | }, {})); 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/querySchemaImpliedTypes.js: -------------------------------------------------------------------------------- 1 | import consumer from '../consumer'; 2 | import edn from 'jsedn'; 3 | import { getAttributeNameFromAttributeIdent, getTypeNameFromAttributeIdent, getReverseRefTypeNameFromReverseRefField, getReverseRefFieldNameFromReverseRefField } from './inflect'; 4 | import { reduce } from 'underscore'; 5 | 6 | // (Value constants) 7 | const SYSTEM_ATTRIBUTE_NAMESPACES = [':db', ':fressian', ':extGraphQL']; 8 | 9 | export default (apiUrl, dbAlias) => { 10 | const db = consumer(apiUrl, dbAlias); 11 | const schemaImpliedTypesQueryEdn = new edn.Vector([ 12 | edn.kw(':find'), 13 | new edn.Vector([ 14 | new edn.List([ 15 | edn.sym('pull'), edn.sym('?e'), new edn.Vector([ 16 | edn.kw(':db/id'), 17 | edn.kw(':db/ident'), 18 | edn.kw(':db/doc'), 19 | edn.kw(':db/index'), 20 | edn.kw(':db/fullText'), 21 | new edn.Map([edn.kw(':db/valueType'), new edn.Vector([edn.kw(':db/ident')])]), 22 | new edn.Map([edn.kw(':db/cardinality'), new edn.Vector([edn.kw(':db/ident')])]), 23 | new edn.Map([edn.kw(':db/unique'), new edn.Vector([edn.kw(':db/ident')])]), 24 | edn.kw(':extGraphQL/required'), 25 | new edn.Map([edn.kw(':extGraphQL/refTarget'), new edn.Vector([edn.kw(':db/ident'), edn.kw(':extGraphQL.type/name'), edn.kw(':extGraphQL.interface/name')])]), 26 | edn.kw(':extGraphQL/reverseRefField'), 27 | new edn.Map([edn.kw(':extGraphQL/enumValues'), new edn.Vector([edn.kw(':db/ident')])]), 28 | ]), 29 | ]), 30 | edn.sym('...'), 31 | ]), 32 | edn.kw(':where'), 33 | new edn.Vector([edn.sym('?e'), edn.kw(':db/valueType'), edn.sym('_')]), 34 | ]); 35 | 36 | return db.query(schemaImpliedTypesQueryEdn) 37 | .then(rawSchemaAttributes => rawSchemaAttributes.map(rawSchemaAttribute => parseSchemaAttribute(rawSchemaAttribute))) 38 | .then(schemaAttributes => schemaAttributes.reduce((aggregateSchema, schemaAttribute) => { 39 | const schemaAttributeType = getTypeNameFromAttributeIdent(schemaAttribute.ident); 40 | const schemaAttributeName = getAttributeNameFromAttributeIdent(schemaAttribute.ident); 41 | 42 | // Exclude system-namespaced attributes from schema data... 43 | if (SYSTEM_ATTRIBUTE_NAMESPACES.find(ns => schemaAttribute.ident.indexOf(ns) >= 0)) { 44 | return aggregateSchema; 45 | } 46 | 47 | let nextSchemaValue = { 48 | ...aggregateSchema, 49 | [schemaAttributeType]: { 50 | ...aggregateSchema[schemaAttributeType], 51 | [schemaAttributeName]: schemaAttribute, 52 | }, 53 | }; 54 | 55 | if (schemaAttribute.reverseRefField) { 56 | const schemaAttributeReverseRefTypeName = getReverseRefTypeNameFromReverseRefField(schemaAttribute.reverseRefField); 57 | const schemaAttributeReverseRefFieldName = getReverseRefFieldNameFromReverseRefField(schemaAttribute.reverseRefField); 58 | 59 | nextSchemaValue = { 60 | ...aggregateSchema, 61 | ...nextSchemaValue, 62 | [schemaAttributeReverseRefTypeName]: { 63 | ...aggregateSchema[schemaAttributeReverseRefTypeName], 64 | [schemaAttributeReverseRefFieldName]: { 65 | ident: schemaAttribute.ident, 66 | index: true, 67 | valueType: 'ref', 68 | cardinality: 'many', 69 | isReverseRef: true, 70 | refTarget: schemaAttributeType, 71 | doc: `Reverse reference to ${schemaAttribute.ident}`, 72 | }, 73 | }, 74 | }; 75 | } 76 | 77 | return nextSchemaValue; 78 | }, {})); 79 | }; 80 | 81 | function parseSchemaAttribute(rawSchemaAttribute) { 82 | return reduce(rawSchemaAttribute, (aggregateAttribute, metaAttribute, metaAttributeKey) => { 83 | const metaAttributeKeyKeyword = edn.kw(metaAttributeKey); 84 | const metaAttributeKeyName = metaAttributeKeyKeyword.name; 85 | let metaAttributeValue; 86 | 87 | switch (metaAttributeKeyName) { 88 | case 'unique': 89 | case 'valueType': 90 | case 'cardinality': 91 | metaAttributeValue = edn.kw(metaAttribute[':db/ident']).name; 92 | break; 93 | case 'refTarget': 94 | metaAttributeValue = metaAttribute[':extGraphQL.type/name'] || metaAttribute[':extGraphQL.interface/name']; 95 | break; 96 | case 'enumValues': 97 | metaAttributeValue = metaAttribute.map(enumValue => edn.kw(enumValue[':db/ident']).name); 98 | break; 99 | default: 100 | metaAttributeValue = metaAttribute; 101 | } 102 | 103 | return { 104 | ...aggregateAttribute, 105 | [metaAttributeKeyName]: metaAttributeValue, 106 | }; 107 | }, {}); 108 | } 109 | --------------------------------------------------------------------------------