I have created page query for my individual product page.
It does goes to an individual product and I want to add a filter like if multiple product has similar SKU I need to show them. Query works fine in Grphiql interface but in code it only show one node.
Grahiql Query
query ($id: String, $sku: String) {
productsCsv(id: {eq: $id}) {
id
name
productCategory
sizes
sku
stock
instock
description
colors
templateKey
price
discount
fields {
slug
}
}
allProductsCsv(filter: {sku: {regex: $sku}, instock: {eq: "TRUE"}}) {
edges {
node {
id
instock
stock
sku
colors
sizes
}
}
}
}
In query Variables section, I am passing variable like this
{
"sku": "/DW20DK18/"
}
allProductCsv only resulted in one node in gatsby though it returns multiple nodes in graphiql
Gatsby-node.js
const { createFilePath } = require("gatsby-source-filesystem")
const path = require(`path`)
const onCreateNode = ({node,actions,getNode}) => {
const {createNodeField} = actions
if(node.internal.type === `MarkdownRemark`) {
const value = createFilePath({node, getNode})
createNodeField({
name:`slug`,
node,
value
})
}
if(node.internal.type === `ProductsCsv`) {
const value = node.name
const processedSlug = value.replace(/\s+/g, '-').toLowerCase();
createNodeField({
name:`slug`,
node,
value: processedSlug
})
}
}
const createPages = async ({actions,graphql}) => {
const {createPage} = actions
const result = await graphql(`
{
allMarkdownRemark {
edges {
node {
frontmatter {
productColors
age
}
}
}
}
allProductsCsv {
edges {
node {
id
sku
fields{
slug
}
templateKey
productCategory
}
}
}
}
`)
if(result.errors){
console.error(result.errors)
}
result.data.allProductsCsv.edges.forEach(({ node }) => {
console.log(node.productCategory)
createPage({
path: `${String(node.productCategory)}/${node.fields.slug}`,
component: path.resolve(
`src/templates/${String(node.templateKey)}.js`
),
context: {
id: node.id,
sku: `/${node.sku}/`.toString(). //Passing SKU from Here
}
})
})
}
module.exports = {
/*
1. function for how to create page
2. On create node
*/
onCreateNode,
createPages
};
I am passing SKU along side of ID in Context in gatsby-node.js
Your workaround should work except for the fact that you are using a template literal to hold a dynamic regular expression. For this approach I would try to do something like:
context: {
id: node.id,
sku: new RegExp(String.raw `${node.sku}`, 'i'), //add other flags if needed
}
Alternatively try:
new RegExp(node.sku, 'i').toString()
The RegExp constructor should do the trick for this use-case with a little trick to force a raw string (since the comparison within GraphQL needs to be represented by a string value).
Related
I need to be able to receive data from an external API and map it dynamically to classes. When the data is plain object, a simple Object.assign do the job, but when there's nested objects you need to call Object.assign to all nested objects.
The approach which I used was to create a recursive function, but I stumble in this case where there's a nested array of objects.
Classes
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
}
Function to initialize a class
function create(instance: object, data: any) {
for (const [key, value] of Object.entries(instance)) {
if (Array.isArray(value)) {
for (const element of data[key]) {
// get the type of the element in array dynamically
const newElement = new User();
create(newElement, element)
value.push(newElement);
}
} else if (typeof value === 'object') {
create(value, data[key]);
}
Object.assign(value, data);
}
}
const orgWithError = Object.assign(new Organization(), { admin: { id: 'admin-external' }});
console.log(orgWithError.admin.getFullName()); // orgWithError.admin.getFullName is not a function
const org = new Organization();
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
create(org, data);
// this case works because I manually initialize the user in the create function
// but I need this function to be generic to any class
console.log(org.users[0].getFullName()); // "name surname"
Initially I was trying to first scan the classes and map it and then do the assign, but the problem with the array of object would happen anyway I think.
As far as I understand from your code, what you basically want to do is, given an object, determine, what class it is supposed to represent: Organization, Account or User.
So you need a way to distinguish between different kinds of objects in some way. One option may be to add a type field to the API response, but this will only work if you have access to the API code, which you apparently don't. Another option would be to check if an object has some fields that are unique to the class it represents, like admin for Organization or account for User. But it seems like your API response doesn't always contain all the fields that the class does, so this might also not work.
So why do you need this distinction in the first place? It seems like the only kind of array that your API may send is array of users, so you could just stick to what you have now, anyway there are no other arrays that may show up.
Also a solution that I find more logical is not to depend on Object.assign to just assign all properties somehow by itself, but to do it manually, maybe create a factory function, like I did in the code below. That approach gives you more control, also you can perform some validation in these factory methods, in case you will need it
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
static fromApiResponse(data: any) {
const org = new Organization()
if(data.id) org.id = data.id
if(data.admin) org.admin = User.fromApiResponse(data.admin)
if(data.users) {
this.users = org.users.map(user => User.fromApiResponse(user))
}
return org
}
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
static fromApiResponse(data: any) {
const user = new User()
if(data.id) user.id = data.id
if(data.name) user.name = data.name
if(data.account)
user.account = Account.fromApiResponse(data.account)
return user
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
static fromApiResponse(data: any) {
const acc = new Account()
if(data.id) acc.id = data.id
if(data.money) acc.money = data.money
return acc
}
}
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const organization = Organization.fromApiResponse(data)
I can't conceive of a way to do this generically without any configuration. But I can come up with a way to do this using a configuration object that looks like this:
{
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
}
and a pointer to the root node, 'org'.
The keys of this object are simple handles for your type/subtypes. Each one is mapped to an object that has a _ctor property pointing to a constructor function, and a collection of other properties that are the names of members of your object and matching properties of your input. Those then are references to other handles. For an array, the handle is [surrounded by square brackets].
Here's an implementation of this idea:
const create = (root, config) => (data, {_ctor, ...keys} = config [root]) =>
Object.assign (new _ctor (), Object .fromEntries (Object .entries (data) .map (
([k, v]) =>
k in keys
? [k, /^\[.*\]$/ .test (keys [k])
? v .map (o => create (keys [k] .slice (1, -1), config) (o))
: create (keys [k], config) (v)
]
: [k, v]
)))
class Organization {
constructor () { this.id = 'org1'; this.admin = new User(); this.users = [] }
}
class User {
constructor () { this.id = 'user1'; this.name = 'name'; this.account = new Account() }
getFullName () { return `${this.name} surname`}
}
class Account {
constructor () { this.id = 'account1'; this.money = 10 }
calculate () { return 10 * 2 }
}
const createOrganization = create ('org', {
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
})
const orgWithoutError = createOrganization ({ admin: { id: 'admin-external' }});
console .log (orgWithoutError .admin .getFullName ()) // has the right properties
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const org = createOrganization (data)
console .log (org .users [0] .getFullName ()) // has the right properties
console .log ([
org .constructor .name,
org .admin .constructor.name, // has the correct hierarchy
org .users [0]. account. constructor .name
] .join (', '))
console .log (org) // entire object is correct
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function, create, receives the name of the root node and such a configuration object. It returns a function which takes a plain JS object and hydrates it into your Object structure. Note that it doesn't require you to pre-construct the objects as does your attempt. All the calling of constructors is done internally to the function.
I'm not much of a Typescript user, and I don't have a clue about how to type such a function, or whether TS is even capable of doing so. (I think there's a reasonable chance that it is not.)
There are many ways that this might be expanded, if needed. We might want to allow for property names that vary between your input structure and the object member name, or we might want to allow other collection types besides arrays. If so, we probably would need a somewhat more sophisticated configuration structure, perhaps something like this:
{
org: { _ctor: Organization, admin: {type: 'usr'}, users: {type: Array, itemType: 'usr'} },
usr: { _ctor: User, account: {type: 'acct', renameTo: 'clientAcct'} },
acct: { _ctor: Account }
}
But that's for another day.
It's not clear whether this approach even comes close to meeting your needs, but it was an interesting problem to consider.
I am beating my head against a wall. I have updated to Apollo 3, and cannot figure out how to migrate an updateQuery to a typePolicy. I am doing basic continuation based pagination, and this is how I used to merged the results of fetchMore:
await fetchMore({
query: MessagesByThreadIDQuery,
variables: {
threadId: threadId,
limit: Configuration.MessagePageSize,
continuation: token
},
updateQuery: (prev, curr) => {
// Extract our updated message page.
const last = prev.messagesByThreadId.messages ?? []
const next = curr.fetchMoreResult?.messagesByThreadId.messages ?? []
return {
messagesByThreadId: {
__typename: 'MessagesContinuation',
messages: [...last, ...next],
continuation: curr.fetchMoreResult?.messagesByThreadId.continuation
}
}
}
I have made an attempt to write the merge typePolicy myself, but it just continually loads and throws errors about duplicate identifiers in the Apollo cache. Here is what my typePolicy looks like for my query.
typePolicies: {
Query: {
fields: {
messagesByThreadId: {
keyArgs: false,
merge: (existing, incoming, args): IMessagesContinuation => {
const typedExisting: IMessagesContinuation | undefined = existing
const typedIncoming: IMessagesContinuation | undefined = incoming
const existingMessages = (typedExisting?.messages ?? [])
const incomingMessages = (typedIncoming?.messages ?? [])
const result = existing ? {
__typename: 'MessageContinuation',
messages: [...existingMessages, ...incomingMessages],
continuation: typedIncoming?.continuation
} : incoming
return result
}
}
}
}
}
So I was able to solve my use-case. It seems way harder than it really needs to be. I essentially have to attempt to locate existing items matching the incoming and overwrite them, as well as add any new items that don't yet exist in the cache.
I also have to only apply this logic if a continuation token was provided, because if it's null or undefined, I should just use the incoming value because that indicates that we are doing an initial load.
My document is shaped like this:
{
"items": [{ id: string, ...others }],
"continuation": "some_token_value"
}
I created a generic type policy that I can use for all my documents that have a similar shape. It allows me to specify the name of the items property, what the key args are that I want to cache on, and the name of the graphql type.
export function ContinuationPolicy(keyArgs: Array<string>, itemPropertyKey: string, typeName: string) {
return {
keyArgs,
merge(existing: any, incoming: any, args: any) {
if (!!existing && !!args.args?.continuation) {
const existingItems = (existing ? existing[itemPropertyKey] : [])
const incomingItems = (incoming ? incoming[itemPropertyKey] : [])
let items: Array<any> = [...existingItems]
for (let i = 0; i < incomingItems.length; i++) {
const current = incomingItems[i] as any
const found = items.findIndex(m => m.__ref === current.__ref)
if (found > -1) {
items[found] === current
} else {
items = [...items, current]
}
}
// This new data is a continuation of the last data.
return {
__typename: typeName,
[itemPropertyKey]: items,
continuation: incoming.continuation
}
} else {
// When we have no existing data in the cache, we'll just use the incoming data.
return incoming
}
}
}
}
Object1 looks like this { category1: '', category2: '', description: '' }
Category1 has an array field for [category2] and a Name
My firebase contains 3 documents: 1 for object1 1 for Category1 and the last one is not relevant.
In my Ionic application I have made an input field where the user can paste an array of JSON objects. these JSON objects get converted to Object1 and added to the database.
My problem is when category1 is new it should be added to the firebase document for category1 but can't be a duplicate. Or when category2 is new it should update category1.
I think the solution is to make category1 unique Id its property name, so I can check with firebase security rules if it already exists when trying to add it.
Can someone explain to me how to set name as the unique Id of this document or does anyone know a better solution?
in my service:
private category1Collection: AngularFirestoreCollection<Category1>;
private category1s: Observable<Category1[]>;
constructor(db: AngularFirestore) {
this.category1Collection = db.collection<category1>('category1s');
var docRef = db.collection("category1s").doc("category1s");
this.category1s= this.category1Collection.snapshotChanges().pipe(
map(actions => {
return actions.map(a => {
const data = a.payload.doc.data();
const id = a.payload.doc.id;
return { id, ...data };
})
})
)
}
addItem(category1: Category1) {
return this.category1Collection.add(category1);
}
I fixed it:
changed code in the service to:
addItem(category1: Category1) {
return this.category1Collection.doc(category1.Naam).set(category1);
}
and this code in the page.ts:
this.category1Service.getItem(HF.Naam).subscribe(data => {
if (data != null) {
console.log(data)
HF.category2s = data.category2s;
if (!this.containsCategory2(element.category2, HF.category2s)) {
HF.category2s.push(element.category2);
this.category1Service.addItem(HF);
}
} else {
this.category1Service.addItem(HF);
}
});
containsCategory2(category2: string, list: string[]) {
var i;
for (i = 0; i < list.length; i++) {
if (list[i] === category2) {
return true;
}
}
return false;
}
I'm Using Drupal 8 as a headless CMS with Gatsby generating static pages.
In Drupal I have set up some node Types (Article, Image, other...). All entities have brand and category term relationships.
category is a single selection and brand is a multiple selection
Gatsby creates pages from my template on URL's like.
http://example.com/category-1/brand-1/
http://example.com/category-1/brand-2/
http://example.com/category-2/brand-1/
http://example.com/category-2/brand-2/
The articles and image nodes with the terms selected will display on the corresponding url's.
The problem I'm having is since I changed the brand term to a multi selection, some URL's are not being created.
Because of field_brand[0].name Gatsby will only create the URL for the first selection of brand on the article node.
// gatsby-node.js
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
const pageTemplate = path.resolve(`src/templates/pageTemplate.js`);
return graphql(`
{
taxonomyTermBrand {
field_colours
name
}
allNodeImage {
nodes {
relationships {
field_image_drop {
uri {
url
}
}
}
}
}
allNodeArticle {
nodes {
body {
processed
}
relationships {
field_brand {
name
}
field_category {
name
}
}
}
}
}
`, { limit: 1 }).then(result => {
if (result.errors) {
throw result.errors
}
result.data.allNodeArticle.nodes.forEach(node => {
const brand_path = node.relationships.field_brand[0].name;
const category_path = node.relationships.field_category.name;
createPage({
path: `${category_path}/${brand_path}`,
component: pageTemplate,
context: {
brandName: node.relationships.field_brand[0].name,
categoryName: node.relationships.field_category.name,
},
})
})
})
}
Essentially the value of taxonomyTermBrand.name is the same as node.relationships.field_brand[0].name when the data is passed to the template, but I can't use taxonomyTermBrand.name because the path in createPage is in the allNodeArticle.forEach()
Is there a better aproach or another way to set the paths and display the tagged content on those pages?
I think you'd be able to do a forEach over each of the field_brand terms for each of the articles.
Something like:
result.data.allNodeArticle.nodes.forEach(node => {
const brands = node.relationships.field_brand;
const category = node.relationships.field_category.name;
brands.forEach(brand => {
createPage({
path: `${category}/${brand.name}`,
component: pageTemplate,
context: {
brandName: brand,
categoryName: category,
},
})
});
})
I create my state link with defaults values, something like this:
const stateLink = withClientState({
cache,
resolvers,
defaults: {
quote: {
__typename: 'Quote',
name: '',
phoneNumber: '',
email: '',
items: []
}
}
})
So my cache should not be empty. Now my resolvers map looks like this:
resolvers = {
Mutation: { ... },
Query: {
quote: (parent, args, { cache }) => {
const query = gql`query getQuote {
quote #client {
name phoneNumber email items
}
}`
const { quote } = cache.readQuery({ query, variables: {} })
return ({ ...quote })
}
}
}
The datasource of my resolvers is the cache right ? so I have to query the cache somehow. But this is not working, I guess it is because I am trying to respond to quote query, and for that I am making another quote query.
I think I should get the quote data without querying for quote, but how ?
I am getting this error:
Can't find field **quote** on object (ROOT_QUERY) undefined
Please help
Just wanted to post the same question - and fortunatly just figured it out.
readQuery-Methode only allows you to query from root. So instead you should use readFragment, because it allows you to access any normalized field in the cache, as long you got it's id (Something like this: GraphQlTypeName:0 typically constructed from the fields: id and __typename ). Your Query-Resolver should then look something like this:
protected resolvers = {
Query: {
getProdConfig: (parent, args, { cache, getCacheKey }) => {
const id = getCacheKey({ __typename: 'ProdConfig', id: args.id });
const fragment = gql`fragment prodConfig on ProdConfig {
id,
apiKey,
backupUrl,
serverUrl,
cache,
valid
}`;
const data = cache.readFragment({ fragment, id })
return ({ ...data });
}
}
and the call from apollo like:
let query = this.$apollo.query(`
query prodConfig($id: Int!) {
getProdConfig(id: $id) #client {
apiKey,
backupUrl,
serverUrl,
cache,
valid
}
}`,
{ id: 0 }
);