Open michaelbromley opened 10 months ago
I think a general purpose list of the most common GraphQl queries can be useful to reduce the needed boilerplate per project.
For example basically every single store wants to have the ability to log out users, so I must copy something similar to the following snippet per store:
export async function logout() {
return gqlClient.request(
graphql(`
mutation logout {
logout {
success
}
}
`)
)
}
which you can then use in your TanStack Query hooks. But these are getting into implementation detail category because we're talking about how people structure their query keys. For example you will want to invalidate locally cached responses similar to this:
export const useLogout = () => {
const client = useQueryClient()
const mutation = useMutation({
mutationFn: logout,
onSuccess: (d) => {
if (d.logout.success) {
client.invalidateQueries({ queryKey: ['activeCustomer'] })
client.invalidateQueries({ queryKey: ['currentCart'] })
// ...
}
},
})
return mutation
}
Providing essential and reusable queries could then actually help with the initial boiler plate but users do have to customize it to their needs as well and dont forget further nuances like Cookies vs Tokens, multiple Channels (vendure-token
) and i18n (languageCode
).
Whats also important is to research how much you could leverage code generation, since there is a real maintenance burden here in keeping the SDK in sync.
This will be convenient. For me, relatively time consuming repetitive tasks are:
gql
s. Perhaps that even already existsA typical CRUD document for me would like this
export const FRAGMENT = gql`
fragment trainingAssetFragment on RsvTrainingAsset {
id
label
type
createdAt
updatedAt
}
`
export const MUTATION_ADD_ITEM = gql`
${FRAGMENT}
mutation rsv_addTrainingAsset($input: RsvTrainingAssetInput!) {
rsv_addTrainingAsset(input: $input) {
... on RsvTrainingAsset {
...trainingAssetFragment
}
... on ErrorResult {
errorCode
message
}
... on ValidationError {
fieldErrors
}
}
}
`
export const MUTATION_UPDATE_ITEM = gql`
${FRAGMENT}
mutation rsv_updateTrainingAsset($input: RsvTrainingAssetInput!, $id: ID!) {
rsv_updateTrainingAsset(input: $input, id: $id) {
... on RsvTrainingAsset {
...trainingAssetFragment
}
... on ErrorResult {
errorCode
message
}
... on ValidationError {
fieldErrors
}
}
}
`
export const MUTATION_REMOVE_ITEM = gql`
mutation rsv_removeTrainingAsset($id: ID!) {
rsv_removeTrainingAsset(id: $id)
}
`
export const QUERY_ITEM_FORM = gql`
${FRAGMENT}
query rsv_trainingAssetForm($id: ID!) {
rsv_trainingAsset(id: $id) {
...trainingAssetFragment
# more fields here for admin edit form style forms
}
}
`
export const QUERY_ITEM = gql`
${FRAGMENT}
query rsv_trainingAsset($id: ID!) {
rsv_trainingAsset(id: $id) {
...trainingAssetFragment
}
}
`
export const QUERY_LIST = gql`
${FRAGMENT}
query rsv_trainingAssets($options: RsvTrainingAssetListOptions) {
rsv_trainingAssets(options: $options) {
items {
...trainingAssetFragment
}
totalItems
}
}
`
I would like the accompanying provider to be generated. It looks like this for me
export const getForm = (
id: string,
options?: QueryOptions,
): Promise<Rsv_TrainingAssetFormQuery> => {
return sdk.rsv_trainingAssetForm({ id }, options)
}
export const get = (
id: string,
options?: QueryOptions,
): Promise<Rsv_TrainingAssetQuery> => {
return sdk.rsv_trainingAsset({ id }, options)
}
export const list = (
options?: RsvTrainingAssetListOptions,
queryOptions?: QueryOptions,
): Promise<Rsv_TrainingAssetsQuery> => {
return sdk.rsv_trainingAssets({ options }, queryOptions)
}
export const add = (
input: RsvTrainingAssetInput,
options?: QueryOptions,
): Promise<Rsv_AddTrainingAssetMutation> => {
return sdk.rsv_addTrainingAsset({ input }, options)
}
export const update = (
id: string,
input: RsvTrainingAssetInput,
options?: QueryOptions,
): Promise<Rsv_UpdateTrainingAssetMutation> => {
return sdk.rsv_updateTrainingAsset({ id, input }, options)
}
export const remove = (
id: string,
options?: QueryOptions,
): Promise<Rsv_RemoveTrainingAssetMutation> => {
return sdk.rsv_removeTrainingAsset({ id }, options)
}
Obviously, a generator would have no way to name add, update, remove, list. But those could perhaps be based on the query names in the CRUD document: rsv_addTrainingAsset etc
This is a brilliant feature and would be great, as long as certain hooks are exposed for our custom functionality so we can generate our own sdk's?
@JamesLAllen can you expand a bit on what you mean by "generate our own sdks"?
Well, similar to the above post, when you generate your sdk I'm sure you'll put together a process for generating new versions based on shop-api changes. I would like to be able to use this same method so we can generate an sdk that includes the custom extensions we've built or plugins we've installed.
You might be able to develop a convention or system like the inferencer model from Refine, like this maybe? https://refine.dev/docs/examples/data-provider/nestjs-query/
Wouldn't it be nice to have a cli that allows you to take graphql doc and generate a provider file containing wrappers for the supplied graphql file. Then you could use that to generate the out of the box functionality you're talking about, potentially in different flavors (apollo, etc), and we could use it to generate our custom providers. Does feel like something that prob exists in the codegen community already
@mschipperheyn that sounds a bit like what the typescript-generic-sdk codegen does. IMO this approach has been superseded by the direct use of TypeDocumentNodes, which eliminates the need for the wrapper methods, because one single query()
method will automatically have all the type info it needs when you pass the code-generated document to it.
Background
When developing a storefront or other client app, some developers like to work with an "SDK" (software development kit). This is typically a package you can install which exposes some TS APIs that allow you to interact with the backend though convenient pre-built methods.
SDK solve a few problems:
sdk.products.findAll()
, and make getting started more accessible.The issue with GraphQL APIs
Usually you see such SDKs backed by a REST-style API, where each method more-or-less corresponds to a resource-plus-verb, e.g.:
This approach does not work with GraphQL for two reasons:
Example:
what fields does the result have? All scalar fields? Do we join any relations? What about custom fields?
If we (the Vendure team) just decide on a "most likely" set of fields & relations to include in a given query, we will probably cover maybe 50% of cases. Good for getting started, sure; but as soon as you get into the weeds of a real storefront project you are likely gonna want to have control over the GraphQL document itself.
Giving up the ability to define your own GraphQL documents negates a lot of the point of even using GraphQL in the first place!
Proposal
I propose an approach to a Vendure SDK which combines the typical benefits of an SDK with the flexibility and power of GraphQL.
It consists of two main parts:
1. A fetch wrapper
The heart of the SDK will be a wrapper around
fetch
which handles all the typical boilerplate for you:2. A set of pre-defined, statically typed GraphQL documents for common tasks
We will expose all common queries and mutations through the SDK package, which will supply them in the form of a TypedDocumentNode, which means the document itself contains full static typing information about any inputs as well as the return type.
Example
Here is how it could look for some typical operations:
Custom queries
These provided documents like
GetProductQuery
, while very convenient, will still suffer the same issues as discussed above: as soon as you need control over the query fields, you cannot use them anymore. However, with this approach, supplying your own custom query is as simple as providing an alternative document.In the most simple case this would be:
It is quite probably that a mature storefront solution would end up replacing most of the default documents with custom ones. So does that negate the whole point of the SDK?
I would argue no, because:
Interop
To be broadly useful, we need to make sure the SDK can be easily used with existing popular tools. Let's take React for example: probably the most popular technology for building storefronts right now.
TanStack Query
A popular library for data fetching is TanStack Query.
Following their GraphQL example, a fully type-safe GraphQL query using our SDK would look like this:
Apollo Client
Apollo Client provides a lot more than just fetching - specifically the normalized cache is one of the main selling points. Could we combine the benefits of our SDK with Apollo Client?
I suspect that the SDK package would need to expose a specific adapter which essentially encapsulates the required configuration from our apollo client docs into an easy-to-use "link":
Feedback
I'd love to hear your thoughts on this topic:
Thanks for reading!