-
Notifications
You must be signed in to change notification settings - Fork 275
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
Feature request: Nexus plugins #98
Comments
Actually considered this when building Nexus, (adding the ability to extend the For cases like what you're suggesting you can usually just create "higher order functions" that expose a custom/configurable options object to achieve something like what you're going for: const cardFieldTypeMapping = {
id: 'ID',
exp_month: 'Int',
exp_year: 'Int',
brand: 'String',
last4: 'String'
}
interface TodoStripeTypes {
// ... not taking the time to define these right now,
// but it should be fairly straightforward
}
function stripeObjectType(cfg: TodoStripeTypes) {
const {
definition,
cardFields = ['id', 'brand', 'last4', 'exp_month', 'exp_year']
} = cfg
return objectType({
name: cfg.name,
definition(t) {
cardFields.forEach(field => {
if (typeof field === 'string') {
t.field(field, { type: cardFieldTypeMapping[field] })
} else if (field && typeof field === 'object') {
Object.keys(field).forEach((key) => {
t.field(field[key], {
type: cardFieldTypeMapping[key],
resolve: (root) => root[key]
})
})
}
})
if (typeof definition === 'function') {
definition(t)
}
}
})
} then you can do: const Card = stripeObjectType({
name: "Card",
cardFields: ["id", "brand", "last4", { exp_month: "expMonth" }, { exp_year: "expYear" }]
}); and optionally add new fields by providing a definition block: const Card = stripeObjectType({
name: "Card",
cardFields: ["id", "brand", "last4", { exp_month: "expMonth" }, { exp_year: "expYear" }],
definition(t) {
t.field('filteredCharges', {
type: 'SomeCustomType',
list: true,
resolve() {
// ...
}
})
}
});
Before we use a popular vote to determine this feature, let's think more through the cases we're solving for and how they can be accomplished with the existing features of nexus. I do plan on simplifying some of the patterns used internally by |
@tgriesser Thanks for your detailed response. However, it is still confusing to me. I would like that PS: I know this issue is labeled as discussion but I would like to get it working so I can see pros and cons, and bring to this thread a more robust opinion. |
Are you using |
Everything works well in the same repo ( |
@tgriesser I think there's no easy way right now for jferrettiboke to accomplish what's he's trying to do. If I'm understanding his problem well, he's trying to "merge" several schemas at the same time: One from prisma, and one from stripe. However, afaik the current plugin architecture doesn't seem composable.
TLDR: I think prisma's use-case is not special and we probably need a way to easily be able to compose these plugins and schemas. On your example for instance, where would
|
@Weakky I think you are very close to what I need. However, my use case is not about using Prisma (but other developers could have this use case where they need to mix both plugins at the same time). I will provide more context so both of you can understand better my needs.
The main use case for
The ideal way would be that the
As I already said, I don't have that need yet. However, some developers could be interested in extending an object type from For more context, here is app example using Yoga2 with |
I have been thinking about how I would like to see this. Here's my proposal. What do you guys think? // Creates the `Customer` type in app schema
export const Customer = objectType({
name: "Customer",
definition(t) {
// Refers to the `User` type in Prisma schema
t.prismaObjectType({
name: "User",
fields: ["email"]
});
// Refers to the `Customer` type in Prisma schema
t.prismaObjectType({
name: "Customer",
fields: ["stripeCustomerId"]
});
// Refers to the `Customer` type in Stripe schema
t.stripeObjectType({
name: "Customer",
fields: [
"id",
"cards",
{
name: "created",
alias: "createdAt"
}
]
});
// Custom field
t.field("subscription", {
...t.stripeType.subscription,
resolve(root, args, ctx) {
// Custom implementation
}
});
}
}); Output: type Customer {
email: String!
stripeCustomerId: ID!
id: ID!
cards: [Card!]!
createdAt: DateTime!
subscription: Subscription
} In this way, we can extend very easily without having to type by ourselves third-party schemes and/or fields. |
Could this also be accomplished by having the import { stripeType, stripeObjectType } from 'stripe-graphql'
// Creates the `Customer` type in app schema
export const Customer = objectType({
name: "Customer",
definition(t) {
// Refers to the `User` type in Prisma schema
t.prismaObjectType({
name: "User",
fields: ["email"]
});
// Refers to the `Customer` type in Prisma schema
t.prismaObjectType({
name: "Customer",
fields: ["stripeCustomerId"]
});
// Refers to the `Customer` type in Stripe schema
stripeObjectType(t, {
name: "Customer",
fields: [
"id",
"cards",
{
name: "created",
alias: "createdAt"
}
]
});
// Custom field
t.field("subscription", {
...stripeType.subscription,
resolve(root, args, ctx) {
// Custom implementation
}
});
}
}); I believe this is the API I had originally suggested to Prisma specifically because it makes things a lot more difficult internally and from a type-generation standpoint to have things dynamically attached to the "builder" in the way that On a related note that might be relevant for your If I had a type Customer {
email: String!
stripeCustomer: StripeCustomer
} |
I integrated nexus with import {
objectType,
extendType,
queryField,
mutationField,
subscriptionField,
} from 'nexus'
import {
NexusObjectTypeConfig,
NexusExtendTypeConfig,
GetGen,
FieldOutConfig,
SubscribeFieldConfig,
} from 'nexus/dist/core'
import {
ShieldRule,
IRuleFieldMap,
IRuleTypeMap,
} from 'graphql-shield/dist/types'
import { merge, capitalize } from 'lodash'
import { Rules } from './rules'
import { graphql as Log } from '../log'
export const isShieldRule = (
rule: ShieldRule | IRuleFieldMap | IRuleTypeMap
): rule is ShieldRule => {
// FieldMaps and TypeMaps have the Object constructor, whereas
// Rule and LogicRule have a dedicated constructor (e.g. Rule, RuleAnd, RuleTrue, ...)
return rule.constructor.name !== 'Object'
}
enum ParentType {
NONE,
QUERY,
MUTATION,
SUBSCRIPTION,
}
const mergeShieldRule = (
type: ParentType,
field: string,
rule: ShieldRule | IRuleFieldMap
) => {
// Some logic here
}
export const shieldedObjectType = <TypeName extends string>(
config: NexusObjectTypeConfig<TypeName> & {
rules?: ShieldRule | IRuleFieldMap
}
) => {
if (config.rules) {
mergeShieldRule(ParentType.NONE, config.name, config.rules)
}
return objectType(config)
}
export const shieldedExtendType = <
TypeName extends GetGen<'objectNames', string> | 'Query' | 'Mutation'
>(
config: NexusExtendTypeConfig<TypeName> & {
rules?: ShieldRule | IRuleFieldMap
}
) => {
if (config.rules) {
mergeShieldRule(ParentType.NONE, config.type, config.rules)
}
return extendType(config)
}
export const shieldedQueryField = <FieldName extends string>(
fieldName: FieldName,
config:
| FieldOutConfig<'Query', FieldName> & {
rule?: ShieldRule
}
| (() => FieldOutConfig<'Query', FieldName> & {
rule?: ShieldRule
})
) => {
if (typeof config === 'function') {
// eslint-disable-next-line no-param-reassign
config = config()
}
if (config.rule) {
mergeShieldRule(ParentType.QUERY, fieldName, config.rule)
}
return queryField(fieldName, config)
}
export const shieldedMutationField = <FieldName extends string>(
fieldName: FieldName,
config:
| FieldOutConfig<'Mutation', FieldName> & {
rule?: ShieldRule
}
| (() => FieldOutConfig<'Mutation', FieldName> & {
rule?: ShieldRule
})
) => {
if (typeof config === 'function') {
// eslint-disable-next-line no-param-reassign
config = config()
}
if (config.rule) {
mergeShieldRule(ParentType.MUTATION, fieldName, config.rule)
}
return mutationField(fieldName, config)
}
export const shieldedSubscriptionField = <FieldName extends string>(
fieldName: string,
config:
| SubscribeFieldConfig<'Subscription', FieldName> & { rule?: ShieldRule }
| (() => SubscribeFieldConfig<'Subscription', FieldName> & {
rule?: ShieldRule
})
) => {
if (typeof config === 'function') {
// eslint-disable-next-line no-param-reassign
config = config()
}
if (config.rule) {
mergeShieldRule(ParentType.SUBSCRIPTION, fieldName, config.rule)
}
return subscriptionField(fieldName, config)
} Sorry for the huge code block. I realised that I need to overwrite a bunch of nexus types ( It would be great if there was an api to register plugins, which can add fields to different object types and do something with it. The added fields could then be automatically added to the generated nexus typings for automatic type safety. I haven't thought a lot about a good api to realise this. In my case it could look something like this: import { registerPlugin } from 'nexus'
registerPlugin({
objectType: {
rule: {
type: '' // Somehow get the type info here.
onRegister: config => {
// Do something with the added field here.
}
}
}) |
Check out #143 - it's an initial pass at solving the problem of defining custom definition block fields. |
@tgriesser I just checked out To illustrate better my needs, I can give you an example of what I would like to achieve. stripe-graphql/src/StripeObjectType.ts import { dynamicOutputMethod } from "nexus";
import generatedTypes from "../generated/mapped-types";
interface TypeFields {
[key: string]: string | any;
}
interface Types {
[key: string]: TypeFields;
}
const types: Types = generatedTypes;
interface Field {
name: string;
alias: string;
}
interface StripeObjectType {
name: string;
fields: Array<string | Field>;
}
export const StripeObjectType = dynamicOutputMethod({
name: "stripeObjectType",
typeDefinition: `(opts: {
name: string,
fields: Array<string | {
name: string;
alias: string;
}>
}): void`,
factory({ typeDef: t, args: [{ name, fields }] }) {
if (fields && fields.includes("*")) {
fields = Object.keys(types[name]).map(key => key);
}
fields.forEach(field => {
if (typeof field === "string") {
t.field(field, { type: types[name][field] });
} else if (field && typeof field === "object") {
t.field(field.alias, {
type: types[name][field.name],
resolve: root => root[field.name]
});
}
});
}
}); app/src/graphql/Query.ts import { objectType, stringArg } from "nexus";
import { StripeObjectType as SOT } from "stripe-graphql";
export const StripeObjectType = SOT;
export const Query = objectType({
name: "Query",
definition(t) {
// I DON'T WANT THIS! Time is gold.
t.string("customer", {
args: {
id: stringArg()
},
async resolve(root, args, ctx) {
return await stripe.customer({ id });
}
});
// I WOULD LIKE SOMETHING LIKE THIS! Save me time and I will love you forever!
// Expose all fields in `Query`.
t.stripeObjectType({
name: "Query",
fields: ["*"]
});
// Expose only `customer` and `customers` fields in `Query`.
t.stripeObjectType({
name: "Query",
fields: ["customer", "customers"]
});
// Rename `customer` field to `getCustomer` in `Query` and expose it.
t.stripeObjectType({
name: "Query",
fields: [{ name: "customer", alias: "getCustomer" }]
});
// Expose all args for `customer` field in `Query`.
t.stripeObjectType({
name: "Query",
fields: [{ name: "customer", args: ["*"] }]
});
// Rename `id` argument to `customerId` for `customer` field in `Query`.
t.stripeObjectType({
name: "Query",
fields: [
{ name: "customer", args: [{ name: "id", alias: "customerId" }] }
]
});
// Custom field.
t.field("customField", {
...t.stripeType.customers,
resolve(root, args, ctx) {
// Custom implementation
}
});
}
}); Would something like this work? Can I merge parts of other schemes into my app schema? I just want to compose my app schema from third-party schemes easily and save time without having to mirror by myself everything. |
@tgriesser I just found this. This is what I need in Nexus. I would like to merge and extend my schema with other third-party schemes. Is this possible in Nexus today? |
It seems like Hasura is trying to solve the same issue with “remote joins”. Having a similar feature in Nexus would be awesome. Please, let’s consider to prioritize this. We should have a easy way in Nexus to connect different data-sources. https://blog.hasura.io/remote-joins-a-graphql-api-to-join-database-and-other-data-sources/ |
Hey 👋, I'd like give a quick sum-up of the state of what we think plugins could be after some discussions with @tgriesser at GraphQL Conf: In a sense, We think plugins can be used for 3 different use-cases:
What do you think? |
Hello All, I'm also combining multiple GraphQL End Points, two of them being Prisma. But I have two endpoints that are external. I'm using GraphQL Binding to connect to them and access their data in the context, however I can't figure out how to easily integrate them into Nexus. GraphQL-Binding has already created TS files, which I believe I just need to integrate into the makeSchema function: import * as path from "path";
import Query from "./Query";
import { makeSchema } from "nexus";
const schema = makeSchema({
types: [Query],
outputs: {
schema: path.join(
__dirname,
"../../generated/ordData/schema.graphql"
),
typegen: path.join(__dirname, "../../generated/ordData/types.ts")
},
typegenAutoConfig: {
sources: [
{
source: path.join(__dirname, "../../generated/types.ts"),
alias: "t"
}
],
contextType: "t.<No idea what to put here>"
}
});
export { schema as default }; I am them combining sachems with: import { mergeSchemas } from "graphql-tools"; Is this even possible? I think that there has be be a way to use Nexus with Graphql-binding and seems like the easiest and best solution to this issue. Thanks for the help in advance. |
Related #200 |
For anyone interested I have started to put together a proposal for my take on plugins here https://www.notion.so/prismaio/Nexus-Plugin-System-cf2025b1f1084e4e98035c2acc2d3723. |
is there support for generating dynamic root types (not just my use-case is that I need to filter an objects nested relation fields by the same parent status. i.e. if I query a notebook this could be solved if
|
Hey @alidcastano thanks for sharing that. Your solution seems reasonable but I've noted your comment to think about later, and if there's a way the underlying API can better serve your use-case. It's interesting. If you're motivated, please open a new feature request for an API that would better serve your use-case. Maybe we'll conclude that your approach is the idiomatic way but I think we need to talk that through more. |
Closing, since Plugins are now possible in the latest 0.12.0-rc.4. Feel free to open additional issues / PRs to improve the docs, they're a little bare bones at the moment. |
I would LOVE to see support to create plugins in Nexus so that we can create plugins like
nexus-prisma
.I think this is the best way to do schema stitching within Nexus.
My current use case, as an example, is with Stripe. I am working on
stripe-graphql
and I would like to use it for some projects within Nexus to do schema stitching but in a more powerful way so that I can save time.This "powerful way" allow us to:
Pros that I see:
nexus-prisma
won't have to maintain its own plugin, just the logic.However, I got a question. How to use more than one plugin for the same object type? (I am not sure if there are important use cases for that.)
How many of you would be up for this? Let's see the support.
The text was updated successfully, but these errors were encountered: