groqd icon indicating copy to clipboard operation
groqd copied to clipboard

Potential gaps in 0.x to 1.x migration guide

Open Jamiewarb opened this issue 10 months ago • 7 comments

Is there an existing issue for this?

  • [x] I have searched the existing issues

Code of Conduct

  • [x] I agree to follow this project's Code of Conduct

Question

Hi there,

Thanks for the work on this.

I'm inheriting a project from another agency and they're using groqd 0.x.

I'm looking to migrate to 1.x but struggling on a couple areas, and I can't see guidance on migrating them in the current 0.x to 1.x guide.

I wanted to note a few things that seem to be missing, and ask for advice on migrating them:

  • How to migrate from the previously exported sanityImage. Is the expectation to write our own fragment for this now? All good if so, I just can't find an answer in the docs. I looked here but seems there's currently still an issue so wasn't sure what I should be doing
  • I've seen uses of q("").grab$() in the codebase. Does this now become just q.project()?
  • Similarly I've seen .filter(), however filter() requires now 1 parameter—how is this migrated? E.G. from the old docs q("types").filter().deref().grab({ name: q.string() }),
  • This code also imports some types: Selection, InferType and TypeFromSelection, which are no longer exported. What's the migration path for these?

I apologise if any of these should be obvious. I'm new to Groqd.

Jamiewarb avatar Feb 04 '25 22:02 Jamiewarb

I will try to answer some of these because I am also in the middle of migrating.

How to migrate from the previously exported sanityImage

I created a fragment with the fields I need (I mainly need the sanity cdn URL and some metadata dimensions with deref), but you could add any field you need and copy the structure from the sanity type gen.

export const sanityImageFragment = q
  .fragment<{
    asset?: {
      _ref: string
      _type: 'reference'
      _weak?: boolean
      [internalGroqTypeReferenceTo]?: 'sanity.imageAsset'
    }
    _type: 'image'
  }>()
  .project((sub) => ({
    url: sub.field('asset').deref().field('url'),
    metadata: sub
      .field('asset')
      .deref()
      .project({
        width: ['metadata.dimensions.width', q.number()],
        height: ['metadata.dimensions.height', q.number()],
      })
      .notNull(),
  }))

// and then in project
  .project({
    ...
    image: sub.field('image').project(sanityImageFragment),
    ...
  })

q("").grab$()

Will be

q.star.filterByType('something') // or something similar like sub.field() ...
  .project({
    name: q.default(q.string(), "DEFAULT")
  }),

q("types").filter().deref().grab({ name: q.string() }),

q("...") should become .field("...") so the above will be

.project(sub => ({
    types: sub.field("types[]").deref().project({
      name: q.string(),
    }),
  }))

You don't need to .filter() after .field because field will automatically do the groq string [fieldname]. You only need to filter if you want to additionally filter the subquery like

sub
      .field('content[]')
      .filterByType('content-block-name-1', 'content-block-name-2')
      .project({...})

Selection, InferType and TypeFromSelection

A selection is now a fragment and to generate a type from a fragment, you can do

export const pokemonFragment = q
  .fragmentForType<'pokemon'>()
  .project({...})


export type PokemonType = InferFragmentType<typeof pokemonFragment>

If you want to create a type directly from the GroqQueryBuilder you can do something like that, I suppose

const pokemonQuery =   q.star
  .filterByType("pokemon")
  .slice(0, 8)
  .project(sub => ({
    name: q.string(),
    attack: sub.field("base.Attack", q.number()),
    types: sub.field("types[]").deref().project({
      name: q.string(),
    }),
  }))

type Pokemon = InferResultItem<typeof pokemonQuery> // will be a single Item from the Query
type Pokemons = InferResultType<typeof pokemonQuery> // will be Array<Pokemon> (which is the result of the query)

matzexcom avatar Feb 05 '25 10:02 matzexcom

Thank you so much for pointing out these areas. I will definitely consider how to add these to the migration guide! Thank you @matzexcom for your answers, you're doing such a great job of migrating and finding all the new patterns!

One thing I'd like to clarify, if you're doing a "root" level projection via q("").grab({...}) then yes, you can now just do:

q.project({
  products: q.star.filterByType("product"),
  categories: q.star.filterByType("category"),
})

scottrippey avatar Feb 05 '25 15:02 scottrippey

Regarding sanityImage, I'm eager to hear how helpful it would be to recreate this helper?

I was hoping that it would be easy enough (and/or obvious enough) that you could create your own fragment, like:

q.fragment<SanitySchema.Image>().project({
  asset: true,
  ...etc
})

but I'm eager to see how you were using the sanityImage helper, and I'd be happy to port it over!

scottrippey avatar Feb 05 '25 15:02 scottrippey

Thanks for all the help here.

As for the sanityImage helper, I think it was being used fairly standard. I just wasn't clear on the migration path for it as it disappeared from the exports but without a mention in the migration guide. Hope that helps clear that up.

Jamiewarb avatar Feb 06 '25 16:02 Jamiewarb

I would also like to add the nullToUndefined helper is not obvious how to upgrade.

I think we have found the solution using q.string().nullable().transform(val => val ?? undefined)

Perhaps it could also be added to the docs?

scottquested avatar Jun 30 '25 13:06 scottquested

@scottquested Can you share some use-cases for nullToUndefined?

The most common use-case I know of is to provide defaults, so we did add a helper for this, like q.default(q.string(), ""). It's basically the same as q.string().optional().default("") but it works for nulls.

We do have documentation for migrating from grab$ to project by using q.default(), which is similar to this use-case, but perhaps we do need to better document how to migrate away from nullToUndefined too.

scottrippey avatar Jun 30 '25 16:06 scottrippey

@scottrippey So we use it mainly for this type of use case

export const someButtonId = {
  id: nullToUndefined(q.string().optional()),
} satisfies Selection;

Which with the new syntax we think should look like this

export const someButtonId = {
  id: q.string().nullable().transform(val => val ?? undefined)
} satisfies Selection;

scottquested avatar Jul 01 '25 08:07 scottquested