chillu

https://www.silverstripe.org/blog/first-table-leverages-silverstripe-cms-and-gatsby 💙 Thanks for writing this @gened!

Show 2 attachment(s)
silverstripe.org  
First Table Leverages Silverstripe CMS and Gatsby

Gatsby is a development framework that delivers high performing websites and apps. And if you’re willing to experiment, it’s a match made in heaven with Silverstripe CMS. Lead Developer at First Table, Gene Dower shares his experience with this web development dream team.

silverstripe.org  
First Table Leverages Silverstripe CMS and Gatsby

Gatsby is a development framework that delivers high performing websites and apps. And if you’re willing to experiment, it’s a match made in heaven with Silverstripe CMS. Lead Developer at First Table, Gene Dower shares his experience with this web development dream team.

Hide attachment content
wmk

@gened do you think you can provide a "forms with gatsby and SS" howto?

gened

Hey @wmk yeah I'll put something together. What would you like to a see? End to end, so form generation in FE and SS handling form submissions? Would an example FE and SS sample project be best?

harvs1789uk

@unclecheese Can I remove the default args for a scaffolded Update mutation?

I have an object which implements ScaffoldingProvider and has a provideGraphQLScaffolding() method which adds a readMany query and an update mutation. On the mutation, I have defined a number of my own argument, including some non-nullable ones which are used to lookup the relevant object to be updated (making a lookup by object ID redundant)

I can see a few examples of Input: { ID: ID! } (plus other non-complex types from $db) being set up and required as ‘default’ args, but I do not want or need these and even adding the following to my chainable operation scaffolding doesn’t seem to get rid of these defaults?

->removeArg('Input')

harvs1789uk

For context, my object is called MessageThread and represents a conversation between two Member objects. My frontend has little knowledge of these MessageThread objects and doesn’t maintain any record of their ID’s, but it does have full knowledge of the two participants of the thread, which is why I have args of ParticipantIdA and ParticipantIdB both of which are required, non-nullable ID types

unclecheese

I guess I would say I’d you’re removing the default arg, then the default resolver won’t work, so why are you using a scaffolded mutation in the first place?

unclecheese

In v4, you have much more control over all this, and I would love for you to be an alpha tester. You seem to be pushing on a lot of the sharp corners of the api.

harvs1789uk

I have a custom resolver yes, in terms of why scaffold, perhaps the most honest answer is, I don’t know how to do it any other way?

harvs1789uk

I am using scaffolding for sure, but I am essentially defining all the fields, args, resolvers etc myself…

harvs1789uk

Happy to be involved in some alpha testing

robjingram

Is there any way to sort by a field on an association using ‘setSortableFields’. The generated enum doesn’t seem to like dot notation.

mineka

is there a way to exclude archived records when reading in LATEST mode?

harvs1789uk

Not certain if this is a GraphQL related issue, or just a general issue, so bear with me…

I have an unversioned DataObject, called DiaryEntry. It has_one Image and it also owns Image (Image of course being versioned by default)

When using the CMS interface to manage (CRUD) these DiaryEntry objects, everything works as expected, saving a DiaryEntry with a related Image (as a draft) correctly publishes the related Image.

However, My Create and Update GraphQL resolvers (which work in all other respects, seem not to behave in the same way, despite calling both write() and publishRecurive() on the parent/owning DiaryEntry object (as below)

Sample of my Create and Update resolvers on DiaryEntry:

  1. function ($object, array $args, $context, ResolveInfo $info) {
  2.  
  3. // Redacted For Brevity - Create or Lookup DiaryEntry + Set Fields Accordingly etc
  4.  
  5. try {
  6. $entry->write();
  7. $entry->publishRecursive();
  8. } catch (Exception $e) {
  9. throw new Exception("Failed to update " . __CLASS__ . " # " . $entry->ID . "!", 500, $e);
  10. }
  11.  
  12. return $entry;
  13.  
  14. }

The GraphQL mutation completes successfully, the DiaryEntry is created or updated as expected however, the related Image remains in Draft

Anyone got any idea what I am doing wrong?

unclecheese

Hi, GraphQL users!

I’ve spend the last couple of weeks on the v4 release of the graphql-module. As previously stated, this release will focus on performance and a better dev experience. I’ve published a draft PR here: https://github.com/silverstripe/silverstripe-graphql/pull/266 detailing some of the changes. This should be considered a living document that will update as the API takes shape, but it does make clear what the intentions are, and a bit about what the upgrade will be like.

Please have a look and share any feedback you have. Still heaps to do, and get those feature requests in while you can!

Show 1 attachment(s)
unclecheese

GraphQL v4: Let's scale this thing

Note: This document will continue to be updated as changes evolve. It is not a comprehensive view of everything that will be included in v4, but rather an overview of where we want to be and how things are going.

This pull request represents a large body of changes to the GraphQL module that enhance its performance and developer experience on a very fundamental level. It is a complete rewrite of the entire schema-facing APIs including scaffolding, type creators, middleware, and more.

High-level goals/changes

This section describes the overarching intention of the changes. For information on upgrading, see the "Upgrading" section below.

Drastically improved performance

Some rough testing showed plenty of merit to the chosen approach:

Vagrant stack, no major performance optimisations such as opcode caching

While there appears to be some variance in the results, it may very well be possible that the response times do not scale with the number of types queries -- in fact, due to the new lazy loading approach, we would expect this to be the case. That the response times seem to increase with types could just be noise in the averaging of response times that varied quite a bit.

tl;dr this needs more testing, but we know we're on the right path.

No more dynamic schema generation

Generating the schema at runtime was the biggest performance bottleneck for this module and the primary reason why it could not be scaled out for heavy production use. This could not go on.

Instead of a dynamically generated schema, we build one in a new task, e.g. /dev/graphql/build with code generation. This will be a major change to the developer experience, but we believe the tradeoffs are worth it. It means that any additions of fields or changes to types, resolvers, etc., will require a separate build. It's possible that we could bundle this with dev/build, however it should be noted that while the two tasks are often interdependent, there are many cases where you need one and not the other. At the moment, the build schema task appears to be many times faster than dev/build.

Manager has been removed broken up into multiple concerns

The Manager class was becoming a catch-all that handled registration of types, execution of scaffolding, running queries/ middleware, error handling, and more. This has been broken up into a few new classes, primarily:

SchemaBuilder <-- the new config namespace for your schema • QueryHandlerInterface

3: Much more generous configuration API

In GraphQL 4, we'll aim to build schemas with config by default, and code will fill in the gaps for parts of the schema that can only be computed at build time. A thorough review of the current "TypeCreator" APIs, with the exception of their resolvers, are just value objects that map to graphql-php primitives. This can be done declaratively.

4: Resolvers use a new discovery pattern

Types can be created declaratively, but what about resolvers? The key to caching the schema has always been moving to static resolvers rather than context-aware class members. To solve this problem, we'll use a ResolverRegistry class where the developer can register any number of ResolverProvider implementations. Those implementations must provide logic on how to locate the static method given a type name and field name. Below is a simple resolver provider that ships with the module:

    public static function getResolverMethod(?string $typeName = null, ?string $fieldName = null): ?string
    {
        $candidates = array_filter([
            // resolveMyTypeMyField()
            $typeName &amp;&amp; $fieldName ?
                sprintf('resolve%s%s', ucfirst($typeName), ucfirst($fieldName)) :
                null,
            // resolveMyType()
            $typeName ? sprintf('resolve%s', ucfirst($typeName)) : null,
            // resolveMyField()
            $fieldName ? sprintf('resolve%s', ucfirst($fieldName)) : null,
            // resolve()
            'resolve',
        ]);

        foreach ($candidates as $method) {
            $callable = [static::class, $method];
            $isCallable = is_callable($callable, false);
            if ($isCallable) {
                return $method;
            }
        }

        return null;
    }

A type named Product with a field named Price will look in the resolver registry in this order:

resolveProductPriceresolveProductresolvePriceresolve

An important note here is that this computation is not happening at query time. It solely exists to determine what static method will be added to the generated schema PHP code.

Non-dataobject types are just... config

With resolvers relying on their own discovery logic, writing new types becomes exceedingly simple:

types:
  Product:
    fields:
      price: Int!
      reviews: '[ProductReview]'
      image(Size: ImageSize!): Image

Notice the ability to inline arguments.

Goodbye, scaffolders, hello models

This version decouples DataObjects from the module and abstracts them away into a concept called "models." A model is any class that can express awareness of its schema. It should be able to answer questions like:

• Do you have field X? • What type is field Y? • What are all the fields you offer? • What operations do you provide? • Do you require any extra types to be added to the schema?

By default, we ship a DataObject implementation of a model. The configuration looks like this:

models:
  MyDataObjectType:
    class: My\DataObject
    fields:
      # infer type
      title: true
      # explicit type
      myGetter: String
      # add an arg!
      date(Format: String): true
    operations:
      read: true
      update:
        exclude: sensitiveField

Note that dataobjects are no longer one-to-one with their types. You can have one dataobject map to multiple types.

Resolver contexts

For scaffolded operations, context is critical to the resolvers. They need to know what class to get in the ORM, for instance. For this, you can use resolver context. Fields accept resolver context and pass it as the argument to the resolver function.

public static function resolve(array $context)
{

    return static function ($obj, $args = [])
    {
        return DataList::create($context['dataClass']);
    }
}

Enums are first-class citizens

You can now define enums as plain config:

      enums:
        SortDirection:
          values:
            - ASC
            - DESC
        ProductStatus:
          values:
            - PENDING
            - CANCELLED
            - SHIPPED

Lowercase field names are encouraged and supported

The current pattern of mapping field names to their UpperCase counterparts in the ORM is a bit unconventional. If you express them in lowercase, they'll just work.

New GraphQL developer tools

The developer API for GraphQL has expanded quite a bit in the lifetime of this module, and with these new changes, we'll now have three dev tools all with their own endpoints:

• GraphQL IDE (graphiql) • Show schema (print the schema in type language) • Build schema • Debug resolvers (show all types and where their fields resolve)

In light of this, it makes sense to give these tools their own namespace in dev/graphql. We'll have:

dev/graphql/idedev/graphql/schemadev/graphql/builddev/graphql/resolvers

New performance middleware

Since the primary focus of this version is performance improvements, a performance middleware will ship by default, enabled in non-live modes:

query {
  myQuery {

  }
  __perf {
   schema # time it took to load the schema
   query # query execution time
   middleware # middleware execution time
  }

Upgrade to graphql-php ^14.0

The new <https://github.com/webonyx/graphql-php/pull/557|lazy loading of types> makes this all possible.

Upgrading

Again, this is a work in progress, and should mostly be conside…

Hide attachment content
Scopey

Can you put together a list of TODOs or something? I might finally get around to doing some SS work this weekend and I'd love to know how I can contribute here

harvs1789uk

I am not going to pretend to understand everything you have laid out there, but of that which I do understand, it all looks good 👏