Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save andreyobrezkov/b68011a7bad80170c1fd95ba464286bf to your computer and use it in GitHub Desktop.

Select an option

Save andreyobrezkov/b68011a7bad80170c1fd95ba464286bf to your computer and use it in GitHub Desktop.

Revisions

  1. andreyobrezkov revised this gist Jul 24, 2023. 1 changed file with 38 additions and 25 deletions.
    63 changes: 38 additions & 25 deletions PgGroupedAttributesPlugin.ts
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,6 @@
    import type { GrafastFieldConfig } from "postgraphile/grafast";
    import type { PgSelectSingleStep, PgCodecWithAttributes } from "@dataplan/pg";
    import "postgraphile";
    declare global {
    namespace GraphileBuild {
    interface PgCodecTags {
    @@ -9,43 +12,46 @@ declare global {
    // type, and `address` group might produce `UserAddress` grouped type name
    groupedTypeName(details: {
    codec: PgCodecWithAttributes;
    group: string;
    group: string | boolean;
    }): string;

    // Determines the name of the field which exposes the groupedTypeName.
    groupedFieldName(details: {
    codec: PgCodecWithAttributes;
    group: string;
    group: string | boolean;
    }): string;

    // Our inflector to pick the name of the attribute added to the group.
    groupColumn(details: {
    codec: PgCodecWithAttributes;
    group: string;
    group: string | boolean;
    attributeName: string;
    }): string;
    }
    interface ScopeObject {
    // Scope data so other plugins can hook this
    pgAttributeGroup?: string;
    pgAttributeGroup?: string | boolean;
    }
    }
    }
    const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    name: "PgGroupedAttributesPlugin",
    version: "0.0.0",


    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);
    return this.camelCase(group.toString());
    },
    groupColumn(options, { codec, group, attributeName }) {
    const remainderOfName = attributeName.substring(
    group.length + "_".length,
    group.toString().length + separator.length,
    );
    return this.camelCase(remainderOfName);
    },
    @@ -62,7 +68,7 @@ const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    // 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}_`));
    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
    @@ -77,35 +83,42 @@ const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    for (const [codecName, codec] of Object.entries(
    build.input.pgRegistry.pgCodecs,
    )) {
    if (!codec.attributes) continue;
    const groupsRaw = codec.extensions?.tags?.group;
    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 (const group of groups) {
    const attributes = Object.entries(codec.attributes).filter(
    ([attributeName]) => attributeName.startsWith(`${group}_`),
    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 ${codec.name} uses @group smart tag to declare group '${group}', but no attributes with names starting '${group}_' were found.`,
    `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,
    });

    const groupTypeName = build.inflection.groupedTypeName({
    codec: codec as PgCodecWithAttributes,
    group: group
    });


    build.registerObjectType(
    groupTypeName,
    { pgCodec: codec, pgAttributeGroup: group },
    { pgCodec, pgAttributeGroup:group },
    () => ({
    fields: attributes.reduce(
    (memo, [attributeName, attribute]) => {
    const fieldName = build.inflection.groupColumn({
    codec: codec as PgCodecWithAttributes,
    group,
    codec: pgCodec,
    group: group,
    attributeName,
    });

    const resolveResult = build.pgResolveOutputType(
    attribute.codec,
    attribute.notNull || attribute.extensions?.tags?.notNull,
    @@ -156,6 +169,7 @@ const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    return fields;
    }
    const codec = pgCodec as PgCodecWithAttributes;

    const groupsRaw = codec.extensions?.tags?.group;
    if (!groupsRaw) return fields;
    const groups = Array.isArray(groupsRaw) ? groupsRaw : [groupsRaw];
    @@ -168,7 +182,7 @@ const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    const typeName = build.inflection.groupedTypeName({ codec, group });
    const type = build.getOutputTypeByName(typeName);
    const attributes = Object.entries(codec.attributes).filter(
    ([attributeName]) => attributeName.startsWith(`${group}_`),
    ([attributeName]) => attributeName.startsWith(`${group}${separator}`),
    );
    const someAttributeIsNonNullable = attributes.some(
    ([name, attr]) => attr.notNull,
    @@ -189,4 +203,3 @@ const PgGroupedAttributesPlugin: GraphileConfig.Plugin = {
    },
    },
    };

  2. @benjie benjie created this gist Jul 21, 2023.
    192 changes: 192 additions & 0 deletions PgGroupedAttributesPlugin.ts
    Original file line number Diff line number Diff line change
    @@ -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);
    },
    },
    },
    };