graphile / crystal

🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!
https://graphile.org/
Other
12.61k stars 571 forks source link

"Gather" plugin example: treating unique indexes similarly to unique constraints #2054

Open MarioSimou opened 6 months ago

MarioSimou commented 6 months ago

Summary

PostGraphile recognizes columns as unique only if they have a unique constraint and not when a unique index is created

While this is not a direct problem with postgraphile, the solution below can help those that use postgraphile in conjunction with prisma. As mentioned here, prisma doesn't set a unique constraint on a column using the @unique directive. Instead, it only creates a unique index. Consequently, postgraphile doesn't recognize those columns as unique, which causes a few issues.

Steps to reproduce

Add the @unique directive in a prisma schema. Also, run the migration against the database.

Expected results

Columns marked with @unique directive to be considered unique by postgraphile.

Actual results

Columns marked with @unique directive are ignored.

Possible Solution

Create this plugin and include it in your graphile config.

// uniqueIndexesPlugin.ts
import type {PgResourceUnique, PgCodecAttributes, PgResourceOptions} from 'postgraphile/@dataplan/pg'
import type {PgAttribute} from 'postgraphile/graphile-build-pg/pg-introspection'

const createUnique = <TAttributes extends PgCodecAttributes = PgCodecAttributes>(
  isPrimary: boolean,
  attributes: PgAttribute[],
  description?: string
): PgResourceUnique<TAttributes> => {
  const names = attributes.map(attribute => attribute.attname)
  const tags = attributes.reduce((tags, attribute) => ({...tags, ...attribute.getTags()}), {})

  return {
    isPrimary,
    attributes: names,
    description,
    extensions: {tags},
  }
}

const isPgAttribute = (attribute: PgAttribute | null): attribute is PgAttribute =>
  Boolean(attribute && attribute._type === 'PgAttribute')

const createUniquesLookup = (options: PgResourceOptions) => {
  const createUniqueId = (unique: PgResourceUnique) => unique.attributes.join('-')

  const hash =
    options.uniques?.reduce<Record<string, true>>(
      (uniques, unique) => ({
        ...uniques,
        [createUniqueId(unique)]: true,
      }),
      {}
    ) ?? {}

  return (unique: PgResourceUnique) => Boolean(hash[createUniqueId(unique)])
}

export const UniqueIndexesPlugin: GraphileConfig.Plugin = {
  name: 'UniqueIndexesPlugin',
  version: '0.0.1',
  gather: {
    hooks: {
      async pgTables_PgResourceOptions(_, event) {
        const relkinds = ['r']
        const {pgClass, resourceOptions} = event

        if (!relkinds.includes(pgClass.relkind)) {
          return
        }
        if (!resourceOptions.uniques) {
          return
        }

        const indexes = pgClass.getIndexes().filter(idx => idx.indisunique)
        const uniquesToAdd: PgResourceUnique[] = []
        const lookup = createUniquesLookup(resourceOptions)

        for (const idx of indexes) {
          const attributes = idx.getKeys()

          if (!attributes.every(isPgAttribute)) {
            continue
          }

          const isPrimary = idx.indisprimary ?? false

          const unique = createUnique(isPrimary, attributes)

          if (lookup(unique)) {
            continue
          }

          uniquesToAdd.push(unique)
        }

        resourceOptions.uniques = [...resourceOptions.uniques, ...uniquesToAdd]
      },
    },
  },
}
benjie commented 6 months ago

Thanks for filling out the example from Discord and submitting it as an example for others! We really appreciate it :raised_hands: