-
-
Save andreyobrezkov/b68011a7bad80170c1fd95ba464286bf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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