From 6e28f4b606a92ffeee6b15357ab7d0e56324a5be Mon Sep 17 00:00:00 2001 From: Gabriel Massadas Date: Thu, 20 Jun 2024 14:39:14 +0100 Subject: [PATCH] Update C3 OpenAPI template to allow users to pick either Hono or itty-router --- .changeset/metal-seahorses-occur.md | 6 + .../create-cloudflare/templates/openapi/c3.ts | 41 +++- .../templates/openapi/{ts => hono}/.gitignore | 0 .../templates/openapi/{ts => hono}/README.md | 6 +- .../openapi/{ts => hono}/package.json | 5 +- .../{ts => hono}/src/endpoints/taskCreate.ts | 42 ++-- .../{ts => hono}/src/endpoints/taskDelete.ts | 39 ++-- .../openapi/hono/src/endpoints/taskFetch.ts | 81 ++++++++ .../{ts => hono}/src/endpoints/taskList.ts | 51 ++--- .../templates/openapi/hono/src/index.ts | 23 +++ .../templates/openapi/hono/src/types.ts | 10 + .../openapi/{ts => hono}/tsconfig.json | 0 .../{ts => hono}/worker-configuration.d.ts | 0 .../openapi/{ts => hono}/wrangler.toml | 0 .../templates/openapi/itty-router/.gitignore | 171 ++++++++++++++++ .../templates/openapi/itty-router/README.md | 25 +++ .../openapi/itty-router/package.json | 22 ++ .../itty-router/src/endpoints/taskCreate.ts | 58 ++++++ .../itty-router/src/endpoints/taskDelete.ts | 56 ++++++ .../itty-router/src/endpoints/taskFetch.ts | 81 ++++++++ .../itty-router/src/endpoints/taskList.ts | 69 +++++++ .../openapi/itty-router/src/index.ts | 22 ++ .../openapi/itty-router/src/types.ts | 10 + .../openapi/itty-router/tsconfig.json | 32 +++ .../itty-router/worker-configuration.d.ts | 4 + .../openapi/itty-router/wrangler.toml | 107 ++++++++++ .../templates/openapi/ts/src/index.ts | 29 --- .../templates/openapi/ts/src/types.ts | 9 - .../functions/utils/repoAllowlist.ts | 2 +- templates/worker-openapi/.gitignore | 171 +++++++++++++++- templates/worker-openapi/README.md | 42 ++-- templates/worker-openapi/package.json | 23 ++- .../src/endpoints/taskCreate.ts | 58 ++++++ .../src/endpoints/taskDelete.ts | 56 ++++++ .../src/endpoints/taskFetch.ts | 51 +++-- .../worker-openapi/src/endpoints/taskList.ts | 69 +++++++ templates/worker-openapi/src/index.ts | 43 ++-- templates/worker-openapi/src/tasks.ts | 188 ------------------ templates/worker-openapi/src/types.ts | 10 + templates/worker-openapi/tsconfig.json | 32 ++- .../worker-openapi/worker-configuration.d.ts | 3 + templates/worker-openapi/wrangler.toml | 2 +- 42 files changed, 1368 insertions(+), 381 deletions(-) create mode 100644 .changeset/metal-seahorses-occur.md rename packages/create-cloudflare/templates/openapi/{ts => hono}/.gitignore (100%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/README.md (74%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/package.json (76%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/src/endpoints/taskCreate.ts (54%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/src/endpoints/taskDelete.ts (58%) create mode 100644 packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskFetch.ts rename packages/create-cloudflare/templates/openapi/{ts => hono}/src/endpoints/taskList.ts (55%) create mode 100644 packages/create-cloudflare/templates/openapi/hono/src/index.ts create mode 100644 packages/create-cloudflare/templates/openapi/hono/src/types.ts rename packages/create-cloudflare/templates/openapi/{ts => hono}/tsconfig.json (100%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/worker-configuration.d.ts (100%) rename packages/create-cloudflare/templates/openapi/{ts => hono}/wrangler.toml (100%) create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/.gitignore create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/README.md create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/package.json create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskCreate.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskDelete.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskFetch.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskList.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/index.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/src/types.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/tsconfig.json create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/worker-configuration.d.ts create mode 100644 packages/create-cloudflare/templates/openapi/itty-router/wrangler.toml delete mode 100644 packages/create-cloudflare/templates/openapi/ts/src/index.ts delete mode 100644 packages/create-cloudflare/templates/openapi/ts/src/types.ts create mode 100644 templates/worker-openapi/src/endpoints/taskCreate.ts create mode 100644 templates/worker-openapi/src/endpoints/taskDelete.ts rename {packages/create-cloudflare/templates/openapi/ts => templates/worker-openapi}/src/endpoints/taskFetch.ts (57%) create mode 100644 templates/worker-openapi/src/endpoints/taskList.ts delete mode 100644 templates/worker-openapi/src/tasks.ts create mode 100644 templates/worker-openapi/src/types.ts create mode 100644 templates/worker-openapi/worker-configuration.d.ts diff --git a/.changeset/metal-seahorses-occur.md b/.changeset/metal-seahorses-occur.md new file mode 100644 index 000000000000..dcada0f4f10e --- /dev/null +++ b/.changeset/metal-seahorses-occur.md @@ -0,0 +1,6 @@ +--- +"create-cloudflare": minor +"@cloudflare/prerelease-registry": patch +--- + +fix: Update C3 OpenAPI template to allow users to pick either Hono or itty-router diff --git a/packages/create-cloudflare/templates/openapi/c3.ts b/packages/create-cloudflare/templates/openapi/c3.ts index 134640fafb7b..a19ffa5b08ab 100644 --- a/packages/create-cloudflare/templates/openapi/c3.ts +++ b/packages/create-cloudflare/templates/openapi/c3.ts @@ -1,9 +1,48 @@ +import { processArgument } from "@cloudflare/cli/args"; +import type { C3Context } from "types"; + +const frameworkConfig = [ + { + label: "Hono", + value: "hono", + }, + { + label: "itty-router", + value: "itty-router", + }, +]; + export default { configVersion: 1, id: "openapi", displayName: "API starter (OpenAPI compliant)", platform: "workers", copyFiles: { - path: "./ts", + async selectVariant(ctx: C3Context) { + const framework = await processArgument(ctx.args, "framework", { + type: "select", + label: "framework", + question: "Which framework do you want to use?", + options: frameworkConfig, + defaultValue: frameworkConfig[0].value, + validate: (value) => { + if ( + !frameworkConfig.map((obj) => obj.value).includes(String(value)) + ) { + return `Invalid framework \`${value}\`. Please choose one of the following: ${frameworkConfig.map((obj) => obj.value).join(", ")}.`; + } + }, + }); + + return framework; + }, + variants: { + hono: { + path: "./hono", + }, + "itty-router": { + path: "./itty-router", + }, + }, }, }; diff --git a/packages/create-cloudflare/templates/openapi/ts/.gitignore b/packages/create-cloudflare/templates/openapi/hono/.gitignore similarity index 100% rename from packages/create-cloudflare/templates/openapi/ts/.gitignore rename to packages/create-cloudflare/templates/openapi/hono/.gitignore diff --git a/packages/create-cloudflare/templates/openapi/ts/README.md b/packages/create-cloudflare/templates/openapi/hono/README.md similarity index 74% rename from packages/create-cloudflare/templates/openapi/ts/README.md rename to packages/create-cloudflare/templates/openapi/hono/README.md index 8263f12c5409..2aedc3bb1331 100644 --- a/packages/create-cloudflare/templates/openapi/ts/README.md +++ b/packages/create-cloudflare/templates/openapi/hono/README.md @@ -1,6 +1,6 @@ # Cloudflare Workers OpenAPI 3.1 -This is a Cloudflare Worker with OpenAPI 3.1 using [itty-router-openapi](https://github.com/cloudflare/itty-router-openapi). +This is a Cloudflare Worker with OpenAPI 3.1 using [chanfana](https://github.com/cloudflare/chanfana) and [Hono](https://github.com/honojs/hono). This is an example project made to be used as a quick start into building OpenAPI compliant Workers that generates the `openapi.json` schema automatically from code and validates the incoming request to the defined parameters or request body. @@ -16,10 +16,10 @@ This is an example project made to be used as a quick start into building OpenAP 1. Your main router is defined in `src/index.ts`. 2. Each endpoint has its own file in `src/endpoints/`. -3. For more information read the [itty-router-openapi official documentation](https://cloudflare.github.io/itty-router-openapi/). +3. For more information read the [chanfana documentation](https://chanfana.pages.dev/) and [Hono documentation](https://hono.dev/docs). ## Development 1. Run `wrangler dev` to start a local instance of the API. -2. Open `http://localhost:9000/` in your browser to see the Swagger interface where you can try the endpoints. +2. Open `http://localhost:8787/` in your browser to see the Swagger interface where you can try the endpoints. 3. Changes made in the `src/` folder will automatically trigger the server to reload, you only need to refresh the Swagger interface. diff --git a/packages/create-cloudflare/templates/openapi/ts/package.json b/packages/create-cloudflare/templates/openapi/hono/package.json similarity index 76% rename from packages/create-cloudflare/templates/openapi/ts/package.json rename to packages/create-cloudflare/templates/openapi/hono/package.json index 662ac025a73e..597a14d7ebff 100644 --- a/packages/create-cloudflare/templates/openapi/ts/package.json +++ b/packages/create-cloudflare/templates/openapi/hono/package.json @@ -9,11 +9,14 @@ "cf-typegen": "wrangler types" }, "dependencies": { - "@cloudflare/itty-router-openapi": "^1.0.1" + "chanfana": "^2.0.2", + "zod": "^3.23.8", + "hono": "^4.4.7" }, "devDependencies": { "@types/node": "20.8.3", "@types/service-worker-mock": "^2.0.1", + "@cloudflare/workers-types": "^4.20240605.0", "wrangler": "^3.60.3" } } diff --git a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskCreate.ts b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskCreate.ts similarity index 54% rename from packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskCreate.ts rename to packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskCreate.ts index 2acca12b5c7a..798aa82ceddb 100644 --- a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskCreate.ts +++ b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskCreate.ts @@ -1,33 +1,43 @@ -import { - OpenAPIRoute, - OpenAPIRouteSchema, -} from "@cloudflare/itty-router-openapi"; +import { Bool, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; import { Task } from "../types"; export class TaskCreate extends OpenAPIRoute { - static schema: OpenAPIRouteSchema = { + schema = { tags: ["Tasks"], summary: "Create a new Task", - requestBody: Task, + request: { + body: { + content: { + "application/json": { + schema: Task, + }, + }, + }, + }, responses: { "200": { description: "Returns the created task", - schema: { - success: Boolean, - result: { - task: Task, + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), }, }, }, }, }; - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + // Retrieve the validated request body const taskToCreate = data.body; diff --git a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskDelete.ts b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskDelete.ts similarity index 58% rename from packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskDelete.ts rename to packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskDelete.ts index 0a8c41a8eb64..ed6f691e78eb 100644 --- a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskDelete.ts +++ b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskDelete.ts @@ -1,38 +1,39 @@ -import { - OpenAPIRoute, - OpenAPIRouteSchema, - Path, -} from "@cloudflare/itty-router-openapi"; +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; import { Task } from "../types"; export class TaskDelete extends OpenAPIRoute { - static schema: OpenAPIRouteSchema = { + schema = { tags: ["Tasks"], summary: "Delete a Task", - parameters: { - taskSlug: Path(String, { - description: "Task slug", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), }), }, responses: { "200": { description: "Returns if the task was deleted successfully", - schema: { - success: Boolean, - result: { - task: Task, + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), }, }, }, }, }; - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + // Retrieve the validated slug const { taskSlug } = data.params; diff --git a/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskFetch.ts b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskFetch.ts new file mode 100644 index 000000000000..07cf324e856e --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskFetch.ts @@ -0,0 +1,81 @@ +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskFetch extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Get a single Task by slug", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), + }), + }, + responses: { + "200": { + description: "Returns a single task if found", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + "404": { + description: "Task not found", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + error: Str(), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated slug + const { taskSlug } = data.params; + + // Implement your own object fetch here + + const exists = true; + + // @ts-ignore: check if the object exists + if (exists === false) { + return Response.json( + { + success: false, + error: "Object not found", + }, + { + status: 404, + }, + ); + } + + return { + success: true, + task: { + name: "my task", + slug: taskSlug, + description: "this needs to be done", + completed: false, + due_date: new Date().toISOString().slice(0, 10), + }, + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskList.ts b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskList.ts similarity index 55% rename from packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskList.ts rename to packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskList.ts index 7aa074377bfa..8f50506e085d 100644 --- a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskList.ts +++ b/packages/create-cloudflare/templates/openapi/hono/src/endpoints/taskList.ts @@ -1,43 +1,46 @@ -import { - OpenAPIRoute, - OpenAPIRouteSchema, - Query, -} from "@cloudflare/itty-router-openapi"; +import { Bool, Num, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; import { Task } from "../types"; export class TaskList extends OpenAPIRoute { - static schema: OpenAPIRouteSchema = { + schema = { tags: ["Tasks"], summary: "List Tasks", - parameters: { - page: Query(Number, { - description: "Page number", - default: 0, - }), - isCompleted: Query(Boolean, { - description: "Filter by completed flag", - required: false, + request: { + query: z.object({ + page: Num({ + description: "Page number", + default: 0, + }), + isCompleted: Bool({ + description: "Filter by completed flag", + required: false, + }), }), }, responses: { "200": { description: "Returns a list of tasks", - schema: { - success: Boolean, - result: { - tasks: [Task], + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + tasks: Task.array(), + }), + }), + }), }, }, }, }, }; - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + // Retrieve the validated parameters const { page, isCompleted } = data.query; diff --git a/packages/create-cloudflare/templates/openapi/hono/src/index.ts b/packages/create-cloudflare/templates/openapi/hono/src/index.ts new file mode 100644 index 000000000000..46aee732be23 --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/hono/src/index.ts @@ -0,0 +1,23 @@ +import { fromHono } from "chanfana"; +import { Hono } from "hono"; +import { TaskCreate } from "./endpoints/taskCreate"; +import { TaskDelete } from "./endpoints/taskDelete"; +import { TaskFetch } from "./endpoints/taskFetch"; +import { TaskList } from "./endpoints/taskList"; + +// Start a Hono app +const app = new Hono(); + +// Setup OpenAPI registry +const openapi = fromHono(app, { + docs_url: "/", +}); + +// Register OpenAPI endpoints +openapi.get("/api/tasks", TaskList); +openapi.post("/api/tasks", TaskCreate); +openapi.get("/api/tasks/:taskSlug", TaskFetch); +openapi.delete("/api/tasks/:taskSlug", TaskDelete); + +// Export the Hono app +export default app; diff --git a/packages/create-cloudflare/templates/openapi/hono/src/types.ts b/packages/create-cloudflare/templates/openapi/hono/src/types.ts new file mode 100644 index 000000000000..f91817f6c44a --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/hono/src/types.ts @@ -0,0 +1,10 @@ +import { DateTime, Str } from "chanfana"; +import { z } from "zod"; + +export const Task = z.object({ + name: Str({ example: "lorem" }), + slug: Str(), + description: Str({ required: false }), + completed: z.boolean().default(false), + due_date: DateTime(), +}); diff --git a/packages/create-cloudflare/templates/openapi/ts/tsconfig.json b/packages/create-cloudflare/templates/openapi/hono/tsconfig.json similarity index 100% rename from packages/create-cloudflare/templates/openapi/ts/tsconfig.json rename to packages/create-cloudflare/templates/openapi/hono/tsconfig.json diff --git a/packages/create-cloudflare/templates/openapi/ts/worker-configuration.d.ts b/packages/create-cloudflare/templates/openapi/hono/worker-configuration.d.ts similarity index 100% rename from packages/create-cloudflare/templates/openapi/ts/worker-configuration.d.ts rename to packages/create-cloudflare/templates/openapi/hono/worker-configuration.d.ts diff --git a/packages/create-cloudflare/templates/openapi/ts/wrangler.toml b/packages/create-cloudflare/templates/openapi/hono/wrangler.toml similarity index 100% rename from packages/create-cloudflare/templates/openapi/ts/wrangler.toml rename to packages/create-cloudflare/templates/openapi/hono/wrangler.toml diff --git a/packages/create-cloudflare/templates/openapi/itty-router/.gitignore b/packages/create-cloudflare/templates/openapi/itty-router/.gitignore new file mode 100644 index 000000000000..42387803be0c --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/.gitignore @@ -0,0 +1,171 @@ +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars diff --git a/packages/create-cloudflare/templates/openapi/itty-router/README.md b/packages/create-cloudflare/templates/openapi/itty-router/README.md new file mode 100644 index 000000000000..e9153c079613 --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/README.md @@ -0,0 +1,25 @@ +# Cloudflare Workers OpenAPI 3.1 + +This is a Cloudflare Worker with OpenAPI 3.1 using [chanfana](https://github.com/cloudflare/chanfana) and [itty-router](https://github.com/kwhitley/itty-router). + +This is an example project made to be used as a quick start into building OpenAPI compliant Workers that generates the +`openapi.json` schema automatically from code and validates the incoming request to the defined parameters or request body. + +## Get started + +1. Sign up for [Cloudflare Workers](https://workers.dev). The free tier is more than enough for most use cases. +2. Clone this project and install dependencies with `npm install` +3. Run `wrangler login` to login to your Cloudflare account in wrangler +4. Run `wrangler deploy` to publish the API to Cloudflare Workers + +## Project structure + +1. Your main router is defined in `src/index.ts`. +2. Each endpoint has its own file in `src/endpoints/`. +3. For more information read the [chanfana documentation](https://chanfana.pages.dev/) and [itty-router documentation](https://itty.dev/itty-router/). + +## Development + +1. Run `wrangler dev` to start a local instance of the API. +2. Open `http://localhost:8787/` in your browser to see the Swagger interface where you can try the endpoints. +3. Changes made in the `src/` folder will automatically trigger the server to reload, you only need to refresh the Swagger interface. diff --git a/packages/create-cloudflare/templates/openapi/itty-router/package.json b/packages/create-cloudflare/templates/openapi/itty-router/package.json new file mode 100644 index 000000000000..5fd637bb7f48 --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/package.json @@ -0,0 +1,22 @@ +{ + "name": "cloudflare-workers-openapi", + "version": "0.0.1", + "private": true, + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "chanfana": "^2.0.2", + "zod": "^3.23.8", + "itty-router": "^5.0.17" + }, + "devDependencies": { + "@types/node": "20.8.3", + "@types/service-worker-mock": "^2.0.1", + "@cloudflare/workers-types": "^4.20240605.0", + "wrangler": "^3.60.3" + } +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskCreate.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskCreate.ts new file mode 100644 index 000000000000..798aa82ceddb --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskCreate.ts @@ -0,0 +1,58 @@ +import { Bool, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskCreate extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Create a new Task", + request: { + body: { + content: { + "application/json": { + schema: Task, + }, + }, + }, + }, + responses: { + "200": { + description: "Returns the created task", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated request body + const taskToCreate = data.body; + + // Implement your own object insertion here + + // return the new task + return { + success: true, + task: { + name: taskToCreate.name, + slug: taskToCreate.slug, + description: taskToCreate.description, + completed: taskToCreate.completed, + due_date: taskToCreate.due_date, + }, + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskDelete.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskDelete.ts new file mode 100644 index 000000000000..ed6f691e78eb --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskDelete.ts @@ -0,0 +1,56 @@ +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskDelete extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Delete a Task", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), + }), + }, + responses: { + "200": { + description: "Returns if the task was deleted successfully", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated slug + const { taskSlug } = data.params; + + // Implement your own object deletion here + + // Return the deleted task for confirmation + return { + result: { + task: { + name: "Build something awesome with Cloudflare Workers", + slug: taskSlug, + description: "Lorem Ipsum", + completed: true, + due_date: "2022-12-24", + }, + }, + success: true, + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskFetch.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskFetch.ts new file mode 100644 index 000000000000..07cf324e856e --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskFetch.ts @@ -0,0 +1,81 @@ +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskFetch extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Get a single Task by slug", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), + }), + }, + responses: { + "200": { + description: "Returns a single task if found", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + "404": { + description: "Task not found", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + error: Str(), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated slug + const { taskSlug } = data.params; + + // Implement your own object fetch here + + const exists = true; + + // @ts-ignore: check if the object exists + if (exists === false) { + return Response.json( + { + success: false, + error: "Object not found", + }, + { + status: 404, + }, + ); + } + + return { + success: true, + task: { + name: "my task", + slug: taskSlug, + description: "this needs to be done", + completed: false, + due_date: new Date().toISOString().slice(0, 10), + }, + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskList.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskList.ts new file mode 100644 index 000000000000..8f50506e085d --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/endpoints/taskList.ts @@ -0,0 +1,69 @@ +import { Bool, Num, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskList extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "List Tasks", + request: { + query: z.object({ + page: Num({ + description: "Page number", + default: 0, + }), + isCompleted: Bool({ + description: "Filter by completed flag", + required: false, + }), + }), + }, + responses: { + "200": { + description: "Returns a list of tasks", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + tasks: Task.array(), + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated parameters + const { page, isCompleted } = data.query; + + // Implement your own object list here + + return { + success: true, + tasks: [ + { + name: "Clean my room", + slug: "clean-room", + description: null, + completed: false, + due_date: "2025-01-05", + }, + { + name: "Build something awesome with Cloudflare Workers", + slug: "cloudflare-workers", + description: "Lorem Ipsum", + completed: true, + due_date: "2022-12-24", + }, + ], + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/index.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/index.ts new file mode 100644 index 000000000000..eb0cf079c8da --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/index.ts @@ -0,0 +1,22 @@ +import { fromIttyRouter } from "chanfana"; +import { Router } from "itty-router"; +import { TaskCreate } from "./endpoints/taskCreate"; +import { TaskDelete } from "./endpoints/taskDelete"; +import { TaskFetch } from "./endpoints/taskFetch"; +import { TaskList } from "./endpoints/taskList"; + +// Start a new itty-router +const router = Router(); + +// Setup OpenAPI registry +const openapi = fromIttyRouter(router, { + docs_url: "/", +}); + +// Register OpenAPI endpoints +openapi.get("/api/tasks", TaskList); +openapi.post("/api/tasks", TaskCreate); +openapi.get("/api/tasks/:taskSlug", TaskFetch); +openapi.delete("/api/tasks/:taskSlug", TaskDelete); + +export default openapi; diff --git a/packages/create-cloudflare/templates/openapi/itty-router/src/types.ts b/packages/create-cloudflare/templates/openapi/itty-router/src/types.ts new file mode 100644 index 000000000000..f91817f6c44a --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/src/types.ts @@ -0,0 +1,10 @@ +import { DateTime, Str } from "chanfana"; +import { z } from "zod"; + +export const Task = z.object({ + name: Str({ example: "lorem" }), + slug: Str(), + description: Str({ required: false }), + completed: z.boolean().default(false), + due_date: DateTime(), +}); diff --git a/packages/create-cloudflare/templates/openapi/itty-router/tsconfig.json b/packages/create-cloudflare/templates/openapi/itty-router/tsconfig.json new file mode 100644 index 000000000000..196166eb6db1 --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "src", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "inlineSourceMap": false, + "lib": ["esnext"], + "listEmittedFiles": false, + "listFiles": false, + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "pretty": true, + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "strict": false, + "traceResolution": false, + "outDir": "", + "target": "esnext", + "module": "esnext", + "types": [ + "@types/node", + "@types/service-worker-mock", + "@cloudflare/workers-types" + ] + }, + "exclude": ["node_modules", "dist", "tests"], + "include": ["src", "scripts"] +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/worker-configuration.d.ts b/packages/create-cloudflare/templates/openapi/itty-router/worker-configuration.d.ts new file mode 100644 index 000000000000..5b2319b3f29f --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/worker-configuration.d.ts @@ -0,0 +1,4 @@ +// Generated by Wrangler +// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` +interface Env { +} diff --git a/packages/create-cloudflare/templates/openapi/itty-router/wrangler.toml b/packages/create-cloudflare/templates/openapi/itty-router/wrangler.toml new file mode 100644 index 000000000000..8d570a896fe7 --- /dev/null +++ b/packages/create-cloudflare/templates/openapi/itty-router/wrangler.toml @@ -0,0 +1,107 @@ +#:schema node_modules/wrangler/config-schema.json +name = "" +main = "src/index.ts" +compatibility_date = "" + +# Automatically place your workloads in an optimal location to minimize latency. +# If you are running back-end logic in a Worker, running it closer to your back-end infrastructure +# rather than the end user may result in better performance. +# Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement +# [placement] +# mode = "smart" + +# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) +# Docs: +# - https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables +# Note: Use secrets to store sensitive data. +# - https://developers.cloudflare.com/workers/configuration/secrets/ +# [vars] +# MY_VARIABLE = "production_value" + +# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai +# [ai] +# binding = "AI" + +# Bind an Analytics Engine dataset. Use Analytics Engine to write analytics within your Pages Function. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#analytics-engine-datasets +# [[analytics_engine_datasets]] +# binding = "MY_DATASET" + +# Bind a headless browser instance running on Cloudflare's global network. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#browser-rendering +# [browser] +# binding = "MY_BROWSER" + +# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#d1-databases +# [[d1_databases]] +# binding = "MY_DB" +# database_name = "my-database" +# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# Bind a dispatch namespace. Use Workers for Platforms to deploy serverless functions programmatically on behalf of your customers. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#dispatch-namespace-bindings-workers-for-platforms +# [[dispatch_namespaces]] +# binding = "MY_DISPATCHER" +# namespace = "my-namespace" + +# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. +# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects +# [[durable_objects.bindings]] +# name = "MY_DURABLE_OBJECT" +# class_name = "MyDurableObject" + +# Durable Object migrations. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations +# [[migrations]] +# tag = "v1" +# new_classes = ["MyDurableObject"] + +# Bind a Hyperdrive configuration. Use to accelerate access to your existing databases from Cloudflare Workers. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive +# [[hyperdrive]] +# binding = "MY_HYPERDRIVE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# Bind an mTLS certificate. Use to present a client certificate when communicating with another service. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates +# [[mtls_certificates]] +# binding = "MY_CERTIFICATE" +# certificate_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues +# [[queues.producers]] +# binding = "MY_QUEUE" +# queue = "my-queue" + +# Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#queues +# [[queues.consumers]] +# queue = "my-queue" + +# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# Bind another Worker service. Use this binding to call another Worker without network overhead. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings +# [[services]] +# binding = "MY_SERVICE" +# service = "my-service" + +# Bind a Vectorize index. Use to store and query vector embeddings for semantic search, classification and other vector search use-cases. +# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes +# [[vectorize]] +# binding = "MY_INDEX" +# index_name = "my-index" diff --git a/packages/create-cloudflare/templates/openapi/ts/src/index.ts b/packages/create-cloudflare/templates/openapi/ts/src/index.ts deleted file mode 100644 index a3f2f4dec6bc..000000000000 --- a/packages/create-cloudflare/templates/openapi/ts/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { OpenAPIRouter } from "@cloudflare/itty-router-openapi"; -import { TaskCreate } from "./endpoints/taskCreate"; -import { TaskDelete } from "./endpoints/taskDelete"; -import { TaskFetch } from "./endpoints/taskFetch"; -import { TaskList } from "./endpoints/taskList"; - -export const router = OpenAPIRouter({ - docs_url: "/", -}); - -router.get("/api/tasks/", TaskList); -router.post("/api/tasks/", TaskCreate); -router.get("/api/tasks/:taskSlug/", TaskFetch); -router.delete("/api/tasks/:taskSlug/", TaskDelete); - -// 404 for everything else -router.all("*", () => - Response.json( - { - success: false, - error: "Route not found", - }, - { status: 404 } - ) -); - -export default { - fetch: router.handle, -} satisfies ExportedHandler; diff --git a/packages/create-cloudflare/templates/openapi/ts/src/types.ts b/packages/create-cloudflare/templates/openapi/ts/src/types.ts deleted file mode 100644 index f5f0eff5f8d3..000000000000 --- a/packages/create-cloudflare/templates/openapi/ts/src/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { DateTime, Str } from "@cloudflare/itty-router-openapi"; - -export const Task = { - name: new Str({ example: "lorem" }), - slug: String, - description: new Str({ required: false }), - completed: Boolean, - due_date: new DateTime(), -}; diff --git a/packages/prerelease-registry/functions/utils/repoAllowlist.ts b/packages/prerelease-registry/functions/utils/repoAllowlist.ts index b89d1e60232b..74d9da741db1 100644 --- a/packages/prerelease-registry/functions/utils/repoAllowlist.ts +++ b/packages/prerelease-registry/functions/utils/repoAllowlist.ts @@ -2,5 +2,5 @@ export const repos = [ "workers-sdk", "next-on-pages", "pages-plugins", - "itty-router-openapi", + "chanfana", ]; diff --git a/templates/worker-openapi/.gitignore b/templates/worker-openapi/.gitignore index ceb7ecfd0b9b..42387803be0c 100644 --- a/templates/worker-openapi/.gitignore +++ b/templates/worker-openapi/.gitignore @@ -1,2 +1,171 @@ -wrangler-local-state +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# wrangler project + +.dev.vars diff --git a/templates/worker-openapi/README.md b/templates/worker-openapi/README.md index 7b4e9c31b2aa..2aedc3bb1331 100644 --- a/templates/worker-openapi/README.md +++ b/templates/worker-openapi/README.md @@ -1,33 +1,25 @@ -## Template: worker-openapi +# Cloudflare Workers OpenAPI 3.1 -[![Deploy with Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/workers-sdk/tree/main/templates/worker-openapi) +This is a Cloudflare Worker with OpenAPI 3.1 using [chanfana](https://github.com/cloudflare/chanfana) and [Hono](https://github.com/honojs/hono). -This template demonstrates using the [`itty-router-openapi`](https://github.com/cloudflare/itty-router-openapi) package to add openapi 3 schema generation and validation. +This is an example project made to be used as a quick start into building OpenAPI compliant Workers that generates the +`openapi.json` schema automatically from code and validates the incoming request to the defined parameters or request body. -You can try this template in your browser [here](https://worker-openapi-example.radar.cloudflare.com/docs)! +## Get started -## Setup +1. Sign up for [Cloudflare Workers](https://workers.dev). The free tier is more than enough for most use cases. +2. Clone this project and install dependencies with `npm install` +3. Run `wrangler login` to login to your Cloudflare account in wrangler +4. Run `wrangler deploy` to publish the API to Cloudflare Workers -To create a `my-project` directory using this template, run: +## Project structure -```sh -$ npx wrangler generate my-project worker-openapi -# or -$ yarn wrangler generate my-project worker-openapi -# or -$ pnpm wrangler generate my-project worker-openapi -``` +1. Your main router is defined in `src/index.ts`. +2. Each endpoint has its own file in `src/endpoints/`. +3. For more information read the [chanfana documentation](https://chanfana.pages.dev/) and [Hono documentation](https://hono.dev/docs). -## Local development +## Development -Run `wrangler dev` and head to `/docs` our `/redocs` with your browser. - -You'll be greeted with an OpenAPI page that you can use to test and call your endpoints. - -## Deploy - -Once you are ready, you can publish your code by running the following command: - -```sh -$ wrangler deploy -``` +1. Run `wrangler dev` to start a local instance of the API. +2. Open `http://localhost:8787/` in your browser to see the Swagger interface where you can try the endpoints. +3. Changes made in the `src/` folder will automatically trigger the server to reload, you only need to refresh the Swagger interface. diff --git a/templates/worker-openapi/package.json b/templates/worker-openapi/package.json index 6e437522d5bb..597a14d7ebff 100644 --- a/templates/worker-openapi/package.json +++ b/templates/worker-openapi/package.json @@ -1,21 +1,22 @@ { - "name": "worker-openapi", - "version": "1.0.0", + "name": "cloudflare-workers-openapi", + "version": "0.0.1", "private": true, - "description": "Cloudflare workers template with OpenAPI 3 schema generation and validation", - "keywords": [], - "license": "MIT", - "author": "", - "main": "src/index.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "start": "wrangler dev", + "cf-typegen": "wrangler types" }, "dependencies": { - "@cloudflare/itty-router-openapi": "^0.0.10" + "chanfana": "^2.0.2", + "zod": "^3.23.8", + "hono": "^4.4.7" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240605.0", + "@types/node": "20.8.3", "@types/service-worker-mock": "^2.0.1", - "wrangler": "^3.0.0" + "@cloudflare/workers-types": "^4.20240605.0", + "wrangler": "^3.60.3" } } diff --git a/templates/worker-openapi/src/endpoints/taskCreate.ts b/templates/worker-openapi/src/endpoints/taskCreate.ts new file mode 100644 index 000000000000..798aa82ceddb --- /dev/null +++ b/templates/worker-openapi/src/endpoints/taskCreate.ts @@ -0,0 +1,58 @@ +import { Bool, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskCreate extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Create a new Task", + request: { + body: { + content: { + "application/json": { + schema: Task, + }, + }, + }, + }, + responses: { + "200": { + description: "Returns the created task", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated request body + const taskToCreate = data.body; + + // Implement your own object insertion here + + // return the new task + return { + success: true, + task: { + name: taskToCreate.name, + slug: taskToCreate.slug, + description: taskToCreate.description, + completed: taskToCreate.completed, + due_date: taskToCreate.due_date, + }, + }; + } +} diff --git a/templates/worker-openapi/src/endpoints/taskDelete.ts b/templates/worker-openapi/src/endpoints/taskDelete.ts new file mode 100644 index 000000000000..ed6f691e78eb --- /dev/null +++ b/templates/worker-openapi/src/endpoints/taskDelete.ts @@ -0,0 +1,56 @@ +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskDelete extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "Delete a Task", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), + }), + }, + responses: { + "200": { + description: "Returns if the task was deleted successfully", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated slug + const { taskSlug } = data.params; + + // Implement your own object deletion here + + // Return the deleted task for confirmation + return { + result: { + task: { + name: "Build something awesome with Cloudflare Workers", + slug: taskSlug, + description: "Lorem Ipsum", + completed: true, + due_date: "2022-12-24", + }, + }, + success: true, + }; + } +} diff --git a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskFetch.ts b/templates/worker-openapi/src/endpoints/taskFetch.ts similarity index 57% rename from packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskFetch.ts rename to templates/worker-openapi/src/endpoints/taskFetch.ts index fa081c081b8a..7d032ea1d2bf 100644 --- a/packages/create-cloudflare/templates/openapi/ts/src/endpoints/taskFetch.ts +++ b/templates/worker-openapi/src/endpoints/taskFetch.ts @@ -1,45 +1,52 @@ -import { - OpenAPIRoute, - OpenAPIRouteSchema, - Path, -} from "@cloudflare/itty-router-openapi"; +import { Bool, OpenAPIRoute, Str } from "chanfana"; +import { z } from "zod"; import { Task } from "../types"; export class TaskFetch extends OpenAPIRoute { - static schema: OpenAPIRouteSchema = { + schema = { tags: ["Tasks"], summary: "Get a single Task by slug", - parameters: { - taskSlug: Path(String, { - description: "Task slug", + request: { + params: z.object({ + taskSlug: Str({ description: "Task slug" }), }), }, responses: { "200": { description: "Returns a single task if found", - schema: { - success: Boolean, - result: { - task: Task, + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + task: Task, + }), + }), + }), }, }, }, "404": { description: "Task not found", - schema: { - success: Boolean, - error: String, + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + error: Str(), + }), + }), + }, }, }, }, }; - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + // Retrieve the validated slug const { taskSlug } = data.params; diff --git a/templates/worker-openapi/src/endpoints/taskList.ts b/templates/worker-openapi/src/endpoints/taskList.ts new file mode 100644 index 000000000000..8f50506e085d --- /dev/null +++ b/templates/worker-openapi/src/endpoints/taskList.ts @@ -0,0 +1,69 @@ +import { Bool, Num, OpenAPIRoute } from "chanfana"; +import { z } from "zod"; +import { Task } from "../types"; + +export class TaskList extends OpenAPIRoute { + schema = { + tags: ["Tasks"], + summary: "List Tasks", + request: { + query: z.object({ + page: Num({ + description: "Page number", + default: 0, + }), + isCompleted: Bool({ + description: "Filter by completed flag", + required: false, + }), + }), + }, + responses: { + "200": { + description: "Returns a list of tasks", + content: { + "application/json": { + schema: z.object({ + series: z.object({ + success: Bool(), + result: z.object({ + tasks: Task.array(), + }), + }), + }), + }, + }, + }, + }, + }; + + async handle(c) { + // Get validated data + const data = await this.getValidatedData(); + + // Retrieve the validated parameters + const { page, isCompleted } = data.query; + + // Implement your own object list here + + return { + success: true, + tasks: [ + { + name: "Clean my room", + slug: "clean-room", + description: null, + completed: false, + due_date: "2025-01-05", + }, + { + name: "Build something awesome with Cloudflare Workers", + slug: "cloudflare-workers", + description: "Lorem Ipsum", + completed: true, + due_date: "2022-12-24", + }, + ], + }; + } +} diff --git a/templates/worker-openapi/src/index.ts b/templates/worker-openapi/src/index.ts index 1bc2faecb1bf..46aee732be23 100644 --- a/templates/worker-openapi/src/index.ts +++ b/templates/worker-openapi/src/index.ts @@ -1,28 +1,23 @@ -import { OpenAPIRouter } from "@cloudflare/itty-router-openapi"; -import { TaskCreate, TaskDelete, TaskFetch, TaskList } from "./tasks"; +import { fromHono } from "chanfana"; +import { Hono } from "hono"; +import { TaskCreate } from "./endpoints/taskCreate"; +import { TaskDelete } from "./endpoints/taskDelete"; +import { TaskFetch } from "./endpoints/taskFetch"; +import { TaskList } from "./endpoints/taskList"; -const router = OpenAPIRouter({ - schema: { - info: { - title: "Worker OpenAPI Example", - version: "1.0", - }, - }, -}); - -router.get("/api/tasks/", TaskList); -router.post("/api/tasks/", TaskCreate); -router.get("/api/tasks/:taskSlug/", TaskFetch); -router.delete("/api/tasks/:taskSlug/", TaskDelete); +// Start a Hono app +const app = new Hono(); -// Redirect root request to the /docs page -router.original.get("/", (request) => - Response.redirect(`${request.url}docs`, 302) -); +// Setup OpenAPI registry +const openapi = fromHono(app, { + docs_url: "/", +}); -// 404 for everything else -router.all("*", () => new Response("Not Found.", { status: 404 })); +// Register OpenAPI endpoints +openapi.get("/api/tasks", TaskList); +openapi.post("/api/tasks", TaskCreate); +openapi.get("/api/tasks/:taskSlug", TaskFetch); +openapi.delete("/api/tasks/:taskSlug", TaskDelete); -export default { - fetch: router.handle, -}; +// Export the Hono app +export default app; diff --git a/templates/worker-openapi/src/tasks.ts b/templates/worker-openapi/src/tasks.ts deleted file mode 100644 index 3f09560b602e..000000000000 --- a/templates/worker-openapi/src/tasks.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { - Bool, - DateOnly, - Int, - OpenAPIRoute, - Path, - Query, - Str, -} from "@cloudflare/itty-router-openapi"; - -const Task = { - name: new Str({ example: "lorem" }), - slug: String, - description: new Str({ required: false }), - completed: Boolean, - due_date: new DateOnly(), -}; - -export class TaskFetch extends OpenAPIRoute { - static schema = { - tags: ["Tasks"], - summary: "Get a single Task by slug", - parameters: { - taskSlug: Path(Str, { - description: "Task slug", - }), - }, - responses: { - "200": { - schema: { - metaData: {}, - task: Task, - }, - }, - }, - }; - - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { - // Retrieve the validated slug - const { taskSlug } = data; - - // Actually fetch a task using the taskSlug - - return { - metaData: { meta: "data" }, - task: { - name: "my task", - slug: taskSlug, - description: "this needs to be done", - completed: false, - due_date: new Date().toISOString().slice(0, 10), - }, - }; - } -} - -export class TaskCreate extends OpenAPIRoute { - static schema = { - tags: ["Tasks"], - summary: "Create a new Task", - requestBody: Task, - responses: { - "200": { - schema: { - task: Task, - }, - }, - }, - }; - - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { - // Retrieve the validated request body - const { body } = data; - - // Actually insert the task - - // return the new task - return { - metaData: { meta: "data" }, - task: { - name: body.name, - slug: body.slug, - description: body.description, - completed: body.completed, - due_date: body.due_date, - }, - }; - } -} - -export class TaskList extends OpenAPIRoute { - static schema = { - tags: ["Tasks"], - summary: "List Tasks", - parameters: { - page: Query(Int, { - description: "Page number", - default: 0, - }), - isCompleted: Query(Bool, { - description: "Filter by completed flag", - required: false, - }), - }, - responses: { - "200": { - schema: { - tasks: [Task], - }, - }, - }, - }; - - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { - // Retrieve the validated parameters - const { page, isCompleted } = data; - - return { - metaData: { page: page, isCompleted: isCompleted }, - tasks: [ - { - name: "Clean my room", - slug: "clean-room", - description: null, - completed: false, - due_date: "2025-01-05", - }, - { - name: "Build something awesome with Cloudflare Workers", - slug: "cloudflare-workers", - description: "Lorem Ipsum", - completed: true, - due_date: "2022-12-24", - }, - ], - }; - } -} - -export class TaskDelete extends OpenAPIRoute { - static schema = { - tags: ["Tasks"], - summary: "Delete a Task", - parameters: { - taskSlug: Path(Str, { - description: "Task slug", - }), - }, - responses: { - "200": { - schema: { - metaData: {}, - success: Boolean, - }, - }, - }, - }; - - async handle( - request: Request, - env: any, - context: any, - data: Record - ) { - // Retrieve the validated slug - const { taskSlug } = data; - - return { - metaData: { taskSlug: taskSlug }, - success: true, - }; - } -} diff --git a/templates/worker-openapi/src/types.ts b/templates/worker-openapi/src/types.ts new file mode 100644 index 000000000000..f91817f6c44a --- /dev/null +++ b/templates/worker-openapi/src/types.ts @@ -0,0 +1,10 @@ +import { DateTime, Str } from "chanfana"; +import { z } from "zod"; + +export const Task = z.object({ + name: Str({ example: "lorem" }), + slug: Str(), + description: Str({ required: false }), + completed: z.boolean().default(false), + due_date: DateTime(), +}); diff --git a/templates/worker-openapi/tsconfig.json b/templates/worker-openapi/tsconfig.json index 7b91086bfc80..196166eb6db1 100644 --- a/templates/worker-openapi/tsconfig.json +++ b/templates/worker-openapi/tsconfig.json @@ -1,12 +1,32 @@ { "compilerOptions": { - "noEmit": true, - "module": "esnext", - "target": "esnext", + "allowJs": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "src", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "inlineSourceMap": false, "lib": ["esnext"], - "strict": true, + "listEmittedFiles": false, + "listFiles": false, "moduleResolution": "node", - "types": ["@cloudflare/workers-types", "@types/service-worker-mock"] + "noFallthroughCasesInSwitch": true, + "pretty": true, + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "strict": false, + "traceResolution": false, + "outDir": "", + "target": "esnext", + "module": "esnext", + "types": [ + "@types/node", + "@types/service-worker-mock", + "@cloudflare/workers-types" + ] }, - "exclude": ["node_modules"] + "exclude": ["node_modules", "dist", "tests"], + "include": ["src", "scripts"] } diff --git a/templates/worker-openapi/worker-configuration.d.ts b/templates/worker-openapi/worker-configuration.d.ts new file mode 100644 index 000000000000..a3f43d2a431f --- /dev/null +++ b/templates/worker-openapi/worker-configuration.d.ts @@ -0,0 +1,3 @@ +// Generated by Wrangler +// After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` +interface Env {} diff --git a/templates/worker-openapi/wrangler.toml b/templates/worker-openapi/wrangler.toml index 7d0a655df60f..c67458455856 100644 --- a/templates/worker-openapi/wrangler.toml +++ b/templates/worker-openapi/wrangler.toml @@ -1,3 +1,3 @@ name = "worker-openapi" main = "src/index.ts" -compatibility_date = "2022-11-28" +compatibility_date = "2024-06-01"