NouanceLabs / payload-better-fields-plugin

This plugin aims to provide you with very specific and improved fields for the Payload admin panel.
MIT License
124 stars 2 forks source link

[Question] How to update queries to use slug instead of id? #50

Closed Evanion closed 6 months ago

Evanion commented 6 months ago

Sorry, I tried looking for a discussion board/section, but couldn't find any so I'm posting here.

Is there any config I can do that will change the default queries that payload generates for each collection, to use the slug field instead of the ID field?

Writing custom query and resolver for each collection feels like a scaling issue for sites with large number of collections. Feels like there must be a more elegant solution, though I'm about 2 hours in to my payload trip so far.

paulpopus commented 6 months ago

So this isn't currently possible, but I believe it's on the roadmap somewhere.

You have two options, first one is to use a fetch with a where query for slug: equals: 'my-value' if that makes sense.

The second option is a custom endpoint, you can wrap it in a plugin function to make it scalabale. Have a look here https://payloadcms.com/docs/plugins/build-your-own You don't need to package your plugin for npm so you can skip a lot of the structure and this would allow you to loop over all collections and add a custom endpoint for each of them. So you only end up writing the code once and then you can re-use it on projects as well!

Given that this isn't related to the plugin I'm going to close this ticket with the information above, if you have further questions you can ping me on the official discord, the usernames the same!

Evanion commented 6 months ago

Just for anyone finding this question if the future, I solved as follows:

findByField.ts

import { Payload } from "payload";
import { GraphQLScalarType } from "graphql";
import { CollectionConfig } from "payload/types";

type Props = {
  GraphQL: any;
  payload: Payload;
  collections: CollectionConfig[];
  field: CollectionConfig["slug"];
  fieldType: GraphQLScalarType<any, any>;
};

export function findByField({
  collections,
  field,
  GraphQL,
  fieldType,
  payload,
}: Props): Record<string, unknown> {
  return collections.reduce((acc, collection) => {
    const { slug } = collection;

    acc[`${singularize(toCamelCase(slug))}By${toPascalCase(field)}`] = {
      type: payload.collections[slug].graphQL?.type,
      args: {
        [field]: {
          type: new GraphQL.GraphQLNonNull(fieldType),
        },
      },
      resolve: async (_parent, args, context) => {
        const value = args[field];
        const document = await payload.find({
          // @ts-ignore
          collection: slug,
          where: {
            [field]: value,
          },
          depth: context.req.depth,
          limit: 1,
          user: context.req.user,
          locale: context.req.locale,
        });

        return document.docs[0];
      },
    };
    return acc;
  }, {});
}

// foo-bar -> FooBar
function toPascalCase(str: string): string {
  return str
    .replace(/\w+/g, (w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
    .replace(/-/g, "");
}

// foo-bar -> fooBar
function toCamelCase(str: string): string {
  return str
    .replace(/\w+/g, (w, i) => {
      if (i === 0) {
        return w.toLowerCase();
      }
      return w[0].toUpperCase() + w.slice(1).toLowerCase();
    })
    .replace(/-/g, "");
}

function singularize(str: string): string {
  if (str.endsWith("s")) {
    return str.slice(0, -1);
  }
  return str;
}

and then in payload.config.ts

export default buildConfig({
  // ...
  graphQL: {
    // ...
    queries: (GraphQL, payload) => ({
      ...findByField({
        collections: [Product],
        field: "slug",
        fieldType: GraphQL.GraphQLString,
        GraphQL,
        payload,
      }),
      ...findByField({
        collections: [Product, Service],
        field: "forSale",
        fieldType: GraphQL.GraphQLBoolean,
        GraphQL,
        payload,
      }),
    }),
  },
  // ...
})

This will automatically add a <collection>By<Field> query for each collection added. This way, you can add multiple instances of findByField to the queries object for different groups of collections that share the same type of field, and to be able to query by that field.

If you define your collections in an array, you could do something like this to automatically add a findByField for any collection that has a specific field.

const collections: CollectionConfig[] = [
  Products,
  Services,
  Users, // this collection wont get a `userBySlug` query
]

export default buildConfig({
  collections,
  // ...
  graphQL: {
    // ...
    queries: (GraphQL, payload) => ({
      ...findByField({
        collections: collections.filter((collection) =>
          collection.fields.some(
            (field) =>
              // @ts-expect-error: We are filtering out fields without a name
              Object.keys(field).includes("name") && field.name === "slug"
          )
        ),
        field: "slug",
        fieldType: GraphQL.GraphQLString,
        GraphQL,
        payload,
      }),
    }),
  },
  // ...
})