diff --git a/plopfile.js b/plopfile.js index f8e92a2..e3a9ff1 100644 --- a/plopfile.js +++ b/plopfile.js @@ -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", diff --git a/templates/domains/api/example.ts.hbs b/templates/domains/api/example.ts.hbs index 1af0539..415be9d 100644 --- a/templates/domains/api/example.ts.hbs +++ b/templates/domains/api/example.ts.hbs @@ -1,50 +1,89 @@ +/** + * 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; +export type Create{{pascalCase name}} = z.infer; -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>(URL, { +export const get{{pascalCase name}} = async (params: {{pascalCase name}}ListParams) => { + const { data } = await api.get>(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>( +export const get{{pascalCase name}}ById = async ({{camelCase name}}Id: string) => { + const { data } = await api.get>( 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) => { - const { data } = await api.post>( +export const create{{pascalCase name}} = async (body: Create{{pascalCase name}}) => { + const { data } = await api.post>( 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>( +export const update{{pascalCase name}} = async (body: {{pascalCase name}}) => { + const { data } = await api.put>( 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) => { @@ -52,5 +91,5 @@ export const delete{{pascalCase name}} = async ({{camelCase name}}Id: string) => URL.concat(["/", {{camelCase name}}Id].join("")), ); - return data.status; + return data; }; diff --git a/templates/domains/api/index.ts.hbs b/templates/domains/api/index.ts.hbs index 35793da..3e55cd5 100644 --- a/templates/domains/api/index.ts.hbs +++ b/templates/domains/api/index.ts.hbs @@ -1 +1,2 @@ +export * from "./schemas"; export * from "./{{kebabCase name}}"; diff --git a/templates/domains/api/schemas/example.hbs b/templates/domains/api/schemas/example.hbs new file mode 100644 index 0000000..b9f26fc --- /dev/null +++ b/templates/domains/api/schemas/example.hbs @@ -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 }); diff --git a/templates/domains/api/schemas/index.hbs b/templates/domains/api/schemas/index.hbs new file mode 100644 index 0000000..263716e --- /dev/null +++ b/templates/domains/api/schemas/index.hbs @@ -0,0 +1 @@ +export * from "./{{camelCase name}}Schemas"; diff --git a/templates/domains/queries/index.ts.hbs b/templates/domains/queries/index.ts.hbs index f9dc915..35793da 100644 --- a/templates/domains/queries/index.ts.hbs +++ b/templates/domains/queries/index.ts.hbs @@ -1 +1 @@ -export * as {{camelCase name}}Queries from "./{{kebabCase name}}"; +export * from "./{{kebabCase name}}"; diff --git a/templates/domains/queries/query-example.tsx.hbs b/templates/domains/queries/query-example.tsx.hbs index e85d468..7344a7f 100644 --- a/templates/domains/queries/query-example.tsx.hbs +++ b/templates/domains/queries/query-example.tsx.hbs @@ -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) => @@ -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}}, ); @@ -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}}, ); @@ -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 @@ -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,