There is a plugin on Strapi Marketplace that do this response transforming stuffs in a more configurable way. Checkout this if you are interested.
-
-
Save hucancode/5b495aabf75fc3b940df3e5f94d5b927 to your computer and use it in GitHub Desktop.
// src/middlewares/flatten-response.js | |
function flattenArray(obj) { | |
return obj.map(e => flatten(e)); | |
} | |
function flattenData(obj) { | |
return flatten(obj.data); | |
} | |
function flattenAttrs(obj) { | |
let attrs = {}; | |
for (var key in obj.attributes) { | |
attrs[key] = flatten(obj.attributes[key]); | |
} | |
return { | |
id: obj.id, | |
...attrs | |
}; | |
} | |
function flatten(obj) { | |
if(Array.isArray(obj)) { | |
return flattenArray(obj); | |
} | |
if(obj && obj.data) { | |
return flattenData(obj); | |
} | |
if(obj && obj.attributes) { | |
return flattenAttrs(obj); | |
} | |
return obj; | |
} | |
async function respond(ctx, next) { | |
await next(); | |
if (!ctx.url.startsWith('/api')) { | |
return; | |
} | |
console.log(`API request (${ctx.url}) detected, transforming response json...`); | |
ctx.response.body = { | |
data: flatten(ctx.response.body.data), | |
meta: ctx.response.body.meta | |
}; | |
} | |
module.exports = () => respond; |
// config/middlewares.js | |
module.exports = [ | |
'strapi::errors', | |
'strapi::security', | |
'strapi::cors', | |
'strapi::poweredBy', | |
'strapi::logger', | |
'strapi::query', | |
'strapi::body', | |
'global::flatten-response', | |
'strapi::favicon', | |
'strapi::public', | |
]; |
Hi, thanks for the code. Personally I think it's still counter intuitive. In the graphql code we still have to pick data/attributes, nesting is still scary.
Unfortunately it doesn't seem to work with populate
API request (/api/technologies?populate=*) detected, transforming response json...
[2022-02-06 17:33:24.171] error: Cannot read property 'data' of null
TypeError: Cannot read property 'data' of null
at flatten (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:24:11)
at flattenAttrs (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:12:18)
at flatten (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:28:12)
at flattenData (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:6:10)
at flatten (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:25:12)
at flattenAttrs (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:12:18)
at flatten (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:28:12)
at /home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:2:25
at Array.map (<anonymous>)
at flattenArray (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:2:14)
at flatten (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:22:12)
at respond (/home/allanbraun/projetos/pernonal-site/apps/backend/src/middlewares/flatten-response.js:42:11)
at async /home/allanbraun/projetos/pernonal-site/node_modules/@strapi/strapi/lib/middlewares/body.js:24:7
at async /home/allanbraun/projetos/pernonal-site/node_modules/@strapi/strapi/lib/middlewares/logger.js:22:5
at async /home/allanbraun/projetos/pernonal-site/node_modules/@strapi/strapi/lib/middlewares/powered-by.js:16:5
at async cors (/home/allanbraun/projetos/pernonal-site/node_modules/@koa/cors/index.js:56:32)
I fixed! Its a problem with null values, a simple if check do the trick
function flattenArray(obj) {
return obj.map((e) => flatten(e));
}
function flattenData(obj) {
return flatten(obj.data);
}
function flattenAttrs(obj) {
let attrs = {};
for (let key in obj.attributes) {
attrs[key] = flatten(obj.attributes[key]);
}
return {
id: obj.id,
...attrs,
};
}
function flatten(obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (Array.isArray(obj)) {
return flattenArray(obj);
}
if (obj.data) {
return flattenData(obj);
}
if (obj.attributes) {
return flattenAttrs(obj);
}
return obj;
}
async function respond(ctx, next) {
await next();
if (!ctx.url.startsWith("/api")) {
return;
}
console.log(
`API request (${ctx.url}) detected, transforming response json...`
);
ctx.response.body = {
data: flatten(ctx.response.body.data),
meta: ctx.response.body.meta,
};
}
module.exports = () => respond;
Thanks for the input, I updated mine.
@YassineElbouchaibi Just tried your solution, however, the result is somewhat strange.
Query:
query Company {
company {
data {
attributes {
content
}
}
}
}
Original result:
{
"data": {
"company": {
"data": {
"attributes": {
"content": "foo"
}
}
}
}
}
Normalized:
{
"company": {
"data": {
"attributes": {
"content": "foo"
}
}
},
"data": {
"company": {
"content": "foo"
}
}
}
@YassineElbouchaibi Just tried your solution, however, the result is somewhat strange.
Query:
query Company { company { data { attributes { content } } } }
Original result:
{ "data": { "company": { "data": { "attributes": { "content": "foo" } } } } }
Normalized:
{ "company": { "data": { "attributes": { "content": "foo" } } }, "data": { "company": { "content": "foo" } } }
Hey @SoftCreatR! I've just put data: [queryName]
before the query name and got the expected result, see example below:
By the way, thanks a lot guys for that workaround @hucancode and @YassineElbouchaibi.
Guys, I've changed response body placing ...parsedBody.data
inside the data attr. Doing that I didn't need put data: [queryName]
anymore on graphql playground. I didn't test enough, but so far has been working for me. Could you tell me if doing this might break at some point?
ctx.response.body = {
// ...parsedBody.data,
data: {
...parsedBody.data,
...normalize(parsedBody.data),
},
};
I was facing some issues because this function only flattens the first depth level of the response. I made some tweaks and now it flattens all the depth levels. Specially useful if you work with relationships and using populate
when calling the API:
const strapiFlatten = (data) => {
const isObject = (data) => Object.prototype.toString.call(data) === '[object Object]';
const isArray = (data) => Object.prototype.toString.call(data) === '[object Array]';
const flatten = (data) => {
if (!data.attributes) return data;
return {
id: data.id,
...data.attributes,
};
};
if (isArray(data)) {
return data.map((item) => strapiFlatten(item));
}
if (isObject(data)) {
if (isArray(data.data)) {
data = [...data.data];
} else if (isObject(data.data)) {
data = flatten({ ...data.data });
} else if (data.data === null) {
data = null;
} else {
data = flatten(data);
}
for (const key in data) {
data[key] = strapiFlatten(data[key]);
}
return data;
}
return data;
};
async function respond(ctx, next) {
await next();
if (!ctx.url.startsWith("/api")) {
return;
}
console.log(
`API request (${ctx.url}) detected, transforming response json...`
);
ctx.response.body = {
data: strapiFlatten(ctx.response.body.data),
meta: ctx.response.body.meta,
};
}
module.exports = () => respond;
Thanks, @zimoo354 !!
I was getting an empty object when authenticating with Google auth provider. It was trying to return: { "jwt": "...", "user" {...} }
. Not sure what the best approach would be for this, but I just added an additional check to short circuit the flattening if the response body doesn't have a data
key.
const strapiFlatten = (data) => {
const isObject = (data) => Object.prototype.toString.call(data) === '[object Object]';
const isArray = (data) => Object.prototype.toString.call(data) === '[object Array]';
const flatten = (data) => {
if (!data.attributes) return data;
return {
id: data.id,
...data.attributes,
};
};
if (isArray(data)) {
return data.map((item) => strapiFlatten(item));
}
if (isObject(data)) {
if (isArray(data.data)) {
data = [...data.data];
} else if (isObject(data.data)) {
data = flatten({ ...data.data });
} else if (data.data === null) {
data = null;
} else {
data = flatten(data);
}
for (const key in data) {
data[key] = strapiFlatten(data[key]);
}
return data;
}
return data;
};
async function respond(ctx, next) {
await next();
if (!ctx.url.startsWith("/api") || !ctx.response.body.data) {
return;
}
console.log(
`API request (${ctx.url}) detected, transforming response json...`
);
ctx.response.body = {
data: strapiFlatten(ctx.response.body.data),
meta: ctx.response.body.meta,
};
}
module.exports = () => respond;
Graphql Support [Partially tested!!!]
// src/middlewares/flatten-response.js const fs = require("fs"); const normalize = (data) => { const isObject = (data) => Object.prototype.toString.call(data) === "[object Object]"; const isArray = (data) => Object.prototype.toString.call(data) === "[object Array]"; const flatten = (data) => { if (!data.attributes) return data; return { id: data.id, ...data.attributes, }; }; if (isArray(data)) { return data.map((item) => normalize(item)); } if (isObject(data)) { if (isArray(data.data)) { data = [...data.data]; } else if (isObject(data.data)) { data = flatten({ ...data.data }); } else if (data.data === null) { data = null; } else { data = flatten(data); } for (const key in data) { data[key] = normalize(data[key]); } return data; } return data; }; const fixTypeDefName = (name) => { name = name.replace("RelationResponseCollection", "s"); name = name.replace("EntityResponseCollection", "s"); name = name.replace("EntityResponse", ""); name = name.replace("Entity", ""); return name; }; const fixTypeRefName = (typeDef) => { if ( typeDef.name != null && typeDef.name.endsWith("EntityResponseCollection") ) { typeDef.ofType = { kind: "NON_NULL", name: null, ofType: { kind: "OBJECT", name: typeDef.name.replace("EntityResponseCollection", ""), ofType: null, }, }; typeDef.kind = "LIST"; typeDef.name = null; return typeDef; } if (typeDef.ofType != null) { typeDef.ofType = fixTypeRefName(typeDef.ofType); } if (typeDef.name != null) { typeDef.name = fixTypeDefName(typeDef.name); } return typeDef; }; const fixTypeDef = (typeDef) => { const fixedType = { ...typeDef, name: fixTypeDefName(typeDef.name), }; fixedType.fields = typeDef.fields.map((y) => ({ ...y, type: { ...fixTypeRefName(y.type), }, })); return fixedType; }; const respond = async (ctx, next) => { await next(); // REST API response if (ctx.url.startsWith("/api")) { console.log( `API request (${ctx.url}) detected, transforming response json...` ); ctx.response.body = { ...ctx.response.body, data: normalize(ctx.response.body.data), }; return; } // GraphQL Response for Apollo Codegen script if ( ctx.url.startsWith("/graphql") && ctx.request.headers.apollocodegen === "true" ) { const parsedBody = JSON.parse(ctx.response.body); parsedBody.data.__schema.types = parsedBody.data.__schema.types .filter((x) => !x.name.endsWith("Entity")) .filter((x) => !x.name.endsWith("EntityResponse")) .filter((x) => !x.name.endsWith("EntityResponseCollection")) .map((x) => { if (x.fields == null) return x; if (x.name == null) return x; if (x.name === "Query" || x.name === "Mutation") { return { ...x, fields: x.fields.map((y) => ({ ...y, type: { ...fixTypeRefName(y.type), }, })), }; } return fixTypeDef(x); }); // Uncomment to Debug: Dump parsedBody to a file // fs.writeFileSync("./schema.json", JSON.stringify(parsedBody, null, 2)); ctx.response.body = parsedBody; return; } // GraphQL Response for Apollo Client if ( ctx.url.startsWith("/graphql") && ctx.request.headers.normalize === "true" ) { const parsedBody = JSON.parse(ctx.response.body); if (parsedBody.data.__schema !== undefined) { return; } console.log( `API request (${ctx.url}) detected, transforming response json...` ); ctx.response.body = { ...parsedBody.data, data: normalize(parsedBody.data), }; return; } }; module.exports = () => respond;Apollo Codegen command
yarn apollo codegen:generate --target=typescript --tagName=gql --includes='operations/**/types.ts' --endpoint=http://localhost:1337/graphql --header='apollocodegen: true'index.ts should contain the real query
import { gql } from '@apollo/client'; export const GET_ASSETS_QUERY = gql` query GetAssetsQuery { asset { data { attributes { banner { data { attributes { url } } } logo { data { attributes { url } } } } } } } `; export * from './__generated__/GetAssetsQuery';types.ts the response type
import { gql } from '@apollo/client'; export const GET_ASSETS_QUERY = gql` query GetAssetsQuery { asset { banner { url } logo { url } } } `;And the return types will be generated correctly in your typescript code
This works as expected. With some amount of changes to get Strapi admin to maintain its sanity
A tiny improvement to forward error as they're without tampering with them
function flattenArray(obj) {
return obj.map((e) => flatten(e));
}
function flattenData(obj) {
return flatten(obj.data);
}
function flattenAttrs(obj) {
let attrs = {};
for (let key in obj.attributes) {
attrs[key] = flatten(obj.attributes[key]);
}
return {
id: obj.id,
...attrs,
};
}
function flatten(obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (Array.isArray(obj)) {
return flattenArray(obj);
}
if (obj.data) {
return flattenData(obj);
}
if (obj.attributes) {
return flattenAttrs(obj);
}
return obj;
}
async function respond(ctx, next) {
await next();
if (!ctx.url.startsWith("/api")) {
return;
}
console.log(
`API request (${ctx.url}) detected, transforming response json...`
);
if (ctx.response.status < 400) {
const body = {
data: ctx.response.body.data && flatten(ctx.response.body.data),
meta: ctx.response.body.meta
}
ctx.response.body = body
}
}
module.exports = () => respond;
The flatten function can be improved like this :
function flatten(obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (obj.data) {
return flattenData(obj);
}
if (Array.isArray(obj)) {
return flattenArray(obj);
}
if (obj.attributes) {
return flattenAttrs(obj);
}
if (typeof obj !== 'string') {
for (let key in obj) {
obj[key] = flatten(obj[key]);
}
}
return obj;
}
This ensures deeper flattening, especially when the model si composed of components and dynamiczone content types
None of the scripts above work for GraphQL XD.
I started debugging it for a while and gave up because I was getting annoyed. But I'll share my learnings in case a masochist wants to fx this coursed script.
-
The file should be placed in:
/src/middlewares/flatten-response.ts
. And the config file we're change in is/config/middlewares.ts
. This wasn't obvious to me... -
The Playground is broken, a new if statement needs to be added to skip changing the GET request of the actual HTML:
if (ctx.url.startsWith("/graphql") && ctx.request.method === 'GET' ) return
-
When I was running my
codegen
script the conditionctx.request.body.operationName === "IntrospectionQuery"
wasn't true. This one is:if (ctx.url.startsWith("/graphql") && ctx.request.body.query.startsWith("query IntrospectionQuery") ) {/* code */}
-
The
parsedBody
was not properly cloned, you need to do something like this:const { data, ...restBody } = parsedBody ctx.response.body = { ...restBody, data: normalize(data), };
This is the wrong code it fixes.:
// This is wrong! Don't copy it! ctx.response.body = { ...parsedBody.data, // I don't understand what this was meant to do... seems useless... data: normalize(parsedBody.data), };
-
After all this changes, I realized that the input still needs to use the "old" ugly structure with all the
data
andattributes
. This totally makes no sense, so I quit here.
What I will do: Make a function in the frontend that converts the response to a pretty format, but the GraphQL query will remain ugly. And not waste any more time with middlewares.
It's me again, this is the script I ended up doing. The code is not clean, but the exported methods let the code where it's used be clean. Could be improved because I did it in an afternoon for a new project and didn't use it in a large codebase to find edge cases, but for a basic usage works. Maybe I give updates about the new use cases I find.
Type inference works perfectly.
Basically, you place this file wherever you want in the frontend. Not in strapi backend, and call it after the GraphQL request to parse the results into a readable format while keeping the types.
// src/lib/gql/index.ts <-- You can chose another location
/* eslint-disable @typescript-eslint/no-explicit-any */
export function simplifyResponse<T extends ObjectType>(response: T): SimpleResponse<T> {
const entries = Object.entries(response).filter(([k]) => k !== '__typename')
if (entries.length >= 2) throw new Error('Cannot simplify a Strapi response that contains an object with more than one key')
return simplify(entries[0][1] as any)
}
export function simplify<T extends ValidType>(value: T): SimpleType<T>
export function simplify<T>(value: T) {
if (Array.isArray(value)) return value.map(simplify)
if (isPlainObject(value)) {
if ('data' in value) return simplify(value.data)
if ('attributes' in value) return simplify(value.attributes)
return objectMap(value, simplify)
}
return value
}
function isPlainObject<O extends R | any, R extends Record<string | number | symbol, any>>(obj: O): obj is R {
return typeof obj === 'object' && obj !== null && obj.constructor === Object && Object.getPrototypeOf(obj) === Object.prototype;
}
interface Dictionary<T> {
[key: string]: T;
}
function objectMap<TValue, TResult>(
obj: Dictionary<TValue>,
valSelector: (val: TValue, obj: Dictionary<TValue>) => TResult,
keySelector?: (key: string, obj: Dictionary<TValue>) => string,
ctx?: Dictionary<TValue>
) {
const ret = {} as Dictionary<TResult>;
for (const key of Object.keys(obj)) {
if (key === '__typename') continue;
const retKey = keySelector
? keySelector.call(ctx || null, key, obj)
: key;
const retVal = valSelector.call(ctx || null, obj[key], obj);
ret[retKey] = retVal;
}
return ret;
}
type ValidType = UntouchedType | ObjectType | ArrayType
type UntouchedType = boolean | number | string | symbol | null | undefined | bigint | Date
type ObjectType = { [key in string]?: ValidType }
type ArrayType = ValidType[]
type IsAny<T> = unknown extends T & string ? true : false;
export type SimpleType<T extends ValidType> = IsAny<T> extends true ? any : (T extends UntouchedType ? T
: T extends [...(infer Ar extends ValidType[])] ? { [Index in keyof Ar]: SimpleType<Ar[Index]> }
: T extends { [K in 'data']?: infer Ob extends ValidType } ? SimpleType<Ob>
: T extends { [K in 'attributes']?: infer Ob extends ValidType } ? SimpleType<Ob>
: T extends Omit<ObjectType, 'data' | 'attributes'> ? { [key in Exclude<keyof T, '__typename'>]: SimpleType<T[key]> }
: T)
type IsUnion<T, U extends T = T> = (T extends any ? (U extends T ? false : true) : never) extends false ? false : true
type GetOnlyKeyOrNever<T extends ObjectType, Keys = Exclude<keyof T, '__typename'>> = IsUnion<Keys> extends true ? never : Keys
export type SimpleResponse<T extends ObjectType> = SimpleType<T[GetOnlyKeyOrNever<T>]>
export type NonNullableItem<T extends any[] | null | undefined> = NonNullable<NonNullable<T>[number]>
What does it do? An example
simplifyResponse()
Transforms this:
{
"detailsBeaches": {
"__typename": "DetailsBeachEntityResponseCollection",
"data": [
{
"__typename": "DetailsBeachEntity",
"attributes": {
"__typename": "DetailsBeach",
"name": "Aigua blava",
"basicDetails": {
"__typename": "ComponentPlaceDetailsBasicDetails",
"shortDescription": "Lorem ipsum...",
"cover": {
"__typename": "UploadFileEntityResponse",
"data": {
"__typename": "UploadFileEntity",
"attributes": {
"__typename": "UploadFile",
"url": "/uploads/Aiguablava_19_ecbb012937.jpg",
"height": 768,
"width": 1413
}
}
}
}
}
}
]
}
}
Into this:
[
{
"name": "Aigua blava",
"basicDetails": {
"shortDescription": "Lorem ipsum...",
"cover": {
"url": "/uploads/Aiguablava_19_ecbb012937.jpg",
"height": 768,
"width": 1413
}
}
}
]
Notice that the first object with only one key gets "unwraped", in this case the key
detailsBeaches
is gone.
And automatically infers proper types. 🎉
simplify()
does the same, but doesn't remove the first object key.
The exported utility types are:
SimpleResponse
: Return type ofsimplifyResponse()
functionSimpleType
: Return type ofsimplify()
functionNonNullableItem
: Used to access the first item of a response that returns a list and removes nulls/undefineds.NonNullable
: Well, this is not exported, and it's native to typescript, but is useful. Removes nulls/undefineds.
Usage example
GraphQL query returns an array, but we just want the first item:
// /src/app/beaches/[slug]/page.tsx <-- Just an example, yours will be different
import { GetBeachQuery, graphql, gqlClient } from 'src/lib/gql/__generated_code_from_codegen__'
import { simplifyResponse, SimpleResponse, NonNullableItem } from 'src/lib/gql'
const getBeachQuery = graphql(`
query getBeach($slug: String!, $locale: I18NLocaleCode!) {
detailsBeaches(filters: { slug: { eq: $slug } }, locale: $locale) {
data {
attributes {
name
basicDetails {
shortDescription
cover {
data {
attributes {
url
height
width
}
}
}
}
}
}
}
}
`)
export default async function PageWrapper({
params: { slug },
}: {
params: { slug: string }
}) {
const locale = useLocale()
const { data } = await gqlClient().query({
query: getBeachQuery,
variables: { locale, slug },
})
const beaches = simplifyResponse(data)
const beach = beaches?.[0]
if (!beach) notFound()
return <Page beach={beach} />
}
// Notice the custom `NonNullableItem` utility type wrapping the `SimpleResponse` to acces the array item and remove nulls at the same time
function Page({ beach }: { beach: NonNullableItem<SimpleResponse<GetBeachQuery>> }) {
return (
<h2>{beach.name}</h2>
<img src={beach.basicDetails?.cover?.url} alt="Beach image" />
)
}
GraphQL returns an object
// /src/app/beaches/page.tsx <-- Just an example, yours will be different
import { GetAllBeachesQuery, graphql, gqlClient } from 'src/lib/gql/__generated_code_from_codegen__'
import { simplifyResponse, SimpleResponse } from 'src/lib/gql'
const getAllBeachesQuery = graphql(`
query getAllBeaches($locale: I18NLocaleCode!) {
detailsBeaches(locale: $locale) {
data {
attributes {
name
slug
}
}
}
}
`)
export default async function PageWrapper() {
const locale = useLocale()
const { data } = await gqlClient().query({
query: getAllBeachesQuery,
variables: { locale },
})
const beaches = simplifyResponse(data)
if (!beaches) return <h1>Error fetching data</h1> // TODO: Do better error handling
return <Page beaches={beaches} />
}
// Notice the TypeScript native `NonNullable` utility type wrapping the `SimpleResponse` to remove nulls
function Page({ beaches }: { beaches: NonNullable<SimpleResponse<GetAllBeachesQuery>> }) {
return (
<ul>
{beaches.map((beach) => beach && (
<li key={beach.slug}>
<Link
href={{
pathname: '/beaches/[slug]',
params: { slug: beach.slug ?? 'null' },
}}
>{beach.name}</Link>
</li>
))}
</ul>
)
}
You're welcome :D
It's me again, this is the script I ended up doing. The code is not clean, but the exported methods let the code where it's used be clean. Could be improved because I did it in an afternoon for a new project and didn't use it in a large codebase to find edge cases, but for a basic usage works. Maybe I give updates about the new use cases I find.
Type inference works perfectly.
Basically, you place this file wherever you want in the frontend. Not in strapi backend, and call it after the GraphQL request to parse the results into a readable format while keeping the types.
// src/lib/gql/index.ts <-- You can chose another location /* eslint-disable @typescript-eslint/no-explicit-any */ export function simplifyResponse<T extends ObjectType>(response: T): SimpleResponse<T> { const entries = Object.entries(response).filter(([k]) => k !== '__typename') if (entries.length >= 2) throw new Error('Cannot simplify a Strapi response that contains an object with more than one key') return simplify(entries[0][1] as any) } export function simplify<T extends ValidType>(value: T): SimpleType<T> export function simplify<T>(value: T) { if (Array.isArray(value)) return value.map(simplify) if (isPlainObject(value)) { if ('data' in value) return simplify(value.data) if ('attributes' in value) return simplify(value.attributes) return objectMap(value, simplify) } return value } function isPlainObject<O extends R | any, R extends Record<string | number | symbol, any>>(obj: O): obj is R { return typeof obj === 'object' && obj !== null && obj.constructor === Object && Object.getPrototypeOf(obj) === Object.prototype; } interface Dictionary<T> { [key: string]: T; } function objectMap<TValue, TResult>( obj: Dictionary<TValue>, valSelector: (val: TValue, obj: Dictionary<TValue>) => TResult, keySelector?: (key: string, obj: Dictionary<TValue>) => string, ctx?: Dictionary<TValue> ) { const ret = {} as Dictionary<TResult>; for (const key of Object.keys(obj)) { if (key === '__typename') continue; const retKey = keySelector ? keySelector.call(ctx || null, key, obj) : key; const retVal = valSelector.call(ctx || null, obj[key], obj); ret[retKey] = retVal; } return ret; } type ValidType = UntouchedType | ObjectType | ArrayType type UntouchedType = boolean | number | string | symbol | null | undefined | bigint | Date type ObjectType = { [key in string]?: ValidType } type ArrayType = ValidType[] type IsAny<T> = unknown extends T & string ? true : false; export type SimpleType<T extends ValidType> = IsAny<T> extends true ? any : (T extends UntouchedType ? T : T extends [...(infer Ar extends ValidType[])] ? { [Index in keyof Ar]: SimpleType<Ar[Index]> } : T extends { [K in 'data']?: infer Ob extends ValidType } ? SimpleType<Ob> : T extends { [K in 'attributes']?: infer Ob extends ValidType } ? SimpleType<Ob> : T extends Omit<ObjectType, 'data' | 'attributes'> ? { [key in Exclude<keyof T, '__typename'>]: SimpleType<T[key]> } : T) type IsUnion<T, U extends T = T> = (T extends any ? (U extends T ? false : true) : never) extends false ? false : true type GetOnlyKeyOrNever<T extends ObjectType, Keys = Exclude<keyof T, '__typename'>> = IsUnion<Keys> extends true ? never : Keys export type SimpleResponse<T extends ObjectType> = SimpleType<T[GetOnlyKeyOrNever<T>]> export type NonNullableItem<T extends any[] | null | undefined> = NonNullable<NonNullable<T>[number]>What does it do? An example
simplifyResponse()
Transforms this:{ "detailsBeaches": { "__typename": "DetailsBeachEntityResponseCollection", "data": [ { "__typename": "DetailsBeachEntity", "attributes": { "__typename": "DetailsBeach", "name": "Aigua blava", "basicDetails": { "__typename": "ComponentPlaceDetailsBasicDetails", "shortDescription": "Lorem ipsum...", "cover": { "__typename": "UploadFileEntityResponse", "data": { "__typename": "UploadFileEntity", "attributes": { "__typename": "UploadFile", "url": "/uploads/Aiguablava_19_ecbb012937.jpg", "height": 768, "width": 1413 } } } } } } ] } }Into this:
[ { "name": "Aigua blava", "basicDetails": { "shortDescription": "Lorem ipsum...", "cover": { "url": "/uploads/Aiguablava_19_ecbb012937.jpg", "height": 768, "width": 1413 } } } ]Notice that the first object with only one key gets "unwraped", in this case the key
detailsBeaches
is gone.And automatically infers proper types. 🎉
simplify()
does the same, but doesn't remove the first object key.The exported utility types are:
SimpleResponse
: Return type ofsimplifyResponse()
functionSimpleType
: Return type ofsimplify()
functionNonNullableItem
: Used to access the first item of a response that returns a list and removes nulls/undefineds.NonNullable
: Well, this is not exported, and it's native to typescript, but is useful. Removes nulls/undefineds.Usage example
GraphQL query returns an array, but we just want the first item:
// /src/app/beaches/[slug]/page.tsx <-- Just an example, yours will be different import { GetBeachQuery, graphql, gqlClient } from 'src/lib/gql/__generated_code_from_codegen__' import { simplifyResponse, SimpleResponse, NonNullableItem } from 'src/lib/gql' const getBeachQuery = graphql(` query getBeach($slug: String!, $locale: I18NLocaleCode!) { detailsBeaches(filters: { slug: { eq: $slug } }, locale: $locale) { data { attributes { name basicDetails { shortDescription cover { data { attributes { url height width } } } } } } } } `) export default async function PageWrapper({ params: { slug }, }: { params: { slug: string } }) { const locale = useLocale() const { data } = await gqlClient().query({ query: getBeachQuery, variables: { locale, slug }, }) const beaches = simplifyResponse(data) const beach = beaches?.[0] if (!beach) notFound() return <Page beach={beach} /> } // Notice the custom `NonNullableItem` utility type wrapping the `SimpleResponse` to acces the array item and remove nulls at the same time function Page({ beach }: { beach: NonNullableItem<SimpleResponse<GetBeachQuery>> }) { return ( <h2>{beach.name}</h2> <img src={beach.basicDetails?.cover?.url} alt="Beach image" /> ) }GraphQL returns an object
// /src/app/beaches/page.tsx <-- Just an example, yours will be different import { GetAllBeachesQuery, graphql, gqlClient } from 'src/lib/gql/__generated_code_from_codegen__' import { simplifyResponse, SimpleResponse } from 'src/lib/gql' const getAllBeachesQuery = graphql(` query getAllBeaches($locale: I18NLocaleCode!) { detailsBeaches(locale: $locale) { data { attributes { name slug } } } } `) export default async function PageWrapper() { const locale = useLocale() const { data } = await gqlClient().query({ query: getAllBeachesQuery, variables: { locale }, }) const beaches = simplifyResponse(data) if (!beaches) return <h1>Error fetching data</h1> // TODO: Do better error handling return <Page beaches={beaches} /> } // Notice the TypeScript native `NonNullable` utility type wrapping the `SimpleResponse` to remove nulls function Page({ beaches }: { beaches: NonNullable<SimpleResponse<GetAllBeachesQuery>> }) { return ( <ul> {beaches.map((beach) => beach && ( <li key={beach.slug}> <Link href={{ pathname: '/beaches/[slug]', params: { slug: beach.slug ?? 'null' }, }} >{beach.name}</Link> </li> ))} </ul> ) }You're welcome :D
🥺
@jonasmarco It works, check that the code was copied well and also your project's tsconfig.json.
Code working in the TS playground.
Graphql Support [Partially tested!!!]
Apollo Codegen command
Folder Structure for queries:

index.ts should contain the real query
types.ts the response type
Final result is this :

And the return types will be generated correctly in your typescript code