Hi, I'm submitting a
PostGraphile version: 4.5.0
Hi have a table like this :
create table public.user (
id uuid primary key default uuid_generate_v4(),
name text
);
create table public.item (
id uuid primary key default uuid_generate_v4(),
description text
);
create table public.option (
id uuid primary key default uuid_generate_v4(),
name text,
active boolean
);
create type public.task_step as enum (
'task_one',
'task_two'
);
create table public.todo (
id uuid primary key default uuid_generate_v4(),
user_id uuid references public.user(id),
item_id uuid references public.item(id),
option_id uuid references public.option(id),
state public.task_step not null,
created_at timestamp default now(),
updated_at timestamp default now(),
);
create function public.current_task() returns public.todo as $$
select todo.*
from public.todo as todo
where todo.user_id = current_user_id()
order by todo.updated_at desc
limit 1
$$ language sql stable;
I can make query like
currentTask {
item {
description
}
step
updatedAt
}
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:
currentTask {
... on TaskOne {
...TaskOneFragment
}
... on TaskTwo {
...TaskOneFragment
}
}
What is the best way to do this ?
Thanks.
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:
https://github.com/graphile/postgraphile/issues/387
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 step enum should be simpler than distinguishing them by their GraphQL type (__typename).
Hi @ab-pm, you said
Distinguishing the different kinds of tasks by the step enum
Ok, but I dont think I have a way doing so without knowing which step I want.
And in my case, I query for a task without knowing its step
currentTask {
... on TaskOne {
...TaskOneFragment
}
... on TaskTwo {
...TaskOneFragment
}
}
@flo-pereira But you're using the same TaskOneFragment in both cases, and on both types you have the same fields (id, option, createdAt, updatedAt, and step)? Or are you really looking to query different fields, like in
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 step.
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 Assignee union and references either a user or a usergroup to be the concrete object, so whenever there's a relation to an assignee row by its id, you get back either a User or a Usergroup (and never have to deal with assignee ids in the GraphQL)
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 assignee table is automatically filled with rows by a trigger when a user or usergroup is inserted.
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;
}
});
});
}
@ab-pm
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
A serious error occurred when building the initial schema. Exiting because `retryOnInitFail` is not set. Error details:
Error: Could not find GraphQL connection type for table 'assignee'
at reduce (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build-pg/src/plugins/PgAllRows.js:73:19)
at Array.reduce (<anonymous>)
at hook (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build-pg/src/plugins/PgAllRows.js:36:42)
at SchemaBuilder.applyHooks (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/SchemaBuilder.js:394:20)
at fields (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/makeNewBuild.js:668:40)
at resolveThunk (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:438:40)
at defineFieldMap (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:625:18)
at GraphQLObjectType.getFields (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:579:27)
at Object.newWithHooks (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/makeNewBuild.js:877:36)
at QueryPlugin/GraphQLSchema/Query (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/plugins/QueryPlugin.js:31:25)
@ab-pm
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
A serious error occurred when building the initial schema. Exiting because `retryOnInitFail` is not set. Error details: Error: Could not find GraphQL connection type for table 'assignee' at reduce (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build-pg/src/plugins/PgAllRows.js:73:19) at Array.reduce (<anonymous>) at hook (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build-pg/src/plugins/PgAllRows.js:36:42) at SchemaBuilder.applyHooks (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/SchemaBuilder.js:394:20) at fields (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/makeNewBuild.js:668:40) at resolveThunk (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:438:40) at defineFieldMap (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:625:18) at GraphQLObjectType.getFields (/Users/rich/Developer/graft-ai/api/node_modules/graphql/type/definition.js:579:27) at Object.newWithHooks (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/makeNewBuild.js:877:36) at QueryPlugin/GraphQLSchema/Query (/Users/rich/Developer/graft-ai/api/node_modules/graphile-build/src/plugins/QueryPlugin.js:31:25)
I got this error too
Most helpful comment
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
Assigneeunion and references either auseror ausergroupto be the concrete object, so whenever there's a relation to anassigneerow by its id, you get back either aUseror aUsergroup(and never have to deal with assignee ids in the GraphQL)The
assigneetable is automatically filled with rows by a trigger when a user or usergroup is inserted.