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', | |
]; |
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.
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.
What does it do? An example
simplifyResponse()
Transforms this:Into this:
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:
GraphQL returns an object
You're welcome :D