{"_id":"@andyrmitchell/endpoint-schemas","_rev":"2-f15e361f2338a2ba6f4e12e65aae291e","name":"@andyrmitchell/endpoint-schemas","description":"Validate Request/Response for a http endpoint (including serverless functions), and generate a TypeScript client for consuming it","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@andyrmitchell/endpoint-schemas","version":"0.1.0","author":{"name":"andymitchell"},"license":"MIT","_id":"@andyrmitchell/endpoint-schemas@0.1.0","maintainers":[{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"}],"bin":{"make-endpoint-schemas-client":"dist/make-endpoint-schemas-client-cli.mjs"},"dist":{"shasum":"552d4cde491c1db2746cf8b19d3dfe0413850f40","tarball":"https://registry.npmjs.org/@andyrmitchell/endpoint-schemas/-/endpoint-schemas-0.1.0.tgz","fileCount":15,"integrity":"sha512-5E3XecNJ7Waj3BPUjzNmCBMSf5QzzTz7Unao3o90eAKzL+5+MSteORz3esU21bLSP893x/8ks0t2d27ynwwnKw==","signatures":[{"sig":"MEYCIQCwBaBKCFEZRDayBtEKbpNvsF81jCubk0j+CPfNCgHavQIhAMDmisqOR003rwLlQj1Rmszf+YoA+d3V2bYmwmZvTZCw","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":49460},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"module":"./dist/index.mjs","require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}}},"gitHead":"c143dc2cf5bb6ff4ba55d348e658dcf99a8cc2a8","scripts":{"test":"jest","build":"tsup","pkglint":"./build/publint_pipeable.sh","test:watch":"jest --watch","build_prepare":"npm run build && npm run pkglint","build_release":"npm run build_prepare && np","prepublishOnly":"npm run build_prepare"},"_npmUser":{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"},"_npmVersion":"9.8.1","description":"Validate Request/Response for a http endpoint (including serverless functions), and generate a TypeScript client for consuming it","directories":{},"_nodeVersion":"18.18.2","dependencies":{"zod":"^3.23.8","@andyrmitchell/file-io":"^0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.0.1","ts-jest":"^29.1.2","babel-jest":"^29.7.0","filenamify":"^6.0.0","typescript":"^5.4.5","@babel/core":"^7.23.9","@types/jest":"^29.5.12","@types/inquirer":"^9.0.7","@babel/preset-env":"^7.23.9","@supabase/supabase-js":"^2.43.4","@babel/preset-typescript":"^7.23.3","babel-plugin-transform-import-meta":"^2.2.1"},"_npmOperationalInternal":{"tmp":"tmp/endpoint-schemas_0.1.0_1717428500137_0.6317893053987362","host":"s3://npm-registry-packages"}},"0.1.1":{"name":"@andyrmitchell/endpoint-schemas","version":"0.1.1","author":{"name":"andymitchell"},"license":"MIT","_id":"@andyrmitchell/endpoint-schemas@0.1.1","maintainers":[{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"}],"bin":{"make-endpoint-schemas-client":"dist/make-endpoint-schemas-client-cli.mjs"},"dist":{"shasum":"6e78a87fcf437c9f2264b3d493454b49e075a576","tarball":"https://registry.npmjs.org/@andyrmitchell/endpoint-schemas/-/endpoint-schemas-0.1.1.tgz","fileCount":15,"integrity":"sha512-/J2wMCcWwYkLA5A4HiuKWiZbcPrlXIGhy2zXhhMhjXmnL4M3JQfq3WgkoI8yQp+K2LHAH3/ByTUYqyp1BZU8jw==","signatures":[{"sig":"MEYCIQCJK+pKC6M1UzFqPJhkYu+avvqGBNeHWn5R/5EqAOgA/AIhAIIrGaDhDYfotsh+T7l54basKHdWEx44hYY05No8brpk","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":51448},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"module":"./dist/index.mjs","require":{"types":"./dist/index.d.ts","default":"./dist/index.js"}}},"gitHead":"7bfa2bd9173fa0a15350882ce673824151746cf6","scripts":{"test":"jest","build":"tsup && chmod +x ./dist/make-endpoint-schemas-client-cli.mjs","pkglint":"./build/publint_pipeable.sh","test_cli":"npx tsup-node --no-config --format esm --entry.test_cli ./src/cli/index.ts && node ./dist/test_cli.mjs && rm ./dist/test_cli.mjs","test:watch":"jest --watch","build_prepare":"npm run build && npm run pkglint","build_release":"npm run build_prepare && np","prepublishOnly":"npm run build_prepare"},"_npmUser":{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"},"_npmVersion":"9.8.1","description":"Validate Request/Response for a http endpoint (including serverless functions), and generate a TypeScript client for consuming it","directories":{},"_nodeVersion":"18.18.2","dependencies":{"zod":"^3.23.8","@andyrmitchell/file-io":"^0.8.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.0.1","ts-jest":"^29.1.2","babel-jest":"^29.7.0","filenamify":"^6.0.0","typescript":"^5.4.5","@babel/core":"^7.23.9","@types/jest":"^29.5.12","@types/inquirer":"^9.0.7","@babel/preset-env":"^7.23.9","@supabase/supabase-js":"^2.43.4","@babel/preset-typescript":"^7.23.3","babel-plugin-transform-import-meta":"^2.2.1"},"_npmOperationalInternal":{"tmp":"tmp/endpoint-schemas_0.1.1_1718026209644_0.9667820352853513","host":"s3://npm-registry-packages"}},"0.1.2":{"name":"@andyrmitchell/endpoint-schemas","version":"0.1.2","description":"Validate Request/Response for a http endpoint (including serverless functions), and generate a TypeScript client for consuming it","exports":{".":{"module":"./dist/index.mjs","require":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"types":"./dist/index.d.ts"}},"main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","bin":{"endpoint-schemas":"dist/make-endpoint-schemas-client-cli.mjs"},"type":"commonjs","publishConfig":{"access":"public"},"scripts":{"build_release":"npm run build_prepare && np","build":"tsup && chmod +x ./dist/make-endpoint-schemas-client-cli.mjs","pkglint":"./build/publint_pipeable.sh","build_prepare":"npm run build && npm run pkglint","prepublishOnly":"npm run build_prepare","test":"jest","test:watch":"jest --watch","test_cli":"npx tsup-node --no-config --format esm --entry.test_cli ./src/cli/index.ts && node ./dist/test_cli.mjs && rm ./dist/test_cli.mjs"},"author":{"name":"andymitchell"},"license":"MIT","devDependencies":{"@babel/core":"^7.23.9","@babel/preset-env":"^7.23.9","@babel/preset-typescript":"^7.23.3","@supabase/supabase-js":"^2.43.4","@types/inquirer":"^9.0.7","@types/jest":"^29.5.12","babel-jest":"^29.7.0","babel-plugin-transform-import-meta":"^2.2.1","filenamify":"^6.0.0","jest":"^29.7.0","ts-jest":"^29.1.2","tsup":"^8.0.1","typescript":"^5.4.5"},"dependencies":{"@andyrmitchell/file-io":"^0.8.2","zod":"^3.23.8"},"_id":"@andyrmitchell/endpoint-schemas@0.1.2","gitHead":"c2f81221dcc21db6dcd6305423a6f55f2dbebcd4","_nodeVersion":"18.18.2","_npmVersion":"9.8.1","dist":{"integrity":"sha512-EXnht97K5TWBc1mE8r1/I9NbynABQJANjdG4l3+qQ0qQ/pl+AqzXZYeVc+GhNFupcJUz6NKJ3wB7A2S60tnUPA==","shasum":"ea49e50fca7c4a741218d6e65ce9b8c487b7e396","tarball":"https://registry.npmjs.org/@andyrmitchell/endpoint-schemas/-/endpoint-schemas-0.1.2.tgz","fileCount":15,"unpackedSize":51903,"signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEYCIQC4pXcXbGfkwBBayMGqncWuJyEXquD0+od0kjmS8LdcKwIhAIIn8138XejJVfa/t08IedRCd9Hi/7fWVhIDVTIGHCbw"}]},"_npmUser":{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"},"directories":{},"maintainers":[{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/endpoint-schemas_0.1.2_1718027421336_0.21440431645087465"},"_hasShrinkwrap":false}},"time":{"created":"2024-06-03T15:28:20.029Z","modified":"2024-06-10T13:50:21.669Z","0.1.0":"2024-06-03T15:28:20.279Z","0.1.1":"2024-06-10T13:30:09.794Z","0.1.2":"2024-06-10T13:50:21.510Z"},"maintainers":[{"name":"andyrmitchell","email":"a.r.mitchell@gmail.com"}],"author":{"name":"andymitchell"},"license":"MIT","readme":"## Guard your endpoints and generate a TypeClient client for consuming them\n\nGoals\n- Share all the types a client would need to know about, with no maintenance overhead\n- Validate the data transferred from client to endpoint (and vice versa)\n\n## Installing\n\n`npm i @andyrmitchell/endpoint-schemas`\n\n## How to use it\n\n### 1. Add an endpoint.ts to each endpoint directory\n\nE.g. in Supabase, add `endpoint.ts` to each function directory, such as `/supabase/functions/function-name` \n\n### 2. Specify what is a valid request and response looks like\n\nIn `endpoint.ts`, specify a schema in Zod. \n\nThe exported const must be called `EndpointSchemas` (no defaults), and must be in this format:\n```typescript\ntype EndpointSchemasType = {\n    [K: 'GET' | 'POST' | 'PUT' | 'DELETE']: {\n        request: z.ZodTypeAny,\n        response: z.ZodTypeAny\n    };\n};\n```\n\nFor example:\n\n```typescript\nimport z from \"zod\";\n\nexport const EndpointSchemas = {\n    'POST': {\n        request: z.object({\n            bundle_id: z.number()\n        }),\n        response: z.object({\n            success: z.boolean(),\n            products: z.array(z.number())\n        })\n    }\n};\n\n```\n\n\n\n### 3. Validate your actual endpoint (optional)\n\nE.g. in Supabase, in `/supabase/functions/function-name/index.ts`:\n\n```typescript\nimport { EndpointSchemas } from \"./endpoint.ts\";\nimport {isRequest, endpointResponse} from '@andyrmitchell/endpoint-schemas'\n\nserve(async (req) => {\n\n    // Validate the input against your schema\n    const input = await req.json();\n    if( !isRequest(EndpointSchemas, 'POST', input) ) {\n        return new Response(JSON.stringify({error: 'Invalid body'}), {headers: {status: 400}});\n    }\n\n    // Do processing\n\n    // Use endpointResponse to type check your response immediately against EndpointSchemas\n    return endpointResponse(EndpointSchemas, 'POST', 200, {\n        success: true, \n        products: [1,2,3]\n    }, {});\n})\n```\n\n### 4. Generate TypeScript for any client of your API / serverless functions\n\n- In Terminal, go to your package root\n- Make sure you've installed this package (`npm i @andyrmitchell/endpoint-schemas -D`). The -D is optional.\n- `npx make-endpoint-schemas-client`\n    - It will ask you where your functions are kept, find every `endpoint.ts` within that root (i.e. every endpoint), and output a .ts file in the destination directory that you choose, called `EndpointMap.ts`. \n    - _Recommendation_: Make this part of your build/deploy process\n\n### 5. In your client, consume fully typed responses from your endpoints\n\nYou'll import the `EndpointMap.ts` you generated, and use its helper functions to fetch a response. That response will be fully typed, and validated.\n\n#### Example: fetch an endpoint\n\n```typescript \n\nimport {fetchEdge} from './path/to/EndpointMap.ts'\n\nconst resource = await fetchEdge('endpoint1::POST', {bundle_id: 1}, {'root_url': 'https://api.yourserver.com', 'bearer_token': 'auth123'});\n// This is all type checked, and will autocomplete in your IDE. (Notice it matches the server's endpoint.ts)\nif( resource.data?.success ) {\n    resource.data.products;\n}\n\n```\n\n#### Example: fetch an endpoint manually, and cast its type \n\nIf you don't want to use the `fetchEdge` helper, and prefer a regular fetch:\n\n```typescript \n\nimport {EndpointTypesMap} from './path/to/EndpointMap.ts';\n\nconst response = await fetch(\"http://example.com/endpoint1\");\n\n// Manually cast it. Pay attention to match up the endpoint name (\"endpoint1\") method used (\"POST\"), and to specify \"response\". \nconst result = response.json() as unknown as EndpointTypesMap['endpoint1::POST']['response'];\n\n```\n\n#### Example: invoke a Supabase function\n\nAs above, but use `invokeEdge` instead of `fetchEdge`\n\n\n\n## Limitations\n\n### Imported modules cannot use 'default' and risk namespace collision\n\nIf two end points import two modules, that both export a 'run' item, potentially bad things happen: \n- At the very least, the output file will have two 'run' declarations, causing a conflict\n- If they have two different purposes, it'll be hard to decide which to use. \n\nCurrent Solution\n- Manually try to make sure endpoint.ts doesn't repeat names anywhere in its imports. Good luck!\n\nTODO Future Solution\n- The dream is that some code will be able to sensibly roll up imports into one TypeScript file\n    - Obviously Webpack/esbuild does this... but only to output Javascript. No good, as we need the types. \n        - They do support plugins, which might be a pathway\n        - tsconfig also exports a type declaration file alongside it. We'd lose the schemas, but maybe they're fine in pure Javascript. WORTH INVESTIGATING.\n            - We could change our approach, to only output the Zod Schemas (pure JS), then use z.infer on the client end to turn them back into types.\n    - I'm sure Deno must do this, as its TypeScript native (no need to convert) but you might still want to bundle into one file for performance.\n        - Deno vendor: downloads remote imports to a local ./vendor folder\n        - Deno compile: rolls things up, but to an executable \n    - Microsoft's API Extractor comes close, but doesn't seem to like Deno (needs tsconfig and package.json, etc.). Also it's technically just for .d.ts files. \n    - Use something like ncc (or Deno emit, or vercel/pkg) but with tsconfig's declaration: true. It should generate type outputs. We wouldn't get the schemas though. \n        - https://stackoverflow.com/questions/15868100/compile-all-typescript-files-into-single-typescript-file \n    - It _must_ be a solved problem... \n- Failing that, we must namespace things. \n    - Change the way it works, so it recursively imports immediately upon reaching every endpoint.ts file (its too late to disambiguate after they've merged mulitple endpoint.ts). \n    - That recursive import should find the tokens in the export, give them a prefix, and update the importing file too. \n        - This is likely to be very brittle with different non-word boundaries. \n            - It's probably a game of whackamole to find edge cases. It might be easier to convert export script to Javascript, as I'm more comfortable with that. \n\n\n## Roadmap\n\nSee ROADMAP.MD\n\n### Solve the limitation by using esbuild instead\n\n### Loosen the requirement to be in a directory\n\n- make it possible to optionally state/override the endpoint name as a const in endpoint.ts (the generator code would then extract this)","readmeFilename":"README.MD"}