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
Related
Background
I'm not sure how I should approach sanitizing data I get from a Java backend for usage in a React form. And also the other way around: sanitizing data I get from a form when making a backend request. For frontend/backend communication we use OpenApi that generates Typescript interfaces and API for us from DTOs defined in Java.
Scenario
Example of the Schema in Java:
public enum Pet {
CAT,
DOG
}
#Schema(description = "Read, create or update an account")
public class AccountDto {
#NotNull
private Boolean active;
#NotBlank
private String userName;
#NotNull
private Pet preferedPet;
#Nullable
private String bioDescription;
// Constructor and getter/setters skipped
}
Current implementation
Example of the generated Typescript interface:
enum Pet {
CAT,
DOG
}
interface AccountDto {
active: boolean,
userName: string,
preferedPet: Pet,
bioDescription?: string // Translates to: string | undefined
}
Example React.js:
import {getAccount, updateAccount, Pet, AccountDto} from "./api"
export default function UpdateAccount() {
const [formData, setFormData] = useState<AccountDto>({
active: true,
userName: "",
preferedPet: Pet.CAT,
bioDescription: ""
})
useEffect(() => {
async function fetchAccount() {
const response = await getAccount();
// Omitted error handling
setFormData(response.data);
// response.data could look like this:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.DOG,
// bioDescription: null
// }
}
}, [])
async function updateAccountHandler() {
const response = await updateAccount(formData);
// Omitted error handling
// Example formData object:
// {
// active: true,
// userName: "John",
// preferedPet: Pet.CAT,
// bioDescription: ""
// }
}
return (
// All input fields
)
}
Problems
When fetching the account, bioDescription is null. React will throw a warning that a component (bioDescription input) is changing from uncontrolled to controlled.
If by any chance there is a situation where null is set for preferedPet we will get a warning that the select value is not valid.
When updating the account all empty strings should be null. Required for the database and generally cleaner in my opinion.
Questions
1.) I'm wondering how other React users prepare/sanitize their data for usage and requests. Is there a go to or good practice I'm not aware of?
2.) Currently I'm using the following function to sanitize my data. It seems to work and Typescript does not notify me about any type mismatches but I think it should since bioDescription can only be string | undefined and not null.
function sanitizeData<T>(data: T, type: "use" | "request"): T {
const sanitizedData = Object.create({});
for (const [key, value] of Object.entries(data)) {
if (!value && type === "use") {
sanitizedData[key] = "";
} else if (!value && type === "request") {
sanitizedData[key] = null;
} else {
sanitizedData[key] = value;
}
}
return sanitizedData;
}
I have a situation where I'm trying to manually change a prop without using the React setState.
formData.description = null;
At this point Typescript is telling me that null is not possible. That's how I detected that my sanitizer function might not be correct.
Demo
Sandbox - https://codesandbox.io/s/async-cdn-7nd2m?file=/src/App.tsx
I have the following hook / query in my frontend, which fetches an some articles and has a filter so it can fetch only the articles that have been proofread or all the articles that have not been:
export default function useArticles(isProofread) {
return useQuery("articles", async () => {
const { articles } = await request(
endpoint,
gql`
query {
articles(isProofread: ${isProofread}) {
id
title
}
}
`
)
return articles
})
}
I do not understand how to fetch all articles though? I am always passing isProofread which will either be true or false. But I struggle to understand how I would just not pass this into my query at all? I have tried not passing the variable into the function, but then undefined will still be passed into the query template, giving me an error.
How can I make this work?
I have the same problem in the backend...
It looks like you're responsible for API, too.
Your API should:
use meaningful names:
an isProofread should be a prop [of entry];
definitely handle undefined - lack of value is a value/state, too:
e.g. if (undefined != args.proofread) filters['proofread'] = args.proofread;
support where variable/arg/condition:
where:{ proofread: { eq: false } };
where:{ proofread: { in: [false, true] } } - equal to undefined;
I have a Dexie.js database with the table "businessLayers" in my React application. I'd like to ensure de data types of the tuples inserted in that table. I thought the method Table.defineClass() would do that, but it does not. My db is the following:
import Dexie from 'dexie';
const db = new Dexie('MyDB');
db.version(1).stores({
businessLayers: '++id, layer'
});
const BusinessLayer = db.businessLayers.defineClass({
id: Number,
layer: String,
values: Object
});
export default db;
I'd like to make not possible to insert an invalid data type on each field. I haven't found any built-in method to do this. Do you know any? Thank you!
Table.defineClass() was an old feature in Dexie 1.x for code completion only - no enforcements. The method should have been deprecated. But the functionality you need can be implemented using a DBCore middleware or creating/updating hooks. DBCore middlware would be the most performant solution as it does not need to verify existing data.
Below is a dry coded full example. Please test and reply if it works. It should support String, Number, Boolean, Array, Object, Set, Map, ArrayBuffer, Uint8Array, etc... and even custom classes. If anyone wants to make a package of this code, please go ahead! I think it could be a nice addon to dexie:
import Dexie from 'dexie';
const db = new Dexie('MyDB');
db.version(1).stores({
businessLayers: '++id, layer'
});
// Use a DBCore middleware "enforceSchema" defined further down...
db.use(
enforceSchema({
businessLayers: {
id: Number,
layer: String,
values: Object
}
}
);
// This is the function that returns the middlware:
function enforceSchema(dbSchema) {
return {
stack: "dbcore",
name: "SchemaEnforcement",
create (downlevelDatabase) {
return {
...downlevelDatabase,
table (tableName) {
const downlevelTable = downlevelDatabase.table(tableName);
const tableSchema = dbSchema[tableName];
if (!tableSchema) return downlevelTable; // No schema for this table.
return {
...downlevelTable,
mutate: req => {
if (req.type === "add" || req.type === "put") {
for (obj of req.values) {
validateSchema(tableName, tableSchema, obj);
}
}
return downlevelTable.mutate(req);
}
}
}
};
}
};
}
function validateSchema(tableName, schema, obj) {
const invalidProp = Object.keys(schema).find(key =>
{
const value = obj[key];
const type = schema[key];
switch (type) {
// Handle numbers, strings and booleans specifically:
case Number: return typeof value !== "number";
case String: return typeof value !== "string";
case Boolean: return typeof value !== "boolean";
// All other types will be supported in the following
// single line:
default: return !(value instanceof type);
}
});
if (invalidProp) {
// Throw exception to abort the transaction and make the
// user get a rejected promise:
throw new TypeError(`Invalid type given for property ${invalidProp} in table ${tableName}. ${schema[invalidProp].name} expected.`);
}
}
My GraphQL query looks like this:
{
p1: property(someArgs: "some_value") {
id
nestedField {
id
moreNestedField {
id
}
}
}
}
On the server side, I'm using Apollo Server.
I have a resolver for the property and other resolvers for nestedField and moreNestedField.
I need to retrieve the value of someArgs on my nested resolvers.
I tried to do this using the context available on the resolver:
property: (_, {someArgs}, ctx) => {
ctx.someArgs = someArgs;
// Do something
}
But this won't work as the context is shared among all resolvers, thus if I have multiple propertyon my query, the context value won't be good.
I also tried to use the path available on info on my nested resolvers. I'm able to go up to the property field but I don't have the arguments here...
I also tried to add some data on info but it's not shared on nested resolvers.
Adding arguments on all resolvers is not an option as it would make query very bloated and cumbersome to write, I don't want that.
Any thoughts?
Thanks!
Params can be passed down to child resolvers using the currently returned value. Additional data will be removed from the response later.
I'll 'borrow' Daniel's code, but without specific params - pass args down as reference (suitable/cleaner/more readable for more args):
function propertyResolver (parent, args) {
const property = await getProperty()
property.propertyArgs = args
return property
}
// if this level args required in deeper resolvers
function nestedPropertyResolver (parent, args) {
const nestedProperty = await getNestedProperty()
nestedProperty.propertyArgs = parent.propertyArgs
nestedProperty.nestedPropertyArgs = args
return nestedProperty
}
function moreNestedPropertyResolver (parent) {
// do something with parent.propertyArgs.someArgs
}
As Daniels stated this method has limited functionality. You can chain results and make something conditionally in child resolver. You'll have parent and filtered children ... not filtered parent using child condition (like in SQL ... WHERE ... AND ... AND ... on joined tables), this can be done in parent resolver.
Do not pass your argument through root, except IDs or parent object, anything from client, use field level argument.
Please check this answer here on how to pass the arguments:
https://stackoverflow.com/a/63300135/11497165
To simplify it, you can put args in your field:
Example Type Definition
Server defination:
type Query{
getCar(color: String): Car
... other queries
}
type Car{
door(color: String): Door // <-- added args
id: ID
previousOwner(offset: Int, limit: Int): Owner // <-- added args
...
}
client query:
query getCar(carId:'123'){
door(color:'grey') // <-- add variable
id
previousOwner(offset: 3) // <-- added variable
... other queries
}
You should be able to access color in your child resolver arguments:
In your resolver:
Car{
door(root,args,context){
const color = args.color // <-- access your arguments here
}
previousOwner(root,args,context){
const offset = args.offset // <-- access your arguments here
const limit = args.limit // <-- access your arguments here
}
...others
}
For your example:
it will be like this
{
p1: property(someArgs: "some_value") { // <-- added variable
id
nestedField(someArgs: "some_value") { // <-- added variable
id
moreNestedField(offset: 5) {
id
}
}
}
}
You can pass the value through the parent field like this:
function propertyResolver (parent, { someArgs }) {
const property = await getProperty()
property.someArgs = someArgs
return property
}
function nestedPropertyResolver ({ someArgs }) {
const nestedProperty = await getNestedProperty()
nestedProperty.someArgs = someArgs
return nestedProperty
}
function moreNestedPropertyResolver ({ someArgs }) {
// do something with someArgs
}
Note that while this works, it may also point to an underlying issue with your schema design in the first place. Depending on how you're resolving these fields (getting them from a database, making requests to another API, etc.), it may be preferable to take a different approach altogether -- for example, by eager loading everything inside the root resolver. Without more context, though, it's hard to make any additional recommendations.
I have a simple page in NEXT.js for example like this:
function Page({ stars }) {
return <div>Next stars: {stars}</div>;
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js');
//console.log(res) shows data at-it-is, -raw
const json = await res.json();
return { stars: json.stargazers_count };
};
export default Page;
and a data-set from API https://directmarketaccess.ru/api/curves/curves/3
in such format which is needed for Google React Chart in exactly the same view at they are like:
[
[header1,..headerX]
...
[valueY,...valueY]
]
It's not an object with {} it's an array of arrays
According to the dataset from Google Charts Demo line chart - array of arrays is fine
I spent almost two days for understanding this case and such tutorials like:
https://nextjs.org/learn/excel/lazy-loading-modules/lazy-loading
https://nextjs.org/learn/basics/fetching-data-for-pages
and searching for answers from google/SO/etc. Probably I don't understand something or missing a trivial thing, but in some cases after importing data with getInititalProps even after console.log(res) stage, I receive error every time when I'm trying to import everything that isn't an object with properties. The funny thing is, that after all the experiments console.log(res) show data correctly, even if server refure to render page for me. So what if necessary data are a string, number, array, but not an object (let skip part where typeof array === object). How to import data in necessary format with or without getInitialProps?
Is there any way to import data on page as-they-are (for example from async function in other file, that connecting to DB (mongo) and receiving data from it) or I always should use fetch from API which should sent me object with {prop: value} model?
You can put any type of data as the property of an object, including an array of arrays.
Just fetch the data, and in getInitialProps, return it as:
function Page({ data }) {
// render your chart here with `data`
}
Page.getInitialProps = async ({ req }) => {
const res = await fetch(GOOGLE_CHARTS_API);
const json = await res.json(); // `json` is your array of arrays of data
return { data: json };
};
export default Page;