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

Plugin should have module-wise resolvers similar to the wasm #603

Closed
Niraj-Kamdar opened this issue Jan 7, 2022 · 3 comments · Fixed by #620
Closed

Plugin should have module-wise resolvers similar to the wasm #603

Niraj-Kamdar opened this issue Jan 7, 2022 · 3 comments · Fixed by #620
Assignees
Milestone

Comments

@Niraj-Kamdar
Copy link
Contributor

Niraj-Kamdar commented Jan 7, 2022

Currently, when we create a plugin, we will just create a single schema file, a Plugin class and resolvers for both query and mutation module. This will create issues while porting some features from wasm polywrapper to plugin that are module dependent Ex: env

In wasm polywrapper, we can import env directly from the w3 and use it since the both query and mutation modules will always be decoupled in the separate files and we can correctly have env of either QueryEnv or MutationEnv type depending on the module but in case of plugin, since both modules are in same file and we only generate one single w3, we can have env with both the QueryEnv and MutationEnv types so we can't assign it either one of the type, though we can workaround this by creating 2 separate variables: queryEnv, mutationEnv but that wouldn't be a good and consistent experience. Not to mention in the future we may have more than 2 modules.

So a better solution would be to create separate directories for every module with its own w3 generated codes in plugin just like we have it in wasm polywrappers.

Ex: In plugin we can have classes for all the available modules:

// Ethereum_Query
import { Input_getGasPrice } from "w3";
class Query {
  constructor(private readonly configs: QueryConfigs) // configs if needed 
  ...
  getGasPrice: async (input: Input_getGasPrice): Promise<string>
  ...
}

This will replace current resolvers with module wise classes, we can pass state in constructor from the Plugin class, this state can even be shared or isolated depending on how plugin devs want it to be implemented.

Ex: Plugin class would just be implementation of the core Plugin interface without any other methods.

export class EthereumPlugin extends Plugin {
  constructor(config: EthereumConfig) 
  ...
  public static manifest(): PluginPackageManifest {
  ...
  }

  public getModules(
    _client: Client
  ): {
    query: Query;
    mutation: Mutation;
  } {
    return {
      query: () => new Query(this.config),
      mutation: () new Mutation(this.config),
    };
  }

We may not even need configs for most of the cases after env support since we can pass it using env from the client, although we would still need it in some cases where user wants to pass an object instance instead of string serializable env so for that having it is still necessary.

@nerfZael
Copy link
Contributor

Looks good! Anything we can do to bring the experience closer to the wasm one is a good thing in my view

@dOrgJelli
Copy link
Contributor

Yes I agree @Niraj-Kamdar, this is very well thought out. I agree with moving in this direction ASAP so we can properly support environments within plugins 👏

@Niraj-Kamdar
Copy link
Contributor Author

Here's the design spec for improving plugin development experience:
The idea is to help user build plugins easily and faster by generating as much as code we can to help them but at the same time allowing power users to extend the functionality easily if they want to.

Plugin Directory structure

Here's the how a project directory structure for a plugin would look like:

├── src
│   ├── index.ts
│   ├── mutation
│   │   ├── index.ts
│   │   ├── schema.graphql
│   │   └── w3
│   │       ├── mutation.ts
│   │       └── types.ts
│   ├── query
│   │   ├── index.ts
│   │   ├── schema.graphql
│   │   └── w3
│   │       ├── query.ts
│   │       └── types.ts
│   └── w3
│       ├── manifest.ts
│       ├── plugin.ts
│       └── schema.ts
├── tsconfig.build.json
├── tsconfig.json
└── web3api.plugin.yaml

What users need to write?

Plugin Manifest

manifest file would become similar to the wasm manifest file

format: 0.0.1-prealpha.2
language: plugin/typescript
entrypoint: ./src
modules: 
  mutation:
    schema: ./mutation/schema.graphql  # relative to entrypoint
    module: ./mutation/index.ts
  query:
    schema: ./query/schema.graphql
    module: ./query/index.ts

Here entrypoint will be root directory where all the code is situated and must contain an index.ts entrypoint file.

Interface schema

User should write interface for any plugins they build, this is to ensure that any implementation of a plugin interface would share the same interface regardless of system, languages and environments and could be used in wasm polywrapper in similar ways.

type Query {
  getData(): String!
  getDataByArg1(arg1: String!): String!
}

Side-discussion on how would it affect polywrap devs who are going to use these plugins

In client, users can use interface and implementation URI configs to attach one or more implementation plugins for an interface, It will be the reponsibility of wrapper developer to go over these implementations using the getImplementations method of an Interface and execute the appropriate implementation depending on context.

The reason for not automating execution of interface implementations are: In some cases like logger, we may want to execute all the implementations for streaming logs to different services: Console, Filesystem, Http, etc while in cases like cache store you may want to first check in-memory cache then file-system and lastly network caches like memcache, this is completely different flow then executing all the implementations and we have no way of knowing how developer of polywrap would like it to be implemented so it'd be better to just leave this upto devs to implement it however they wants.

Here's the client side code snippet to attach interface with an implementation

const client = await getClient({
  plugins: [...]
  interfaces: [
    {
      interface: interfaceUri,
      implementations: [implementationUri],
    }
  ],
});

Here, implementation can either plugin or other polywrapper. Once attaching implemntations with interfaces user can query polywrapper normally.

Here's the possible wrapper side code snippet

  • schema
    #import { Query, InterfaceType } into Interface from "w3://ens/interface.plugin.eth"
    #use { getImplementations } for Interface```
  • assemblyscript
    import { Input_queryMethod, Input_abstractQueryMethod, ImplementationType, Interface } from "./w3";
    
    export function someFunction(): SomeType {
      const implementations = Interface.getImplementations();
      for (const implementation of implementations) {
        doSomething(implementation);
      } 
    }

Module schema -> (path: ./src/query/schema.graphql)

User needs to write schema for all the modules he wants to be supported by plugins

#import { Query } into Interface from "interface.plugin.eth"

type Query implements Interface_Query {}

Module Implementation -> (path: ./src/query/index.ts)

  • User needs to implement abstract module class generated by codegen and export it
  • User also need to write QueryConfigs class and export it
import { QueryModule } from "./w3/query";
import { Input_getDataByArg1, QueryConfigs } from "./w3/types";

export interface QueryConfigs {
  arg1: string
  arg2: number
}

export class Query implements QueryModule {
  constructor(private _configs: QueryConfigs) {}

  getData(): string {
    return "data";
  }

  getDataByArg1(input: Input_getDataByArg1): string {
    return input.arg1;
  }
}

index file -> (path: ./src/index.ts)

Generally user will just export generated plugin class, in special case user can override it by extending it

export * from "./w3/plugin";

What we will generate for user?

Module Types -> (path: ./src/query/w3/types.ts)

We will generate all the types defined in graphql schema of the particular module for the user.

export type Input_getDataByArg1 = {
  arg1: string;
}

Module abstract class -> (path: ./src/query/w3/query.ts)

We will generate an abstract module class with all the methods defined in schema so that user can easily implement it

import { Input_getDataByArg1, QueryConfigs } from "./types";

export abstract class QueryModule {
  constructor(_configs: QueryConfigs) { }

  abstract getData(): string;
  abstract getDataByArg1(input: Input_getDataByArg1): string;
}

We will also generate PluginClass, combined schema and manifest for user where combined schema and manifest would be similar to current one while PluginClass would look like...

Plugin Class -> (path: ./src/w3/plugin.ts)

import { Plugin, PluginFactory, PluginPackageManifest } from "@web3api/core-js";
import { Query, QueryConfigs } from "../query";  // user must export this, raises error if not exported
import { Mutation, MutationConfigs } from "../mutation";
import { manifest } from "./manifest";  // This will be generated the same way we are generating it currently

export interface PluginConfigs {
  query: QueryConfigs;
  mutation: MutationConfigs;
}

class MockPlugin extends Plugin {
  constructor(private _configs: PluginConfigs) {
    super();
  }

  public static manifest(): PluginPackageManifest {
    return manifest;
  }

  public getModules(): {
    query: () => Query;
    mutation: () => Mutation;
  } {
    return {
      query: () => (new Query(this._configs.query)),
      mutation: () => (new Mutation(this._configs.mutation)),
    };
  }
}

export const mockPlugin: PluginFactory<PluginConfigs> = (
  opts: PluginConfigs
) => {
  return {
    factory: () => new MockPlugin(opts),
    manifest: manifest,
  };
};
export const plugin = mockPlugin;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants