Skip to content

Commit

Permalink
Merge pull request #2416 from graphcommerce-org/feature/custom-attrib…
Browse files Browse the repository at this point in the history
…ute-option

Created a new field for products: `custom_attribute(attribute_code: "attribute_code")` to retrieve attribute option value labels.
  • Loading branch information
paales authored Nov 18, 2024
2 parents 39e364c + 53af256 commit b31491e
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-adults-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/magento-graphql': minor
---

Created a new field for products: `custom_attribute(attribute_code: "attribute_code")` to retrieve attribute option value labels. This field is only available in Magento 2.4.7 and up.
5 changes: 5 additions & 0 deletions .changeset/soft-dogs-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/magento-graphql': minor
---

Added an `attribute`-field to `AttributeValueInterface` to be able to retrieve attribute metadata from the value of an attribute.
4 changes: 2 additions & 2 deletions packages/magento-graphql-rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"@graphcommerce/magento-product": "^9.0.0-canary.98",
"@graphcommerce/magento-search": "^9.0.0-canary.98",
"@graphcommerce/next-config": "^9.0.0-canary.98",
"@graphcommerce/next-ui": "^9.0.0-canary.98"
"@graphcommerce/next-ui": "^9.0.0-canary.98",
"graphql": "^16.0.0"
},
"devDependencies": {
"graphql": "^16.0.0",
"tsx": "^4.16.2"
}
}
31 changes: 31 additions & 0 deletions packages/magento-graphql/mesh/attributeValueResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fragments from '@graphcommerce/graphql/generated/fragments.json'
import type {
AttributeValueInterfaceResolvers,
MeshContext,
Resolvers,
} from '@graphcommerce/graphql-mesh'
import { customAttributeMetadataV2 } from './customAttributeMetadataV2'

type AttributeValueResolver = Pick<AttributeValueInterfaceResolvers<MeshContext>, 'attribute'>

const attributeValueResolver: AttributeValueResolver = {
attribute: {
selectionSet: `{ code }`,
resolve: async (root, _, context) =>
root.attribute ??
(await customAttributeMetadataV2(
{ attribute_code: root.code, entity_type: 'catalog_product' },
context,
)),
},
}

type AttributeValueTypes = NonNullable<
Awaited<ReturnType<AttributeValueInterfaceResolvers['__resolveType']>>
>
const attributeValueTypes = fragments.possibleTypes.AttributeValueInterface as AttributeValueTypes[]
const resolvers: Resolvers = {}
attributeValueTypes.forEach((attributeValueType) => {
if (!resolvers[attributeValueType]) resolvers[attributeValueType] = attributeValueResolver
})
export default resolvers
68 changes: 68 additions & 0 deletions packages/magento-graphql/mesh/customAttributeMetadataV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { MeshContext, CustomAttributeMetadataInterface } from '@graphcommerce/graphql-mesh'

export type CustomAttributeInput = { attribute_code: string; entity_type: 'catalog_product' }

export async function customAttributeMetadataV2(
input: CustomAttributeInput,
context: MeshContext,
): Promise<CustomAttributeMetadataInterface | null> {
const cacheKey = `customAttributeMetadata-${input.entity_type}-${input.attribute_code}`
const cached = await context.cache.get(cacheKey)
if (cached) return cached

if (input.entity_type !== 'catalog_product')
throw Error('Only catalog_product is supported at this moment')

if (
!('customAttributeMetadataV2' in context.m2.Query) ||
typeof context.m2.Query.customAttributeMetadataV2 !== 'function'
)
throw Error('This field is only available in Magento 2.4.7 and up')

const attribute = await context.m2.Query.customAttributeMetadataV2({
context,
key: input,
argsFromKeys: (attributes) => ({ attributes }),
selectionSet: /* GraphQL */ `
{
items {
__typename
code
label
default_value
entity_type
frontend_class
frontend_input
is_required
is_unique
label
... on CatalogAttributeMetadata {
apply_to
is_comparable
is_filterable
is_filterable_in_search
is_html_allowed_on_front
is_searchable
is_used_for_price_rules
is_used_for_promo_rules
is_visible_in_advanced_search
is_visible_on_front
is_wysiwyg_enabled
used_in_product_listing
}
options {
label
is_default
value
}
}
}
`,
valuesFromResults: (res, attributes) =>
attributes.map((attr) => res.items?.find((v) => v?.code === attr.attribute_code)),
})

// Cache for 1 hour
await context.cache.set(cacheKey, attribute, { ttl: 60 * 60 })
return attribute ?? null
}
47 changes: 47 additions & 0 deletions packages/magento-graphql/mesh/customAttributeV2Resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fragments from '@graphcommerce/graphql/generated/fragments.json'
import type { MeshContext, ProductInterfaceResolvers, Resolvers } from '@graphcommerce/graphql-mesh'
import { Kind } from 'graphql'
import { CustomAttributeInput, customAttributeMetadataV2 } from './customAttributeMetadataV2'

type CustomAttributeV2Resolver = Pick<ProductInterfaceResolvers<MeshContext>, 'custom_attributeV2'>

const customAttributeV2Resolver: CustomAttributeV2Resolver = {
custom_attributeV2: {
selectionSet: (fieldNode) => ({
kind: Kind.SELECTION_SET,
selections: (fieldNode.arguments ?? [])
.map((arg) => arg.value)
.filter((value) => value.kind === Kind.STRING)
.map((value) => ({ kind: Kind.FIELD, name: { kind: Kind.NAME, value: value.value } })),
}),
resolve: async (root, { attribute_code: code }, context) => {
const value = String(root[code] ?? '')
const input: CustomAttributeInput = { attribute_code: code, entity_type: 'catalog_product' }
const attribute = await customAttributeMetadataV2(input, context)

if (!attribute || !value) return null

if (
attribute?.frontend_input &&
['SELECT', 'MULTISELECT'].includes(attribute.frontend_input)
) {
const values = attribute.frontend_input === 'SELECT' ? [value] : value.split(',')
const selected_options = values.map((v) => {
const found = attribute.options?.find((o) => o?.value === v || o?.label === v)
if (!found) return null
return { ...found, __typename: 'AttributeSelectedOption' }
})
return { __typename: 'AttributeSelectedOptions', code, selected_options, attribute }
}
return { __typename: 'AttributeValue', code, value, attribute }
},
},
}

type ProductTypes = NonNullable<Awaited<ReturnType<ProductInterfaceResolvers['__resolveType']>>>
const productInterfaceTypes = fragments.possibleTypes.ProductInterface as ProductTypes[]
const resolvers: Resolvers = {}
productInterfaceTypes.forEach((productType) => {
if (!resolvers[productType]) resolvers[productType] = customAttributeV2Resolver
})
export default resolvers
1 change: 1 addition & 0 deletions packages/magento-graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@graphcommerce/graphql": "^9.0.0-canary.98",
"@graphcommerce/prettier-config-pwa": "^9.0.0-canary.98",
"@graphcommerce/typescript-config-pwa": "^9.0.0-canary.98",
"graphql": "^16.0.0",
"next": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
24 changes: 24 additions & 0 deletions packages/magento-graphql/plugins/meshConfigAttrValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig'
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'

export const config: PluginConfig = {
module: '@graphcommerce/graphql-mesh/meshConfig',
type: 'function',
}

export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
prev,
baseConfig,
graphCommerceConfig,
) =>
prev(
{
...baseConfig,
additionalResolvers: [
...(baseConfig.additionalResolvers ?? []),
'@graphcommerce/magento-graphql/mesh/customAttributeV2Resolver.ts',
'@graphcommerce/magento-graphql/mesh/attributeValueResolver.ts',
],
},
graphCommerceConfig,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type Query {
"""
Retrieve EAV attributes metadata.
"""
customAttributeMetadataV2(attributes: [AttributeInput!]): AttributesMetadataOutput!
@deprecated(reason: "Magento >= 2.4.7")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
extend interface AttributeValueInterface {
attribute: CustomAttributeMetadataInterface
}
extend type AttributeValue {
attribute: CustomAttributeMetadataInterface
}
extend type AttributeSelectedOptions {
attribute: CustomAttributeMetadataInterface
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
extend interface ProductInterface {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type SimpleProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type ConfigurableProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type BundleProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type DownloadableProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type VirtualProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
extend type GroupedProduct {
"""
This is the singular version of the custom_attributesV2 and allows selecting a single attribute option value.
Available only after Magento 2.4.7 as it relies on the customAttributesV2 query.
"""
custom_attributeV2(attribute_code: String!): AttributeValueInterface
}
8 changes: 8 additions & 0 deletions packagesDev/next-config/__tests__/interceptors/findPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,14 @@ it('finds plugins', () => {
"targetModule": "@graphcommerce/graphql",
"type": "function",
},
{
"enabled": true,
"sourceExport": "meshConfig",
"sourceModule": "@graphcommerce/magento-graphql/plugins/meshConfigAttrValue",
"targetExport": "meshConfig",
"targetModule": "@graphcommerce/graphql-mesh/meshConfig",
"type": "function",
},
{
"enabled": true,
"sourceExport": "graphqlConfig",
Expand Down
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4087,14 +4087,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@graphcommerce/magento-graphql-rest@workspace:packages/magento-graphql-rest"
dependencies:
graphql: "npm:^16.0.0"
tsx: "npm:^4.16.2"
peerDependencies:
"@graphcommerce/graphql-mesh": ^9.0.0-canary.98
"@graphcommerce/magento-product": ^9.0.0-canary.98
"@graphcommerce/magento-search": ^9.0.0-canary.98
"@graphcommerce/next-config": ^9.0.0-canary.98
"@graphcommerce/next-ui": ^9.0.0-canary.98
graphql: ^16.0.0
languageName: unknown
linkType: soft

Expand All @@ -4106,6 +4106,7 @@ __metadata:
"@graphcommerce/graphql": ^9.0.0-canary.98
"@graphcommerce/prettier-config-pwa": ^9.0.0-canary.98
"@graphcommerce/typescript-config-pwa": ^9.0.0-canary.98
graphql: ^16.0.0
next: "*"
react: ^18.2.0
react-dom: ^18.2.0
Expand Down

0 comments on commit b31491e

Please sign in to comment.