Modifying properties fetched with Apollo GraphQL in vueJs - javascript

I'm looking to format a date fetched via Apollo-GraphQL using the javascript Moment library. I'm using VueJS Apollo on the client side to make graphQL queries as such:
import { ALL_EVENTS } from '../constants/graphql.js'
import Event from './Event'
import moment from 'moment'
export default {
name: 'EventList',
data: () => ({
events: [],
loading: 0
}),
apollo: {
events: ALL_EVENTS
},
components: {
Event
},
The apollo middleware returns a list of objects that contain an id, name, a startDate (UTC formatted string)
, and an endDate (also a UTC formatted string) property, among other properties added by apollo for it's use.
Whenever I try to make a computed property in VueJS using the object list from Apollo, it gives me a read-only error, but it seems to me like I'm creating a new object:
computed: {
eventsFormatted: function () {
var out = this.events
for (var i in out) {
out[i].startDate = moment(out[i].startDate)
out[i].endDate = moment(out[i].endDate)
}
return out
}
}
What do I need to do to make a duplicate of the array that I can modify?

When you do var out = this.events the out var is now pointing to the same array of objects as this.events.
So if this.events is read-only, you'll need to make a copy of the array and if each object in the array is read-only you'll need to make a copy of each object as well.
Here's an example using the spread operator to copy the array and objects:
computed: {
eventsFormatted() {
let out = [...this.events];
for (let i in out) {
let item = {...out[i]};
item.startDate = moment(item.startDate);
item.endDate = moment(item.endDate);
out[i] = item;
}
return out;
}
}
The above makes a shallow copy of each object in the array. If you need to make a deep copy because of the structure of this.events, see this post.

Related

Why do I lose data stored in an array, which is a field of an object when I try to access it by

this has really bugged me. I have an array of objects. The objects have string arrays within them and a string based on this interface
export interface Subscription {
uid: string;
books: Array<string>;
}
the issue is that when I pull my data from firebase's firestore, I would get an empty string when I try to access the books array.
The array returns empty but the string stays intact, when I use array.find to get the specific object I want to use.
Here is my code in my ngOnInit block
ngOnInit() {
this.afAuth.currentUser.then((account) => {
this.userId = account.uid;
});
this.booksService.subs.subscribe((subbed) => {
this.subscribers = subbed;
console.log(this.subscribers);
const subscriberInfo = this.subscribers.find(
(sub) => sub.uid === this.userId
);
console.log(subscriberInfo);
});
}
I pull the data from firebase and the whole array of objects logs in the console with the values intact. I lose the values when trying to find the one object I need.
Basically what #epascarello said.
Try something like:
ngOnInit() {
this.afAuth.currentUser.then((account) => {
this.userId = account.uid;
this.booksService.subs.subscribe((subbed) => {
this.subscribers = subbed;
console.log(this.subscribers);
const subscriberInfo = this.subscribers.find(
(sub) => sub.uid === this.userId
);
console.log(subscriberInfo);
});
});
}

How to combine multiple stores as Root Store in Mobx and get access of in each other's fields and functions

I don't know how to combine two stores in one RootStore (using Typescript).
For example, I have liveImageStore.ts (with methods which request image, also holds array of last 10 image's urls) and notificationStore.ts (there are some logic to set/clear notification for further using throughout in whole application. And request in liveImageStore.ts provide some errors (ex., in case of http connection troubles). I would like to call function from notificationStore.ts (setNotification which push in store new notification) in request method from first store. But how to forward and type it all?
Notifier component show message use notificationStore.
This is how I'm using the root store pattern:
class RootStore {
childStoreOne: ChildStoreOne
childStoreTwo: ChildStoreTwo
constructor() {
this.childStoreOne = new ChildStoreOne(this)
this.childStoreTwo = new ChildStoreTwo(this)
}
}
class ChildStoreOne {
root: RootStore
constructor(root: RootStore) {
this.root = root
}
methodOne() {}
}
class ChildStoreTwo {
root: RootStore
constructor(root: RootStore) {
this.root = root
}
getSomethingFromStoreOne() {
this.root.childStoreOne.methodOne()
}
}
And this is the React part:
// holds a reference to the store (singleton)
let store: RootStore
// create the context
const StoreContext = createContext<RootStore | undefined>(undefined);
// create the provider component
function RootStoreProvider({ children }: { children: ReactNode }) {
//only create the store once ( store is a singleton)
const root = store ?? new RootStore()
return <StoreContext.Provider value={root}>{children}</StoreContext.Provider>
}
// create the hook
function useRootStore() {
const context = useContext(StoreContext)
if (context === undefined) {
throw new Error("useRootStore must be used within RootStoreProvider")
}
return context
}
Just make sure you don't access other stores in the constructor functions, because of the initialization order some child store might not be created at that point.
I've written a post about this pattern a while ago:
Mobx root store pattern with react hooks you will also find a link to a demo project with Nextjs.

Methods missing from Javascript Class inside Vuex state

I'm using Nuxt framework alongside Vuex to store data in my web site but I'm facing trouble when I want to use a class directly in the state.
With a model cart.js defined like this:
export class Cart {
constructor(ownedID) {
this._created = new Date();
this._lastUpdated = new Date();
this._ownerID = ownedID || 'visitor'
this._items = []
}
getItem (articleNumber) {
console.log(this._items)
}
...
}
And my store's module cart.js
import { Cart } from "~/models/cart";
const state = () => ({
cart: new Cart()
})
const mutations = {
ADD_ITEM(state, newItem) {
console.log(state.cart)
}
}
...
When the ADD_ITEM(state, newItem) mutation is called the getItem(articleNumber) function is missing and thus I receive the TypeError: state.cart.getItem is not a function error.
This is the result of the console.log:
__ob__: Object { value: {…}, dep: {…}, vmCount: 0 }
​
_created:
_item:
_lastUpdated:
_ownerID:
This is a sandbox link of my setup.
Nuxt vuex sandbox error
Does anyone have a clue about my issue.
Thank you.
Vue accepts only plain objects & Observes only native object properties, It ignores the prototype properties. According to the Vue documentation
The object must be plain: native objects such as browser API objects and prototype properties are ignored
In your case, your using a class which creates variables in plain object and methods in prototype(__proto__), That's why state is unable to find getItem method. You need to use plain objects instead of classes
.

How can I retreive all the fields from a firebase document to a map or Key: Value pair

need help with retrieving all the fields in a Firestore document.
I have a react app that gets documents from Firestore.
I am using react-redux-firebase for this. Below is part of my code on the individual component
const mapStateToProps = (state, ownProps) => {
console.log(state);
const collectionName = state.firestore.data.collectionName;
return {
data: data
}
}
export default compose(
connect(mapStateToProps),
firestoreConnect([
{ collection: 'collectionName' }
])
)(ComponentName);
To get a specific field in the document, I do
data.fieldName
This works well if you know all the field names. My problem is, the fields are not the same for all documents.
It would therefore help if I could get all the fields of each document and return a map or any other key:value pair that i can display to a list on the page.
In Dart, I could do something like this:
Map<String, dynamic> data() => dartify(jsObject.data());
I dont know how to do it in React. I am new to the library so any assistance will be appreciated.
If you are trying to iterate/map over the key/value pairs in an object, you can use Object.entries(), among many other options available in Object:
const formatted = Object.entries(data).map(pair => {
// do something with key and value here
const key = pair[0]
const value = pair[1]
return `${key}: ${value}`
})

Modifying Response of Graphql before sent out

I am looking for a way to modify the response object of a graphql query or mutation before it gets sent out.
Basically in addition the the data object, I want to have extra fields like code and message.
At the moment I am solving this by adding the fields directly into my GQL schemas take this type definition for example:
type Query {
myItems: myItemResponse
}
type myItemResponse {
myItem: Item
code: String!
success: Boolean!
message: String!
}
The response itself would be look like that:
{
data: {
myItems: {
myItem: [ ... fancy Items ... ],
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
}
}
I find that solution not nice because it overcomplicates things in my FrontEnd.
I would prefer a solution where message code and other Metadata are seperated from the actual data, so something like this:
{
data: {
myItems: [ ... fancy Items ... ],
},
message: 'successfully retrieved fancy Items',
code: <CODE_FOR_SUCCESSFUL_QUERY>
}
With apollo-server I already tried the formatResponse object in the constructor:
const server = new ApolloServer({
...
formatResponse({ data }) {
return {
data,
test: 'Property to test if shown in the FrontEnd',
}
}
...
}
unfortunately that doesn't have the desired effect. Before I use express middlewares I want to ask if there is a possibility to do this via apollo-server out of the box or if I am maybe just missing something in the formatResponse function.
from graphql.org:
A response to a GraphQL operation must be a map.
If the operation encountered any errors, the response map must contain an entry with key errors. The value of this entry is described in the “Errors” section. If the operation completed without encountering any errors, this entry must not be present.
If the operation included execution, the response map must contain an entry with key data. The value of this entry is described in the “Data” section. If the operation failed before execution, due to a syntax error, missing information, or validation error, this entry must not be present.
The response map may also contain an entry with key extensions. This entry, if set, must have a map as its value. This entry is reserved for implementors to extend the protocol however they see fit, and hence there are no additional restrictions on its contents.
To ensure future changes to the protocol do not break existing servers and clients, the top level response map must not contain any entries other than the three described above.
After doing a lot of research I found out that the only allowed top level properties in a graphql responses are data, errors, extensions. Here you can find the regarding Issue in GitHub
GitHub Issue
for my purpose I will probably use the extensions field.
Example data modifier
This function will concat ":OK" suffix on each string in the output object
// Data/output modifier - concat ":OK" after each string
function outputModifier(input: any): any {
const inputType = typeof input;
if (inputType === 'string') {
return input + ':OK';
} else if (Array.isArray(input)) {
const inputLength = input.length;
for (let i = 0; i < inputLength; i += 1) {
input[i] = outputModifier(input[i]);
}
} else if (inputType === 'object') {
for (const key in input) {
if (input.hasOwnProperty(key)) {
input[key] = outputModifier(input[key]);
}
}
}
return input;
}
Solution 1 - Override GraphQL Resolvers
Long story short: you have 3 main types (Query, Mutation, and Subscription).
Each main type has fields with resolvers.
The resolvers are returning the output data.
So if you override the resolvers you will be able to modify the outputs.
Example transformer
import { GraphQLSchema } from 'graphql';
export const exampleTransformer = (schema: GraphQLSchema): GraphQLSchema => {
// Collect all main types & override the resolvers
[
schema?.getQueryType()?.getFields(),
schema?.getMutationType()?.getFields(),
schema?.getSubscriptionType()?.getFields()
].forEach(fields => {
// Resolvers override
Object.values(fields ?? {}).forEach(field => {
// Check is there any resolver at all
if (typeof field.resolve !== 'function') {
return;
}
// Save the original resolver
const originalResolve = field.resolve;
// Override the current resolver
field.resolve = async (source, inputData, context, info) => {
// Get the original output
const outputData: any = await originalResolve.apply(originalResolve.prototype, [source, inputData, context, info]);
// Modify and return the output
return outputModifier(outputData);
};
});
});
return schema;
};
How to use it:
// Attach it to the GraphQLSchema > https://graphql.org/graphql-js/type/
let schema = makeExecutableSchema({...});
schema = exampleTransformer(schema);
const server = new ApolloServer({schema});
server.listen(serverConfig.port);
This solution will work on any GraphQL-JS service (apollo, express-graphql, graphql-tools, etc.).
Keep in min with this solution you will be able to manipulate the inputData too.
Solution 2 - Modify the response
This solution is more elegant, but is implemented after the implementation of the directives and scalar types and can not manipulate the input data.
The specific for the output object is that the data is null-prototype object (no instance methods like .hasOwnProperty(), .toString(), ...) and the errors are locked objects (readonly).
In the example I'm unlocking the error object... be careful with this and do not change the structure of the objects.
Example transformer
import { Translator } from '#helpers/translations';
import type { GraphQLResponse, GraphQLRequestContext } from 'apollo-server-types';
import type { GraphQLFormattedError } from 'graphql';
export const exampleResponseFormatter = () => (response: GraphQLResponse, requestContext: GraphQLRequestContext) => {
// Parse locked error fields
response?.errors?.forEach(error => {
(error['message'] as GraphQLFormattedError['message']) = exampleTransformer(error['message']);
(error['extensions'] as GraphQLFormattedError['extensions']) = exampleTransformer(error['extensions']);
});
// Parse response data
response.data = exampleTransformer(response.data);
// Response
return response;
};
How to use it:
// Provide the schema to the ApolloServer constructor
const server = new ApolloServer({
schema,
formatResponse: exampleResponseFormatter()
});
Conclusion
I'm using both solutions in my projects. With the first you can control the input and the output based on specific access directives in the code or to validate the whole data flow (on any graphql type) .
And second to translate all the strings based on the context headers provided by the user without messing resolvers and the code with language variables.
Those examples are tested on TS 4+ and GraphQL 15 and 16

Categories