Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Research: DataObject subclass/extension in queries and mutation #11

Closed
chillu opened this issue Nov 6, 2016 · 5 comments
Closed

Research: DataObject subclass/extension in queries and mutation #11

chillu opened this issue Nov 6, 2016 · 5 comments
Assignees

Comments

@chillu
Copy link
Member

chillu commented Nov 6, 2016

Introduction

GraphQL requires a query to contain every field a view component could use, usually at "compile time". Apollo does this through the graphql-tag helper. There is no wildcard for fields, explicit field querying is built into the specification.

In SilverStripe, a form can display data from arbitrary fields based on your own project's DataObject subclass structure, as well as DataExtension on existing models. This somewhat conflicts with GraphQL's assumptions about the static nature of views and their data requirements.

In order to retain SilverStripe's ability to influence the view through PHP only, we need a way for GraphQL to include fields from DataObject subclasses and DataExtension in queries and mutations, without requiring recompilation of the JS bundle.

This will also become an issue when using GraphQL as a general purpose SilverStripe Content API: You'd need to construct queries based on introspection in order to return all available data for a particular DataObject structure. Since this structure differs between projects, there's no predefined query to query all fields.

Story Candidates

  • Denormalise all fields on a particular DataObject subclass into one GraphQL type (overlaps with boilerplate, ie File and subclasses are one single type)
  • Create JSON structure for client to infer types on queries (both input and type) - run introspection query
  • Create gql AST from JSON structure for particular queries and mutations (ingest the introspection JSON)

Option 1: Use a "fields" array on types

This negates most of the advantages of using GraphQL in the first place (strong typing), since we're only creating types for the field metadata (name and value), not the actual type signature.

import React, { Component } from 'react';
import { graphql } from 'react-apollo';

const readFilesQuery = gql`query {
  readFiles(id: $id) {
    fields {
      name
      value
    }
}`

@graphql(readFilesQuery)
class MyComponent extends Component {
  render() {
    return <div>...</div>;
  }
}

Return:

{
  "fields": [
    { "name": "ID", "value: "123" }
  ]
}

Option 2: Compose GraphQL queries by introspection

GraphQL supports introspection of types and queries, which allows us to identify all available queries upfront. Queries can use interfaces for a returned type, but we can't infer types from these (determined at runtime through GraphQL resolvers). We need to include all fields from the respective types, potentially as reuseable fragments.

GraphQL in Apollo is parsed via the gql template literal tag and the graphql-tag library.

// constructed with JSON data precompiled through SilverStripe
class schemaHelper {
  constructor(schema) {
    this.schema = schema;
  }
  getQuery(name) {
    // TODO
  }
}
export default schemaHelper;
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import schemaHelper from 'lib/schemaHelper';

// Returns a GraphQL AST, same as the gql() tag
const getQuery = (name) => {
  return schemaHelper.getQuery(name);
}

const readFilesQuery = getQuery('readFiles');

@graphql(readFilesQuery)
class MyComponent extends Component {
  render() {
    return <div>...</div>;
  }
}

Option 3: Fragment Registry

If we exclude strongly typed form state and submissions, we're left with GraphQL view components which know their fields upfront since they're using them directly as props (for example <Gallery> knows it'll need a title prop, but has no built-in use for other fields which might be added through a DataExtension). This means new fields in GraphQL queries will always coincide with new view components using them. This can be solved through a "fragment registry" which adds fragments ("containers" for new fields) to named queries: #13

Notes

  • Use case: Populate form field state for SiteTree subclasses (e.g. RedirectorPage, or MyCustomPage), and save them back to their respective database columns
  • Use case: Save additional fields through existing React components, for example choosing an owner in the "create folder" dialog

Example Query

This is a query to read file data (from AssetAdmin.js).

const readFilesQuery = gql`query ReadFiles($id:ID!) {
  readFiles(id: $id) {
    ...allFields
    ...allFileFields
    ...on Folder {
      children {
        ...allFields
	      ...allFileFields
      },
      parents {
        __typename
        id
        title
      }
    }
  }
}
fragment allFields on FileInterface {
  __typename
  id
  parentId
  title
	type
  category
  exists
  name
  filename
  url
  canView
  canEdit
  canDelete
}
fragment allFileFields on File {
	__typename
	extension
	size
}
`;

Example Introspection query

{
  __schema {
    types {
      name
      fields {
        name
        type {
          name
          kind
          ofType {
            name
            kind
          }
        }
      }
    }
  }
}

Returns

https://gist.github.com/chillu/d13cc32771223a15d7b995e581704f54

@chillu chillu added this to the CMS 4.0.0-alpha4 milestone Nov 6, 2016
@chillu
Copy link
Member Author

chillu commented Nov 7, 2016

Sam recommended that we don't use types for subclasses, and rather replicate the DataObject::get() behaviour and return all fields for available subclasses, with a className type and null values where fields don't apply.

He further suggested to look into creating GraphQL queries dynamically based on form schema fields, and avoid querying all fields even if they're not required. Ingo has concerns on the practicality of this approach because Apollo expects queries to be registered as a Higher Order Component on component definition time, not at runtime within the component itself.

@chillu
Copy link
Member Author

chillu commented Nov 15, 2016

More observations:

  • Since the graphql() higher order component executed gql() on script load, we need to have any field/type/fragment definitions available before the Webpack bundle is evaluated by the browser. <script> tags don't have a guaranteed load order. Since we can't inline it during Webpack compilation time, Webpack loaders won't be of any help here (see discussion). We could retrieve the field definitions JSON first via XHR, then load the Webpack bundle in the success callback, but that seems messy. Alternatively, we write the field definitions JSON into the <body> of the HTML response.
  • The graphql() higher order component in react-apollo isn't designed to work with dynamic queries - it assumes a query as its first argument, and calls subscribeToQuery() when the component first mounts (see source)
  • The underlying client has a query() method we could use to pass in queries at runtime, but that's removing all the benefits of the react-apollo module which make GraphQL interesting for us in the first place (data co-location with components, not worrying about data fetching). Apollo recommends using query() for prefetching data, but it seems to assume that the query will also be co-located with a component not shown in the example.
  • Sam mentioned he doesn't like loading all fields just in case, and would like to generate a query based on the form schema. While we could use client.query() at runtime for retrieving form schema state, it'll require substantial boilerplate to make it work nicely with queries created by the graphql() higher order component. For example, a tree component will run a query to retrieve a subset of fields for each node, while the current "edit page" form needs all fields on a particular page. These queries overlap and should have one single "source of truth", otherwise keeping state in sync will be hard (e.g. changing a tree node title when saving an "edit page" form).
  • There's a webpack loader for graphql files, which could simplify inclusion if it wasn't for the fact that it still needs a JS compilation step for adding a field to your domain model.
  • The graphql-tag library underpinning the gql() helper doesn't provide any high level API to modify the syntax tree (AST) after it has been created, making it hard to add or remove fields outside of string manipulation before passing into gql().

@chillu
Copy link
Member Author

chillu commented Nov 17, 2016

I've opened an issue on react-apollo regarding the dynamic query definitions: apollographql/react-apollo#330

@chillu chillu changed the title DataObject subclass/extension in queries and mutation Research: DataObject subclass/extension in queries and mutation Nov 17, 2016
@chillu chillu self-assigned this Nov 17, 2016
@chillu
Copy link
Member Author

chillu commented Nov 24, 2016

I've added an Option 3 "Fragment Registry" (#13)

@chillu
Copy link
Member Author

chillu commented Nov 24, 2016

Research concluded in preferring Option 3 "Fragment Registry" (#13). This assumes we don't need access to all fields on the client (which would necessitate Option 1 or 2). Form submissions are handling generic types (not specific to models), hence can add fields dynamically (see #10). It's unclear how types will work with a React-based GridField, which needs to operate with different types on the same JavaScript source. It'll likely be a similiar solution to form submissions, where generic types expose {name: "MyField", value: "MyValue"} objects.

@chillu chillu closed this as completed Nov 24, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant