-
-
Notifications
You must be signed in to change notification settings - Fork 577
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
How to implement Type Interface(/Union) ? #1210
Comments
I've try this const { makeExtendSchemaPlugin, gql } = require('graphile-utils');
const addTaskInterface = makeExtendSchemaPlugin(
({ pgSql: sql, graphql: { getNamedType, ...a } }) => {
return {
typeDefs: gql`
type TaskOne {
id: ID!
item: Item
createdAt: Datetime
updatedAt: Datetime
step: TaskStep!
}
type TaskTwo {
id: ID!
option Option
createdAt: Datetime
updatedAt: Datetime
step: TaskStep!
}
union Task = TaskOne | TaskTwo
extend type Query {
getCurrentTask: Task
}
`,
resolvers: {
Task: {
resolveType(task) {
return {
task_one: getNamedType('TaskOne'),
task_two: getNamedType('TaskTwo'),
}[task.step];
},
},
Query: {
getCurrentTask: async (_query, args, { pgClient }, resolveInfo) => {
const {
rows: [task],
} = await pgClient.query(`select * from public.current_task()`);
if (!task.id) {
return null;
}
// task = { id: 11c1caeb-d333-486c-b443-94d9d4daf87d, step: task_one, ...etc }
const [
row,
] = await resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`public.current_task()`
);
/*
console.log({ row }); => { row: {}} // there is a result but no value
To be sure, I tried with
const result = await resolveInfo.graphile.selectGraphQLResultFromTable(sql.fragment`public.task`);
console.log({ result });
{ result: [ {}, {}, {}, {}, {} ] } // there is results but values are not populated
*/
return row;
},
},
},
};
}
);
module.exports = addTaskInterface; As you can see in the comments, results are there but field aren't populate with the values 😥 |
We don't currently support PostgreSQL interfaces/unions because it doesn't currently work with our query planning infrastructure. You can track progress on this feature via: If you're interested in funding development of this feature please reach out. |
ok, finaly did it this way const addTaskInterface = makeExtendSchemaPlugin(
return {
/*...*/
resolvers: {
Task: {
resolveType(task) {
return {
task_one: getNamedType('TaskOne'),
task_two: getNamedType('TaskTwo'),
}[task.step];
},
},
Query: {
getCurrentTask: async (_query, args, { pgClient }) => {
const {
rows: [task],
} = await pgClient.query(`select * from public.current_task()`);
if (!task.id) {
return null;
}
return task;
},
},
TaskOne: {
item: {
resolve: async (todo, args, context, resolveInfo) => {
const [
row,
] = await resolveInfo.graphile.selectGraphQLResultFromTable(
sql.fragment`public.item`,
(tableAlias, queryBuilder) => {
queryBuilder.where(
sql.fragment`${tableAlias}.id = ${sql.value(todo.item_id)}`
);
}
);
return row;
},
}, little bit verbose, but it work. |
That’s going to give you N+1 performance issues, but I’m glad you have a solution you’re happy with. |
Hi @flo-pereira, thanks for the elaborate example! However I don't completely understand what the difference between the two task types is, they both seem to have exactly the same fields? Distinguishing the different kinds of tasks by the |
Hi @ab-pm, you said
Ok, but I dont think I have a way doing so without knowing which step I want. currentTask {
... on TaskOne {
...TaskOneFragment
}
... on TaskTwo {
...TaskOneFragment
}
} |
@flo-pereira But you're using the same currentTask {
__typename
... on TaskOne {
option
createdAt
}
... on TaskTwo {
updatedAt
}
} Otherwise I would suggest to simply query all fields on both kinds of tasks currentTask {
option
createdAt
updatedAt
step
} and then distinguish them by their |
that's a mistake, I'm looking for different fields currentTask {
... on TaskOne {
...TaskOneFragment
}
... on TaskTwo {
...TaskTwoFragment
}
} That's is just a simple code to illustrate what I'm looking for. Each Fragment have differents fields which need different joins in the database, so I dont want to query fields that are not needed. |
Wow, yesterday I've written a custom plugin that does build a (specific) union type with complete lookahead support! It does require some ugly hacks, but also shows off how flexible Postgraphile is. We've got a table that represents the CREATE TABLE public.assignee
(
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id integer UNIQUE,
usergroup_id integer UNIQUE,
CONSTRAINT assignee_fk_user_id FOREIGN KEY (user_id) REFERENCES public."user" (id),
CONSTRAINT assignee_fk_usergroup_id FOREIGN KEY (usergroup_id) REFERENCES public.usergroup (id),
CONSTRAINT assignee_chck CHECK (num_nonnulls(user_id, usergroup_id) = 1)
)
COMMENT ON CONSTRAINT assignee_pkey ON public.assignee IS '@omit';
COMMENT ON CONSTRAINT assignee_user_id_key ON public.assignee IS '@omit';
COMMENT ON CONSTRAINT assignee_usergroup_id_key ON public.assignee IS '@omit';
COMMENT ON CONSTRAINT assignee_fk_user_id ON public.assignee IS '@omit';
COMMENT ON CONSTRAINT assignee_fk_usergroup_id ON public.assignee IS '@omit'; The import debugFactory from 'debug';
import { GraphileResolverContext, SchemaBuilder } from 'postgraphile';
import { GraphileUnionTypeConfig } from 'graphile-build';
import { SQL } from 'pg-sql2';
import { ResolveTree } from 'graphql-parse-resolve-info';
const debug = debugFactory('graphile-build-pg');
export default function(builder: SchemaBuilder): void {
// Register a GraphQLUnionType for the public.assignee table instead of the usual GraphQLObjectType
// that would normally be generated by the PgTables plugin
builder.hook('init', (initObject, build) => {
const {
graphql,
newWithHooks,
getTypeByName,
} = build;
const assigneeTable = build.pgIntrospectionResultsByKind.class.find(table => table.name == 'assignee' && table.namespaceName == 'public');
if (!assigneeTable)
throw new Error('Could not find public.assignee');
build.pgRegisterGqlTypeByTypeId(assigneeTable.type.id, () => {
const scope = {
__origin: 'Adding table type for public.assignee',
pgIntrospection: assigneeTable,
isPgRowType: assigneeTable.isSelectable,
isPgCompositeType: !assigneeTable.isSelectable,
};
const tableSpec: GraphileUnionTypeConfig<any, GraphileResolverContext> = {
name: 'Assignee',
description: assigneeTable.description || null,
types(_ctx) {
return [
getTypeByName('User'),
getTypeByName('Usergroup')
].map(type => {
if (!type || !graphql.isObjectType(type)) throw new Error("could not find member type for union 'Assignee'");
return type;
});
}
};
return newWithHooks(
graphql.GraphQLUnionType,
tableSpec,
scope
);
}, false);
return initObject;
}, ['PgAssigneesUnion'], ['PgTables']); // PgTables must run after this
// tweak fields that refer to the Assignee union type
builder.hook('GraphQLObjectType:fields:field', (fieldConfig, build, context) => {
const {
graphql,
pgQueryFromResolveData: queryFromResolveData,
pgSql: sql,
} = build;
const {
Self,
scope: {
fieldName,
isPgFieldConnection,
isPgFieldSimpleCollection,
isPgForwardRelationField,
},
addArgDataGenerator,
getDataFromParsedResolveInfoFragment,
} = context;
const unionType = graphql.getNamedType(fieldConfig.type);
if (!(isPgFieldConnection || isPgFieldSimpleCollection || isPgForwardRelationField) || unionType.name != 'Assignee' || !graphql.isUnionType(unionType)) return fieldConfig;
const memberTypes = unionType.getTypes(); // User and Usergroup, from the `tablespec` above
const tablenameByType: { [typename: string]: SQL} = {User: sql.identifier('public', 'user'), Usergroup: sql.identifier('public', 'usergroup')};
const foreignColumnsByType: { [typename: string]: [string, string]} = {User: ['user_id', 'id'], Usergroup: ['usergroup_id', 'id']};
debug('Tweaking %s.%s to fetch concrete values for %s union', Self, fieldName, unionType);
// We add an ArgDataGenerator because those are *always* called when the field is used, regardless of the subfields to be selected
// usually they just add WHERE clauses to the query
// The generator is called from the `getDataFromParsedResolveInfoFragment()` call inside the PgForwardRelationPlugin and similar ones that refer to the public.assignee table
// To access the simplified query fragment (from which to lookahead into fields of concrete types), we need to make it part of the `args` object (see below)
addArgDataGenerator(function evaluateHackyConcretionArgument(args, ReturnType) {
// check whether we are called on the field that returns the union,
// because this generator is also evaluated on the concrete type in the getDataFromParsedResolveInfoFragment call below
if (ReturnType != unionType)
return {}; // #fixme should return null once ts declaration of ArgDataGeneratorFunction allows it
const concreteFragment = args.__concrete as ResolveTree;
return {
pgQuery(queryBuilder) {
const unionTable = queryBuilder.getTableAlias();
const subselects = memberTypes.map(concreteType => {
// the `query` is constructed very similar to that in PgForwardRelationPlugin
const [column, foreignColumn] = foreignColumnsByType[concreteType.name].map(x => sql.identifier(x));
// getting lookahead data for concreteType
const resolveData = getDataFromParsedResolveInfoFragment(
concreteFragment,
concreteType
);
const foreignTableAlias = sql.identifier(Symbol());
const query = queryFromResolveData(
tablenameByType[concreteType.name],
foreignTableAlias,
resolveData,
{
useAsterisk: false,
asJson: true,
},
innerQueryBuilder => {
innerQueryBuilder.parentQueryBuilder = queryBuilder;
// FIXME subscriptions are ignored here, might need selectIdentifiers etc :-/
// select the typename
innerQueryBuilder.select(sql.literal(concreteType.name), '__typename');
// add the join condition (just like in ForwardRelationPlugin)
innerQueryBuilder.where(sql.fragment`${unionTable}.${column} = ${foreignTableAlias}.${foreignColumn}`);
},
queryBuilder.context,
queryBuilder.rootValue
);
return sql.fragment` WHEN ${unionTable}.${column} IS NOT NULL THEN (${query})`;
});
queryBuilder.select(sql.fragment`CASE${sql.join(subselects)} END`, '__concrete');
// unfortunately queryBuilder.selectIdentifiers() has already been called so we cannot just replace the entire selection clause
// queryBuilder.fixedSelectExpression(sql.fragment`CASE${sql.join(subselects)} END`);
}
};
});
// access the `__concrete` field after resolving the field as usual (by alias etc), potentially nested in a list
// FIXME lists nested in lists are not supported
const getConcrete = graphql.isListType(graphql.getNullableType(fieldConfig.type))
? (vals: any[]) => (console.log(vals), vals?.map(val => val?.__concrete))
: (val: any) => val?.__concrete;
return {
...fieldConfig,
resolve(...args) {
return getConcrete(fieldConfig.resolve!(...args));
}
};
});
// hack into `simplifyParsedResolveInfoFragmentWithType` to make the original parsed fragment
// available on the `args` object for all union types
builder.hook('build', build => {
const {
simplifyParsedResolveInfoFragmentWithType: simplifyResolveInfo,
graphql,
} = build;
return Object.assign(build, {
simplifyParsedResolveInfoFragmentWithType(fragment: ResolveTree, type: import('graphql').GraphQLType) {
const res = simplifyResolveInfo(fragment, type);
if (graphql.isUnionType(type))
res.args = {
...res.args,
__concrete: fragment
};
return res;
}
});
});
} |
Thanks for this example, but when I try it, I get the following error (with or without other plugins or configuration settings). I'm troubleshooting this now, but if you have insight, I would appreciate it, thanks
|
I got this error too |
In case someone stumbles across this question, something like this worked for me: import { Plugin } from 'postgraphile'
export const UnionPlugin: Plugin = (builder, options) => {
builder.hook(
'build',
(input, build, context) => {
const original = input.newWithHooks
build.addType(new build.graphql.GraphQLObjectType({
name: 'SomeTypeA',
fields: {
url: {
type: build.graphql.GraphQLString,
async resolve(parent){
// parent has all direct fields from the database result
return 'https://postgraphile.org/'
}
}
}
}))
// override the newWithHooks method (hacky)
build.newWithHooks = function (...args: any[]) {
const [type, config, scope] = args
if (config?.name === 'YourTypeThatShouldBeAUnion') {
// Our original base type
original.apply(this, [type, { ...config, name: 'YourTypeThatShouldBeAUnionBase' }, scope])
// And the union we want to create
return original.apply(this, [
build.graphql.GraphQLUnionType,
{
name: 'YourTypeThatShouldBeAUnion',
types: [build.getTypeByName('YourTypeThatShouldBeAUnionBase'), build.getTypeByName('SomeTypeA')],
resolveType(args: any) {
// decide based on args what type to return (contains all fields of the row result)
return 'SomeTypeA'
},
},
scope,
])
}
return original.apply(this, args)
}
return build
},
[],
['PgTables']
)
} |
What an interesting approach! |
Polymorphism is fully supported in PostGraphile V5, including a number of approaches (and support in the underlying engine to let you do it other ways too); read more: https://dev.to/graphile/intro-to-postgraphile-v5-part-5-polymorphism-40b4 |
Hi, I'm submitting a
PostGraphile version: 4.5.0
Hi have a table like this :
I can make query like
And it work like it's supposed to be.
But I need another behaviour and I dont know how to do it.
I need the same currentTask function to return a kind of TaskInterface depending on
todo.step
which I could query like below:What is the best way to do this ?
Thanks.
The text was updated successfully, but these errors were encountered: