Track and save on groceries

refactor(api): generate common schemas using meta title

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>

+76 -38
+5
packages/api/src/schema/brands.ts
··· 7 7 name: z.string(), 8 8 }) 9 9 .meta({ 10 + title: "Brand", 10 11 description: "Brand schema", 11 12 examples: [{ id: 1, name: "Nopro" }], 12 13 }); 13 14 export const CreateBrand = Brand.omit({ 14 15 id: true, 16 + }).meta({ 17 + title: "CreateBrand", 18 + description: "Create brand schema", 19 + examples: [{ name: "Nopro" }], 15 20 }); 16 21 17 22 export const BrandsQuery = pageOptions(Brand.shape.id);
+17 -9
packages/api/src/schema/pagination.ts
··· 5 5 export const Limit = z.coerce.number().int().min(0); 6 6 7 7 export const pageOptions = <T extends z.ZodTypeAny>(cursor: T) => 8 - z.object({ 9 - limit: Limit.default(DEFAULT_LIMIT), 10 - cursor: cursor.optional(), 11 - }); 8 + z 9 + .object({ 10 + limit: Limit.default(DEFAULT_LIMIT), 11 + cursor: cursor.optional(), 12 + }) 13 + .meta({ 14 + title: `${cursor.meta()?.title ?? "UNKNOWN_OBJECT"}PageOptions`, 15 + }); 12 16 13 17 export const paginated = <T extends z.ZodTypeAny>(item: T) => 14 - z.object({ 15 - total: Total, 16 - limit: Limit, 17 - items: z.array(item), 18 - }); 18 + z 19 + .object({ 20 + total: Total, 21 + limit: Limit, 22 + items: z.array(item), 23 + }) 24 + .meta({ 25 + title: `${item.meta()?.title ?? "UNKNOWN_OBJECT"}s`, 26 + });
+11
packages/api/src/schema/products.ts
··· 9 9 brandId: Brand.shape.id, 10 10 }) 11 11 .meta({ 12 + title: "Product", 12 13 description: "Product schema", 13 14 examples: [{ id: 1, brandId: 1, name: "Not Milk 1l" }], 14 15 }); 15 16 16 17 export const ExtendedProduct = Product.extend({ 17 18 brand: Brand, 19 + }).meta({ 20 + title: "ExtendedProduct", 21 + description: "Extended product schema, with Brand", 22 + examples: [ 23 + { id: 1, brandId: 1, brand: { id: 1, name: "Nopro" }, name: "Not Milk 1l" }, 24 + ], 18 25 }); 19 26 20 27 export const CreateProduct = Product.omit({ 21 28 id: true, 29 + }).meta({ 30 + title: "CreateProduct", 31 + description: "Create product schema", 32 + examples: [{ brandId: 1, name: "Not Milk 1l" }], 22 33 }); 23 34 24 35 export const ProductsQuery = pageOptions(Product.shape.id);
+6
packages/api/src/schema/stores.ts
··· 7 7 name: z.string(), 8 8 }) 9 9 .meta({ 10 + title: "Store", 10 11 description: "Store schema", 11 12 examples: [{ id: 1, name: "EDEKA" }], 12 13 }); 14 + 13 15 export const CreateStore = Store.omit({ 14 16 id: true, 17 + }).meta({ 18 + title: "CreateStore", 19 + description: "Create store schema", 20 + examples: [{ name: "Lee Deal" }], 15 21 }); 16 22 17 23 export const StoresQuery = pageOptions(Store.shape.id);
+37 -29
packages/api/src/spec.ts
··· 1 - import { OpenAPIGenerator } from "@orpc/openapi"; 1 + import { OpenAPIGenerator, SchemaConvertOptions } from "@orpc/openapi"; 2 2 import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4"; 3 3 import { Store, Stores } from "./schema/stores.js"; 4 4 import { mkdir, writeFile } from "node:fs/promises"; 5 5 import { apiContract } from "./contract/index.js"; 6 6 import { Brand, Brands } from "./schema/brands.js"; 7 7 import { ExtendedProduct, Product, Products } from "./schema/products.js"; 8 + import z from "zod/v4"; 9 + import assert from "node:assert"; 8 10 9 11 const generator = new OpenAPIGenerator({ 10 12 schemaConverters: [new ZodToJsonSchemaConverter()], 11 13 }); 12 14 15 + const registerSchemas = ( 16 + strategy: SchemaConvertOptions["strategy"], 17 + schemas: z.ZodType[], 18 + ) => { 19 + const commonSchemas: Record< 20 + string, 21 + { 22 + strategy?: SchemaConvertOptions["strategy"]; 23 + schema: z.ZodType; 24 + } 25 + > = {}; 26 + 27 + for (const [i, schema] of schemas.entries()) { 28 + const meta = schema.meta(); 29 + assert(meta, `Schema ${i} has meta`); 30 + assert(meta.title, `Schema ${i} has title in meta`); 31 + 32 + commonSchemas[meta.title] = { 33 + strategy, 34 + schema, 35 + }; 36 + } 37 + return commonSchemas; 38 + }; 39 + 13 40 const spec = await generator.generate(apiContract, { 14 41 info: { 15 42 title: "Cherries", 16 43 version: "1.0.0", 17 44 }, 18 45 commonSchemas: { 19 - Brands: { 20 - strategy: "output", 21 - schema: Brands, 22 - }, 23 - Brand: { 24 - strategy: "output", 25 - schema: Brand, 26 - }, 27 - ExtendedProduct: { 28 - strategy: "output", 29 - schema: ExtendedProduct, 30 - }, 31 - Products: { 32 - strategy: "output", 33 - schema: Products, 34 - }, 35 - Product: { 36 - strategy: "output", 37 - schema: Product, 38 - }, 39 - Stores: { 40 - strategy: "output", 41 - schema: Stores, 42 - }, 43 - Store: { 44 - strategy: "output", 45 - schema: Store, 46 - }, 46 + ...registerSchemas("output", [ 47 + Brands, 48 + Brand, 49 + ExtendedProduct, 50 + Products, 51 + Product, 52 + Stores, 53 + Store, 54 + ]), 47 55 }, 48 56 }); 49 57