supabase-cache-helpers
supabase-cache-helpers copied to clipboard
Feature request: convert snake case to camel case and back
For my project, I created a wrapper around the useQuery
, useUpsertMutation
, useDeleteMutation
functions of this library in order to convert the snake-cased column names from the database into camel-cased keys for use in my JS/TS app.
Would be awesome if this library could support that out of the box.
thanks for the feature request! how do you handle the typescript types? do you wrappers also transform the column names in the types? also, what benefit do you have from that other than that it looks nicer?
The benefit is consistency in my typescript code. The convention in ts/js code is for almost everything to be camel-cased. And, as far as I know, there is no convention for anything in ts/js code to be snake-cased.
So, bottom line:
- Consistent code
- No surprises or confusion
- Looks better
Types are also converted, yes.
For instance:
hooks/use-data-query.ts
import { PostgrestError, PostgrestResponse } from '@supabase/postgrest-js';
import { useQuery } from '@supabase-cache-helpers/postgrest-swr';
import { SWRConfiguration, SWRResponse } from 'swr';
import { camelCaseKeys, SnakeToCamelCaseNested } from '../utils';
type UseQueryReturn<Result, Transformed> = Omit<
SWRResponse<PostgrestResponse<Transformed>['data'], PostgrestError>,
'mutate'
> &
Pick<SWRResponse<PostgrestResponse<Result>, PostgrestError>, 'mutate'> &
Pick<PostgrestResponse<Result>, 'count'>;
export function useDataQuery<Result>(
query: PromiseLike<PostgrestResponse<Result>> | null,
config?: SWRConfiguration
): UseQueryReturn<Result, SnakeToCamelCaseNested<Result>> {
const { data, ...rest } = useQuery(query, config);
return {
...rest,
data: data && camelCaseKeys<Result>(data),
};
}
utils/casings.ts
// From: https://github.com/orgs/supabase/discussions/7136
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
? `${Lowercase<T>}${Capitalize<SnakeToCamelCase<U>>}`
: S;
type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
? `${T extends Capitalize<T> ? '_' : ''}${Lowercase<T>}${CamelToSnakeCase<U>}`
: S;
type TransformFn = (value: string) => string;
export type SnakeToCamelCaseNested<T> = T extends object
? {
[K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<
T[K]
>;
}
: T;
export type CamelToSnakeCaseNested<T> = T extends object
? {
[K in keyof T as CamelToSnakeCase<K & string>]: CamelToSnakeCaseNested<
T[K]
>;
}
: T;
const isObjectCustom = (value: unknown) =>
typeof value === 'object' &&
value !== null &&
!(value instanceof RegExp) &&
!(value instanceof Error) &&
!(value instanceof Date);
const snakeCase: TransformFn = (value: string) =>
value
.replace(/([A-Z])/g, '_$1')
.split('_')
.map((value, i) =>
i ? `${value.charAt(0).toLowerCase()}${value.slice(1)}` : value
)
.join('_');
const camelCase: TransformFn = (value: string) =>
value
.split('_')
.map((value, i) =>
i ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value
)
.join('');
// See https://github.com/microsoft/TypeScript/issues/33014#issuecomment-570191837
function changeKeys<T, C>(input: T, fn: TransformFn): C;
function changeKeys<T, C>(input: T[], fn: TransformFn): C[];
function changeKeys<T, C>(input: T | T[], fn: TransformFn): C | C[] {
if (Array.isArray(input)) {
return input.map((item) =>
isObjectCustom(item) ? changeKeys<T, C>(item, fn) : item
) as C[];
}
const result: Record<string, unknown> = {};
Object.keys(input as object).forEach((key) => {
const value = (input as Record<string, unknown>)[key];
result[fn(key)] = isObjectCustom(value) ? changeKeys(value, fn) : value;
});
return result as C;
}
export function snakeCaseKeys<T>(input: T): CamelToSnakeCaseNested<T>;
export function snakeCaseKeys<T>(input: T[]): CamelToSnakeCaseNested<T>[];
export function snakeCaseKeys<T>(
input: T | T[]
): CamelToSnakeCaseNested<T> | CamelToSnakeCaseNested<T>[] {
// Check needed for TypeScript
if (Array.isArray(input)) {
return changeKeys<T, CamelToSnakeCaseNested<T>>(input, snakeCase);
}
return changeKeys<T, CamelToSnakeCaseNested<T>>(input, snakeCase);
}
export function camelCaseKeys<T>(input: T): SnakeToCamelCaseNested<T>;
export function camelCaseKeys<T>(input: T[]): SnakeToCamelCaseNested<T>[];
export function camelCaseKeys<T>(
input: T | T[]
): SnakeToCamelCaseNested<T> | SnakeToCamelCaseNested<T>[] {
// Check needed for TypeScript
if (Array.isArray(input)) {
return changeKeys<T, SnakeToCamelCaseNested<T>>(input, camelCase);
}
return changeKeys<T, SnakeToCamelCaseNested<T>>(input, camelCase);
}
thanks for providing the code!
I do not think this should be added to cache-helpers out-of-the-box, since it's very subjective and just a code style improvement. But I was thinking about adding a transformer
api, which solve this use-case in a more generic manner:
transformer: (result: R) => TransformedResult
The transformer is applied in the fetcher function, and its result cached by SWR / React Query.
This could also be used to eg create Date objects from timestamp columns.
Thanks for the feedback. I see how that could be a very useful feature and how that could also be used to transform casings. Do you have any ticket for that already, to track progress?
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.