Skip to content

Commit

Permalink
[ADD] Added schemas and runtime type validation, also renamed a few f…
Browse files Browse the repository at this point in the history
…iles
  • Loading branch information
ThunderNaka committed Sep 27, 2024
1 parent 8757505 commit 1afa186
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 34 deletions.
11 changes: 11 additions & 0 deletions plopfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ export default function (
path: "src/domains/{{kebabCase name}}/api/{{kebabCase name}}.ts",
templateFile: "templates/domains/api/example.ts.hbs",
},
// Schemas
{
type: "add",
path: "src/domains/{{kebabCase name}}/api/schemas/index.ts",
templateFile: "templates/domains/api/schemas/index.hbs",
},
{
type: "add",
path: "src/domains/{{kebabCase name}}/api/schemas/{{camelCase name}}Schemas.ts",
templateFile: "templates/domains/api/schemas/example.hbs",
},
// Domain Barrel
{
type: "add",
Expand Down
79 changes: 59 additions & 20 deletions templates/domains/api/example.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,56 +1,95 @@
/**
* This file handles user-related API operations with runtime validation using Zod.
* Each function includes a type-safe schema (e.g., `{{camelCase name}}Schema`) for parsing and validating
* responses, ensuring that the data conforms to the expected structure at runtime.
*
* Zod is used here to provide robust validation and type inference, improving data integrity
* throughout the API response lifecycle.
*
* We preserve the `ServiceResponse` structure in the return value, allowing the original metadata
* (such as status, pagination, etc.) to pass through, while validating the actual `data` payload.
*
* The `URL.concat(["/", {{camelCase name}}Id].join(""))` approach is used for constructing dynamic URLs
* due to limitations with Handlebars, preventing the use of template literals (e.g., `${URL}/whateverId}`).
* However, the template literal format is preferred when possible for simplicity and readability.
*/
import { z } from "zod";

import type { ServiceResponse } from "~/config/api";
import { api } from "~/config/api";
import { create{{camelCase name}}Schema, {{camelCase name}}Schema } from "./schemas";

export interface Base{{pascalCase name}} {
id: string;
name: string;
address: string;
}
export type {{pascalCase name}} = z.infer<typeof {{camelCase name}}Schema>;
export type Create{{pascalCase name}} = z.infer<typeof create{{camelCase name}}Schema>;

export interface Base{{pascalCase name}}ListParams {
export type {{pascalCase name}}ListParams = {
page?: number;
}
};

const URL = "/{{kebabCase name}}";

export const get{{pascalCase name}}List = async (params: Base{{pascalCase name}}ListParams) => {
const { data } = await api.get<ServiceResponse<Base{{pascalCase name}}[]>>(URL, {
export const get{{pascalCase name}} = async (params: {{pascalCase name}}ListParams) => {
const { data } = await api.get<ServiceResponse<{{pascalCase name}}[]>>(URL, {
params,
});

return data.data;
// Runtime type check
const parsed = data.data.map((item) => {{camelCase name}}Schema.parse(item));

return {
...data,
data: parsed,
};
};

export const get{{pascalCase name}}Detail = async ({{camelCase name}}Id: string) => {
const { data } = await api.get<ServiceResponse<Base{{pascalCase name}}>>(
export const get{{pascalCase name}}ById = async ({{camelCase name}}Id: string) => {
const { data } = await api.get<ServiceResponse<{{pascalCase name}}>>(
URL.concat(["/", {{camelCase name}}Id].join("")),
);

return data.data;
// Runtime type check
const parsed = {{camelCase name}}Schema.parse(data.data);

return {
...data,
data: parsed,
};
};

export const create{{pascalCase name}} = async (body: Omit<Base{{pascalCase name}}, "id">) => {
const { data } = await api.post<ServiceResponse<Base{{pascalCase name}}>>(
export const create{{pascalCase name}} = async (body: Create{{pascalCase name}}) => {
const { data } = await api.post<ServiceResponse<{{pascalCase name}}>>(
URL,
body,
);

return data.data;
// Runtime type check
const parsed = {{camelCase name}}Schema.parse(data.data);

return {
...data,
data: parsed,
};
};

export const update{{pascalCase name}} = async (body: Base{{pascalCase name}}) => {
const { data } = await api.put<ServiceResponse<Base{{pascalCase name}}>>(
export const update{{pascalCase name}} = async (body: {{pascalCase name}}) => {
const { data } = await api.put<ServiceResponse<{{pascalCase name}}>>(
URL.concat(["/", body.id].join("")),
body,
);

return data.data;
// Runtime type check
const parsed = {{camelCase name}}Schema.parse(data.data);

return {
...data,
data: parsed,
};
};

export const delete{{pascalCase name}} = async ({{camelCase name}}Id: string) => {
const { data } = await api.delete<ServiceResponse<void>>(
URL.concat(["/", {{camelCase name}}Id].join("")),
);

return data.status;
return data;
};
1 change: 1 addition & 0 deletions templates/domains/api/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./schemas";
export * from "./{{kebabCase name}}";
9 changes: 9 additions & 0 deletions templates/domains/api/schemas/example.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const {{camelCase name}}Schema = z.object({
id: z.string(),
name: z.string(),
address: z.string(),
});

export const create{{camelCase name}}Schema = {{camelCase name}}Schema.omit({ id: true });
1 change: 1 addition & 0 deletions templates/domains/api/schemas/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./{{camelCase name}}Schemas";
2 changes: 1 addition & 1 deletion templates/domains/queries/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * as {{camelCase name}}Queries from "./{{kebabCase name}}";
export * from "./{{kebabCase name}}";
26 changes: 13 additions & 13 deletions templates/domains/queries/query-example.tsx.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ import { createQueryKeys } from "@lukemorales/query-key-factory";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import type {
Base{{pascalCase name}},
Base{{pascalCase name}}ListParams,
{{pascalCase name}},
{{pascalCase name}}ListParams,
} from "../api/{{kebabCase name}}";
import {
create{{pascalCase name}},
delete{{pascalCase name}},
get{{pascalCase name}}Detail,
get{{pascalCase name}}List,
get{{pascalCase name}}ById,
get{{pascalCase name}},
update{{pascalCase name}},
} from "../api/{{kebabCase name}}";

const {{camelCase name}}Keys = createQueryKeys("{{camelCase name}}", {
list: {
queryKey: null,
queryFn: () => get{{pascalCase name}}List,
queryFn: () => get{{pascalCase name}},
},
detail: ({{camelCase name}}Id: string) => ({
queryKey: [{{camelCase name}}Id],
queryFn: () => get{{pascalCase name}}Detail({{camelCase name}}Id),
queryFn: () => get{{pascalCase name}}ById({{camelCase name}}Id),
}),
});

const use{{pascalCase name}}ListQuery = (params: Base{{pascalCase name}}ListParams) =>
const use{{pascalCase name}}Query = (params: {{pascalCase name}}ListParams) =>
useQuery({
...{{camelCase name}}Keys.list,
queryFn: () => get{{pascalCase name}}List(params),
queryFn: () => get{{pascalCase name}}(params),
});

const use{{pascalCase name}}DetailQuery = ({{camelCase name}}Id: string) =>
Expand All @@ -44,7 +44,7 @@ const useCreate{{pascalCase name}}Mutation = () => {
mutationFn: create{{pascalCase name}},
onSuccess: (new{{pascalCase name}}) => {
queryClient.setQueryData(
{{camelCase name}}Keys.detail(new{{pascalCase name}}.id).queryKey,
{{camelCase name}}Keys.detail(new{{pascalCase name}}.data.id).queryKey,
new{{pascalCase name}},
);

Expand Down Expand Up @@ -84,7 +84,7 @@ const useUpdate{{pascalCase name}}Mutation = () => {
},
onSuccess: (new{{pascalCase name}}) => {
queryClient.setQueryData(
{{camelCase name}}Keys.detail(new{{pascalCase name}}.id).queryKey,
{{camelCase name}}Keys.detail(new{{pascalCase name}}.data.id).queryKey,
new{{pascalCase name}},
);

Expand All @@ -111,8 +111,8 @@ const useDelete{{pascalCase name}}Mutation = () => {
// Optimistically update to the new value
queryClient.setQueryData(
{{camelCase name}}Keys.detail({{camelCase name}}Id).queryKey,
(old: Base{{pascalCase name}}[]) =>
old.filter((t: Base{{pascalCase name}}) => t.id !== {{camelCase name}}Id),
(old: {{pascalCase name}}[]) =>
old.filter((t: {{pascalCase name}}) => t.id !== {{camelCase name}}Id),
);

// Return a context object with the snapshotted value
Expand All @@ -135,7 +135,7 @@ const useDelete{{pascalCase name}}Mutation = () => {


export {
use{{pascalCase name}}ListQuery,
use{{pascalCase name}}Query,
use{{pascalCase name}}DetailQuery,
usePrefetch{{pascalCase name}}DetailQuery,
useCreate{{pascalCase name}}Mutation,
Expand Down

0 comments on commit 1afa186

Please sign in to comment.