|
|
@@ -0,0 +1,192 @@ |
|
|
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; |
|
|
}): string; |
|
|
|
|
|
// Determines the name of the field which exposes the groupedTypeName. |
|
|
groupedFieldName(details: { |
|
|
codec: PgCodecWithAttributes; |
|
|
group: string; |
|
|
}): string; |
|
|
|
|
|
// Our inflector to pick the name of the attribute added to the group. |
|
|
groupColumn(details: { |
|
|
codec: PgCodecWithAttributes; |
|
|
group: string; |
|
|
attributeName: string; |
|
|
}): string; |
|
|
} |
|
|
interface ScopeObject { |
|
|
// Scope data so other plugins can hook this |
|
|
pgAttributeGroup?: string; |
|
|
} |
|
|
} |
|
|
} |
|
|
const PgGroupedAttributesPlugin: GraphileConfig.Plugin = { |
|
|
name: "PgGroupedAttributesPlugin", |
|
|
version: "0.0.0", |
|
|
|
|
|
inflection: { |
|
|
add: { |
|
|
groupedTypeName(options, { codec, group }) { |
|
|
return this.upperCamelCase(`${this.tableType(codec)}-${group}`); |
|
|
}, |
|
|
groupedFieldName(options, { codec, group }) { |
|
|
return this.camelCase(group); |
|
|
}, |
|
|
groupColumn(options, { codec, group, attributeName }) { |
|
|
const remainderOfName = attributeName.substring( |
|
|
group.length + "_".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, |
|
|
)) { |
|
|
if (!codec.attributes) continue; |
|
|
const groupsRaw = codec.extensions?.tags?.group; |
|
|
if (!groupsRaw) continue; |
|
|
const groups = Array.isArray(groupsRaw) ? groupsRaw : [groupsRaw]; |
|
|
for (const group of groups) { |
|
|
const attributes = Object.entries(codec.attributes).filter( |
|
|
([attributeName]) => attributeName.startsWith(`${group}_`), |
|
|
); |
|
|
if (attributes.length === 0) { |
|
|
console.warn( |
|
|
`Codec ${codec.name} uses @group smart tag to declare group '${group}', but no attributes with names starting '${group}_' were found.`, |
|
|
); |
|
|
continue; |
|
|
} |
|
|
const groupTypeName = build.inflection.groupedTypeName({ |
|
|
codec: codec as PgCodecWithAttributes, |
|
|
group, |
|
|
}); |
|
|
build.registerObjectType( |
|
|
groupTypeName, |
|
|
{ pgCodec: codec, pgAttributeGroup: group }, |
|
|
() => ({ |
|
|
fields: attributes.reduce( |
|
|
(memo, [attributeName, attribute]) => { |
|
|
const fieldName = build.inflection.groupColumn({ |
|
|
codec: codec as PgCodecWithAttributes, |
|
|
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}_`), |
|
|
); |
|
|
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); |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}; |
|
|
|