Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save andreyobrezkov/b68011a7bad80170c1fd95ba464286bf to your computer and use it in GitHub Desktop.
Save andreyobrezkov/b68011a7bad80170c1fd95ba464286bf to your computer and use it in GitHub Desktop.
import type { GrafastFieldConfig } from "postgraphile/grafast";
import type { PgSelectSingleStep, PgCodecWithAttributes } from "@dataplan/pg";
import "postgraphile";
declare global {
namespace GraphileBuild {
interface PgCodecTags {
// This enables TypeScript autocomplete for our @group smart tag
group?: string | string[];
}
interface Inflection {
// Our inflector to pick the name of the grouped type, e.g. `User` table
// type, and `address` group might produce `UserAddress` grouped type name
groupedTypeName(details: {
codec: PgCodecWithAttributes;
group: string | boolean;
}): string;
// Determines the name of the field which exposes the groupedTypeName.
groupedFieldName(details: {
codec: PgCodecWithAttributes;
group: string | boolean;
}): string;
// Our inflector to pick the name of the attribute added to the group.
groupColumn(details: {
codec: PgCodecWithAttributes;
group: string | boolean;
attributeName: string;
}): string;
}
interface ScopeObject {
// Scope data so other plugins can hook this
pgAttributeGroup?: string | boolean;
}
}
}
const separator:string = '__';
export const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
name: "PgGroupedAttributesPlugin",
version: "0.0.1",
inflection: {
add: {
groupedTypeName(options, { codec, group }) {
return this.upperCamelCase(`${this.tableType(codec)}-${group}`);
},
groupedFieldName(options, { codec, group }) {
return this.camelCase(group.toString());
},
groupColumn(options, { codec, group, attributeName }) {
const remainderOfName = attributeName.substring(
group.toString().length + separator.length,
);
return this.camelCase(remainderOfName);
},
},
},
schema: {
entityBehavior: {
pgCodecAttribute(behavior, [codec, attributeName], build) {
// const attribute = codec.attributes[attributeName];
// Get the @group smart tag from the codec (table/type) the attribute belongs to:
const groupsRaw = codec.extensions?.tags?.group;
if (!groupsRaw) return behavior;
// Could be that there's multiple groups, make sure we're dealing with an array:
const groups = Array.isArray(groupsRaw) ? groupsRaw : [groupsRaw];
// See if this attribute belongs to a group
const group = groups.find((g) => attributeName.startsWith(`${g}__`));
if (!group) return behavior;
// It does belong to a group, so we're going to remove the "select"
// behavior so that it isn't added by default, instead we'll add it
// ourself.
return [behavior, "-select"];
},
},
hooks: {
// The init phase is the only phase in which we're allowed to register
// types. We need a type to contain our @group attributes.
init(_, build) {
for (const [codecName, codec] of Object.entries(
build.input.pgRegistry.pgCodecs,
)) {
const pgCodec = codec as PgCodecWithAttributes;
if (!pgCodec.attributes) continue;
const groupsRaw = pgCodec.extensions?.tags?.group;
if (!groupsRaw) continue;
const groups = Array.isArray(groupsRaw) ? groupsRaw : [groupsRaw];
for (let group of groups) {
group = group.toString().replace(/\s/g, ""); // remove whitespace
const attributes = Object.entries(pgCodec.attributes).filter(
([attributeName]) => attributeName.startsWith(`${group}${separator}`),
);
if (attributes.length === 0) {
console.warn(
`Codec ${pgCodec.name} uses @group smart tag to declare group '${group}', but no attributes with names starting '${group}${separator}' were found.`,
);
continue;
}
const groupTypeName = build.inflection.groupedTypeName({
codec: codec as PgCodecWithAttributes,
group: group
});
build.registerObjectType(
groupTypeName,
{ pgCodec, pgAttributeGroup:group },
() => ({
fields: attributes.reduce(
(memo, [attributeName, attribute]) => {
const fieldName = build.inflection.groupColumn({
codec: pgCodec,
group: group,
attributeName,
});
const resolveResult = build.pgResolveOutputType(
attribute.codec,
attribute.notNull || attribute.extensions?.tags?.notNull,
);
if (!resolveResult) {
return memo;
}
const [baseCodec, type] = resolveResult;
if (baseCodec.attributes) {
console.warn(
`PgGroupedAttributesPlugin currently doesn't support composite attributes`,
);
return memo;
}
memo[fieldName] = {
description: attribute.description,
type,
plan($record: PgSelectSingleStep) {
return $record.get(attributeName);
},
};
return memo;
},
Object.create(null) as Record<
string,
GrafastFieldConfig<any, any, any, any, any>
>,
),
}),
"Grouped attribute scope from PgGroupedAttributesPlugin",
);
}
}
return _;
},
// Finally we need to use the type we generated above
GraphQLObjectType_fields(fields, build, context) {
const {
scope: {
pgCodec,
isPgClassType,
pgPolymorphism,
pgPolymorphicSingleTableType,
},
} = context;
if (!isPgClassType || !pgCodec?.attributes) {
return fields;
}
const codec = pgCodec as PgCodecWithAttributes;
const groupsRaw = codec.extensions?.tags?.group;
if (!groupsRaw) return fields;
const groups = Array.isArray(groupsRaw) ? groupsRaw : [groupsRaw];
return groups.reduce((fields, group) => {
return build.recoverable(fields, () => {
const fieldName = build.inflection.groupedFieldName({
codec,
group,
});
const typeName = build.inflection.groupedTypeName({ codec, group });
const type = build.getOutputTypeByName(typeName);
const attributes = Object.entries(codec.attributes).filter(
([attributeName]) => attributeName.startsWith(`${group}${separator}`),
);
const someAttributeIsNonNullable = attributes.some(
([name, attr]) => attr.notNull,
);
fields[fieldName] = {
// TODO: description
type: build.nullableIf(someAttributeIsNonNullable, type),
plan($parent) {
// We still represent the same thing - essentially we're
// transparent from a planning perspective.
return $parent;
},
};
return fields;
});
}, fields);
},
},
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment